"""
User authentication and management services.

Provides business logic for:
- User registration and authentication
- OAuth integration
- MFA/2FA management
- Permission and role management
"""

from typing import Optional, Dict, Any
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone
from django_otp.plugins.otp_totp.models import TOTPDevice
from allauth.socialaccount.models import SocialAccount
import logging

from .models import User, UserRole, UserProfile

logger = logging.getLogger(__name__)


class AuthenticationService:
    """Service for handling user authentication operations"""
    
    @staticmethod
    @transaction.atomic
    def register_user(
        email: str,
        password: str,
        username: Optional[str] = None,
        first_name: str = '',
        last_name: str = ''
    ) -> User:
        """
        Register a new user with email and password.
        
        Args:
            email: User's email address
            password: User's password (will be validated and hashed)
            username: Optional username (defaults to email prefix)
            first_name: User's first name
            last_name: User's last name
            
        Returns:
            Created User instance
            
        Raises:
            ValidationError: If email exists or password is invalid
        """
        # Normalize email
        email = email.lower().strip()
        
        # Check if user exists
        if User.objects.filter(email=email).exists():
            raise ValidationError({'email': 'A user with this email already exists.'})
        
        # Set username if not provided
        if not username:
            username = email.split('@')[0]
            # Make unique if needed
            base_username = username
            counter = 1
            while User.objects.filter(username=username).exists():
                username = f"{base_username}{counter}"
                counter += 1
        
        # Validate password
        try:
            validate_password(password)
        except ValidationError as e:
            raise ValidationError({'password': e.messages})
        
        # Create user
        user = User.objects.create_user(
            email=email,
            username=username,
            password=password,
            first_name=first_name,
            last_name=last_name
        )
        
        # Create role (default: user)
        UserRole.objects.create(user=user, role='user')
        
        # Create profile
        UserProfile.objects.create(user=user)
        
        logger.info(f"New user registered: {user.email}")
        return user
    
    @staticmethod
    def authenticate_user(email: str, password: str) -> Optional[User]:
        """
        Authenticate user with email and password.
        
        Args:
            email: User's email address
            password: User's password
            
        Returns:
            User instance if authentication successful, None otherwise
        """
        email = email.lower().strip()
        user = authenticate(username=email, password=password)
        
        if user and user.banned:
            logger.warning(f"Banned user attempted login: {email}")
            raise ValidationError("This account has been banned.")
        
        if user:
            user.last_login = timezone.now()
            user.save(update_fields=['last_login'])
            logger.info(f"User authenticated: {email}")
        
        return user
    
    @staticmethod
    @transaction.atomic
    def create_oauth_user(
        email: str,
        provider: str,
        oauth_sub: str,
        username: Optional[str] = None,
        first_name: str = '',
        last_name: str = '',
        avatar_url: str = ''
    ) -> User:
        """
        Create or get user from OAuth provider.
        
        Args:
            email: User's email from OAuth provider
            provider: OAuth provider name (google, discord)
            oauth_sub: OAuth subject identifier
            username: Optional username
            first_name: User's first name
            last_name: User's last name
            avatar_url: URL to user's avatar
            
        Returns:
            User instance
        """
        email = email.lower().strip()
        
        # Check if user exists with this email
        try:
            user = User.objects.get(email=email)
            # Update OAuth info if not set
            if not user.oauth_provider:
                user.oauth_provider = provider
                user.oauth_sub = oauth_sub
                user.save(update_fields=['oauth_provider', 'oauth_sub'])
            return user
        except User.DoesNotExist:
            pass
        
        # Create new user
        if not username:
            username = email.split('@')[0]
            base_username = username
            counter = 1
            while User.objects.filter(username=username).exists():
                username = f"{base_username}{counter}"
                counter += 1
        
        user = User.objects.create(
            email=email,
            username=username,
            first_name=first_name,
            last_name=last_name,
            avatar_url=avatar_url,
            oauth_provider=provider,
            oauth_sub=oauth_sub
        )
        
        # No password needed for OAuth users
        user.set_unusable_password()
        user.save()
        
        # Create role and profile
        UserRole.objects.create(user=user, role='user')
        UserProfile.objects.create(user=user)
        
        logger.info(f"OAuth user created: {email} via {provider}")
        return user
    
    @staticmethod
    def change_password(user: User, old_password: str, new_password: str) -> bool:
        """
        Change user's password.
        
        Args:
            user: User instance
            old_password: Current password
            new_password: New password
            
        Returns:
            True if successful
            
        Raises:
            ValidationError: If old password is incorrect or new password is invalid
        """
        # Check old password
        if not user.check_password(old_password):
            raise ValidationError({'old_password': 'Incorrect password.'})
        
        # Validate new password
        try:
            validate_password(new_password, user=user)
        except ValidationError as e:
            raise ValidationError({'new_password': e.messages})
        
        # Set new password
        user.set_password(new_password)
        user.save()
        
        logger.info(f"Password changed for user: {user.email}")
        return True
    
    @staticmethod
    def reset_password(user: User, new_password: str) -> bool:
        """
        Reset user's password (admin/forgot password flow).
        
        Args:
            user: User instance
            new_password: New password
            
        Returns:
            True if successful
            
        Raises:
            ValidationError: If new password is invalid
        """
        # Validate new password
        try:
            validate_password(new_password, user=user)
        except ValidationError as e:
            raise ValidationError({'password': e.messages})
        
        # Set new password
        user.set_password(new_password)
        user.save()
        
        logger.info(f"Password reset for user: {user.email}")
        return True


class MFAService:
    """Service for handling multi-factor authentication"""
    
    @staticmethod
    def enable_totp(user: User, device_name: str = 'default') -> TOTPDevice:
        """
        Enable TOTP-based MFA for user.
        
        Args:
            user: User instance
            device_name: Name for the TOTP device
            
        Returns:
            TOTPDevice instance with QR code data
        """
        # Check if device already exists
        device = TOTPDevice.objects.filter(
            user=user,
            name=device_name
        ).first()
        
        if not device:
            device = TOTPDevice.objects.create(
                user=user,
                name=device_name,
                confirmed=False
            )
        
        return device
    
    @staticmethod
    @transaction.atomic
    def confirm_totp(user: User, token: str, device_name: str = 'default') -> bool:
        """
        Confirm TOTP device with verification token.
        
        Args:
            user: User instance
            token: 6-digit TOTP token
            device_name: Name of the TOTP device
            
        Returns:
            True if successful
            
        Raises:
            ValidationError: If token is invalid
        """
        device = TOTPDevice.objects.filter(
            user=user,
            name=device_name
        ).first()
        
        if not device:
            raise ValidationError("TOTP device not found.")
        
        # Verify token
        if not device.verify_token(token):
            raise ValidationError("Invalid verification code.")
        
        # Confirm device
        device.confirmed = True
        device.save()
        
        # Enable MFA on user
        user.mfa_enabled = True
        user.save(update_fields=['mfa_enabled'])
        
        logger.info(f"MFA enabled for user: {user.email}")
        return True
    
    @staticmethod
    def verify_totp(user: User, token: str) -> bool:
        """
        Verify TOTP token for authentication.
        
        Args:
            user: User instance
            token: 6-digit TOTP token
            
        Returns:
            True if valid
        """
        device = TOTPDevice.objects.filter(
            user=user,
            confirmed=True
        ).first()
        
        if not device:
            return False
        
        return device.verify_token(token)
    
    @staticmethod
    @transaction.atomic
    def disable_totp(user: User) -> bool:
        """
        Disable TOTP-based MFA for user.
        
        Args:
            user: User instance
            
        Returns:
            True if successful
        """
        # Delete all TOTP devices
        TOTPDevice.objects.filter(user=user).delete()
        
        # Disable MFA on user
        user.mfa_enabled = False
        user.save(update_fields=['mfa_enabled'])
        
        logger.info(f"MFA disabled for user: {user.email}")
        return True


class RoleService:
    """Service for managing user roles and permissions"""
    
    @staticmethod
    @transaction.atomic
    def assign_role(
        user: User,
        role: str,
        granted_by: Optional[User] = None
    ) -> UserRole:
        """
        Assign role to user.
        
        Args:
            user: User to assign role to
            role: Role name (user, moderator, admin)
            granted_by: User granting the role
            
        Returns:
            UserRole instance
            
        Raises:
            ValidationError: If role is invalid
        """
        valid_roles = ['user', 'moderator', 'admin']
        if role not in valid_roles:
            raise ValidationError(f"Invalid role. Must be one of: {', '.join(valid_roles)}")
        
        # Get or create role
        user_role, created = UserRole.objects.get_or_create(
            user=user,
            defaults={'role': role, 'granted_by': granted_by}
        )
        
        if not created and user_role.role != role:
            user_role.role = role
            user_role.granted_by = granted_by
            user_role.granted_at = timezone.now()
            user_role.save()
        
        logger.info(f"Role '{role}' assigned to user: {user.email}")
        return user_role
    
    @staticmethod
    def has_role(user: User, role: str) -> bool:
        """
        Check if user has specific role.
        
        Args:
            user: User instance
            role: Role name to check
            
        Returns:
            True if user has the role
        """
        try:
            user_role = user.role
            if role == 'moderator':
                return user_role.is_moderator
            elif role == 'admin':
                return user_role.is_admin
            return user_role.role == role
        except UserRole.DoesNotExist:
            return False
    
    @staticmethod
    def get_user_permissions(user: User) -> Dict[str, bool]:
        """
        Get user's permission summary.
        
        Args:
            user: User instance
            
        Returns:
            Dictionary of permissions
        """
        try:
            user_role = user.role
            is_moderator = user_role.is_moderator
            is_admin = user_role.is_admin
        except UserRole.DoesNotExist:
            is_moderator = False
            is_admin = False
        
        return {
            'can_submit': not user.banned,
            'can_moderate': is_moderator and not user.banned,
            'can_admin': is_admin and not user.banned,
            'can_edit_own': not user.banned,
            'can_delete_own': not user.banned,
        }


class UserManagementService:
    """Service for user profile and account management"""
    
    @staticmethod
    @transaction.atomic
    def update_profile(
        user: User,
        **kwargs
    ) -> User:
        """
        Update user profile information.
        
        Args:
            user: User instance
            **kwargs: Fields to update
            
        Returns:
            Updated User instance
        """
        allowed_fields = [
            'first_name', 'last_name', 'username',
            'avatar_url', 'bio'
        ]
        
        updated_fields = []
        for field, value in kwargs.items():
            if field in allowed_fields and hasattr(user, field):
                setattr(user, field, value)
                updated_fields.append(field)
        
        if updated_fields:
            user.save(update_fields=updated_fields)
            logger.info(f"Profile updated for user: {user.email}")
        
        return user
    
    @staticmethod
    @transaction.atomic
    def update_preferences(
        user: User,
        **kwargs
    ) -> UserProfile:
        """
        Update user preferences.
        
        Args:
            user: User instance
            **kwargs: Preference fields to update
            
        Returns:
            Updated UserProfile instance
        """
        profile = user.profile
        
        allowed_fields = [
            'email_notifications',
            'email_on_submission_approved',
            'email_on_submission_rejected',
            'profile_public',
            'show_email'
        ]
        
        updated_fields = []
        for field, value in kwargs.items():
            if field in allowed_fields and hasattr(profile, field):
                setattr(profile, field, value)
                updated_fields.append(field)
        
        if updated_fields:
            profile.save(update_fields=updated_fields)
            logger.info(f"Preferences updated for user: {user.email}")
        
        return profile
    
    @staticmethod
    @transaction.atomic
    def ban_user(
        user: User,
        reason: str,
        banned_by: User
    ) -> User:
        """
        Ban a user.
        
        Args:
            user: User to ban
            reason: Reason for ban
            banned_by: User performing the ban
            
        Returns:
            Updated User instance
        """
        user.ban(reason=reason, banned_by=banned_by)
        logger.warning(f"User banned: {user.email} by {banned_by.email}. Reason: {reason}")
        return user
    
    @staticmethod
    @transaction.atomic
    def unban_user(user: User) -> User:
        """
        Unban a user.
        
        Args:
            user: User to unban
            
        Returns:
            Updated User instance
        """
        user.unban()
        logger.info(f"User unbanned: {user.email}")
        return user
    
    @staticmethod
    def get_user_stats(user: User) -> Dict[str, Any]:
        """
        Get user statistics.
        
        Args:
            user: User instance
            
        Returns:
            Dictionary of user stats
        """
        profile = user.profile
        
        return {
            'total_submissions': profile.total_submissions,
            'approved_submissions': profile.approved_submissions,
            'reputation_score': user.reputation_score,
            'member_since': user.date_joined,
            'last_active': user.last_login,
        }
