# -*- coding: utf-8 -*-
"""
Authentication Models Module

This module contains models related to authentication and authorization
including login attempts, password reset tokens, and session management.

Author: Senior Django Developer
Date: 2024
"""

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.core.validators import MinLengthValidator
from apps.common.models import BaseModel
import secrets
import string
from datetime import timedelta

# Get the custom user model
User = get_user_model()


class LoginAttempt(BaseModel):
    """
    Model to track login attempts for security monitoring.
    
    This model helps in implementing security features like:
    - Account lockout after multiple failed attempts
    - Suspicious activity detection
    - Login analytics and monitoring
    """
    
    # User who attempted to login (can be null for invalid usernames)
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        verbose_name=_("User"),
        help_text=_("User who attempted to login")
    )
    
    # Email/username used in the login attempt
    attempted_email = models.EmailField(
        verbose_name=_("Attempted Email"),
        help_text=_("Email address used in login attempt")
    )
    
    # IP address of the login attempt
    ip_address = models.GenericIPAddressField(
        verbose_name=_("IP Address"),
        help_text=_("IP address from which login was attempted")
    )
    
    # User agent string
    user_agent = models.TextField(
        blank=True,
        verbose_name=_("User Agent"),
        help_text=_("Browser/client user agent string")
    )
    
    # Whether the login attempt was successful
    is_successful = models.BooleanField(
        default=False,
        verbose_name=_("Is Successful"),
        help_text=_("Whether the login attempt was successful")
    )
    
    # Failure reason for unsuccessful attempts
    failure_reason = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("Failure Reason"),
        help_text=_("Reason for login failure")
    )
    
    # Geographic location (optional)
    country = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("Country"),
        help_text=_("Country from which login was attempted")
    )
    
    city = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("City"),
        help_text=_("City from which login was attempted")
    )

    class Meta:
        verbose_name = _("Login Attempt")
        verbose_name_plural = _("Login Attempts")
        ordering = ['-created_at']
        db_table = 'auth_login_attempt'
        indexes = [
            models.Index(fields=['attempted_email', 'created_at']),
            models.Index(fields=['ip_address', 'created_at']),
            models.Index(fields=['is_successful', 'created_at']),
        ]

    def __str__(self):
        """String representation of the login attempt."""
        status = "Success" if self.is_successful else "Failed"
        return f"{self.attempted_email} - {status} ({self.created_at})"
    
    @classmethod
    def get_failed_attempts_count(cls, email, since_hours=1):
        """
        Get count of failed login attempts for an email within specified hours.
        
        Args:
            email (str): Email address to check
            since_hours (int): Number of hours to look back
            
        Returns:
            int: Number of failed attempts
        """
        since_time = timezone.now() - timedelta(hours=since_hours)
        return cls.objects.filter(
            attempted_email=email,
            is_successful=False,
            created_at__gte=since_time
        ).count()
    
    @classmethod
    def is_ip_suspicious(cls, ip_address, max_attempts=10, since_hours=1):
        """
        Check if an IP address has too many failed attempts.
        
        Args:
            ip_address (str): IP address to check
            max_attempts (int): Maximum allowed failed attempts
            since_hours (int): Number of hours to look back
            
        Returns:
            bool: True if IP is suspicious
        """
        since_time = timezone.now() - timedelta(hours=since_hours)
        failed_count = cls.objects.filter(
            ip_address=ip_address,
            is_successful=False,
            created_at__gte=since_time
        ).count()
        return failed_count >= max_attempts


class PasswordResetToken(BaseModel):
    """
    Model for managing password reset tokens.
    
    This model provides secure password reset functionality with:
    - Time-limited tokens
    - One-time use tokens
    - Secure token generation
    """
    
    # User for whom the token is generated
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        verbose_name=_("User"),
        help_text=_("User for whom the token is generated")
    )
    
    # The reset token
    token = models.CharField(
        max_length=100,
        unique=True,
        verbose_name=_("Token"),
        help_text=_("Password reset token")
    )
    
    # Token expiration time
    expires_at = models.DateTimeField(
        verbose_name=_("Expires At"),
        help_text=_("When the token expires")
    )
    
    # Whether the token has been used
    is_used = models.BooleanField(
        default=False,
        verbose_name=_("Is Used"),
        help_text=_("Whether the token has been used")
    )
    
    # When the token was used
    used_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Used At"),
        help_text=_("When the token was used")
    )
    
    # IP address from which the token was requested
    requested_ip = models.GenericIPAddressField(
        verbose_name=_("Requested IP"),
        help_text=_("IP address from which reset was requested")
    )
    
    # IP address from which the token was used
    used_ip = models.GenericIPAddressField(
        null=True,
        blank=True,
        verbose_name=_("Used IP"),
        help_text=_("IP address from which token was used")
    )

    class Meta:
        verbose_name = _("Password Reset Token")
        verbose_name_plural = _("Password Reset Tokens")
        ordering = ['-created_at']
        db_table = 'auth_password_reset_token'
        indexes = [
            models.Index(fields=['token']),
            models.Index(fields=['user', 'is_used']),
            models.Index(fields=['expires_at']),
        ]

    def __str__(self):
        """String representation of the password reset token."""
        return f"Reset token for {self.user.email} ({self.created_at})"
    
    def save(self, *args, **kwargs):
        """
        Override save to generate token and set expiration.
        
        Automatically generates a secure token and sets expiration time
        if not already set.
        """
        if not self.token:
            self.token = self.generate_token()
        
        if not self.expires_at:
            # Token expires in 1 hour by default
            self.expires_at = timezone.now() + timedelta(hours=1)
        
        super().save(*args, **kwargs)
    
    @staticmethod
    def generate_token(length=64):
        """
        Generate a secure random token.
        
        Args:
            length (int): Length of the token
            
        Returns:
            str: Secure random token
        """
        alphabet = string.ascii_letters + string.digits
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def is_valid(self):
        """
        Check if the token is valid (not used and not expired).
        
        Returns:
            bool: True if token is valid
        """
        return (
            not self.is_used and 
            timezone.now() < self.expires_at
        )
    
    def use_token(self, ip_address=None):
        """
        Mark the token as used.
        
        Args:
            ip_address (str): IP address from which token was used
        """
        self.is_used = True
        self.used_at = timezone.now()
        if ip_address:
            self.used_ip = ip_address
        self.save(update_fields=['is_used', 'used_at', 'used_ip'])
    
    @classmethod
    def cleanup_expired(cls):
        """
        Remove expired tokens from the database.
        
        Returns:
            int: Number of tokens deleted
        """
        expired_tokens = cls.objects.filter(
            expires_at__lt=timezone.now()
        )
        count = expired_tokens.count()
        expired_tokens.delete()
        return count


class EmailVerificationToken(BaseModel):
    """
    Model for managing email verification tokens.
    
    This model handles email verification functionality with:
    - Secure token generation
    - Time-limited verification
    - One-time use tokens
    """
    
    # User for whom the token is generated
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        verbose_name=_("User"),
        help_text=_("User for whom the verification token is generated")
    )
    
    # The verification token
    token = models.CharField(
        max_length=100,
        unique=True,
        verbose_name=_("Token"),
        help_text=_("Email verification token")
    )
    
    # Email address to verify
    email = models.EmailField(
        verbose_name=_("Email"),
        help_text=_("Email address to verify")
    )
    
    # Token expiration time
    expires_at = models.DateTimeField(
        verbose_name=_("Expires At"),
        help_text=_("When the token expires")
    )
    
    # Whether the token has been used
    is_used = models.BooleanField(
        default=False,
        verbose_name=_("Is Used"),
        help_text=_("Whether the token has been used")
    )
    
    # When the token was used
    used_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Used At"),
        help_text=_("When the token was used")
    )
    
    # IP address from which verification was completed
    verified_ip = models.GenericIPAddressField(
        null=True,
        blank=True,
        verbose_name=_("Verified IP"),
        help_text=_("IP address from which email was verified")
    )

    class Meta:
        verbose_name = _("Email Verification Token")
        verbose_name_plural = _("Email Verification Tokens")
        ordering = ['-created_at']
        db_table = 'auth_email_verification_token'
        indexes = [
            models.Index(fields=['token']),
            models.Index(fields=['user', 'is_used']),
            models.Index(fields=['email']),
        ]

    def __str__(self):
        """String representation of the email verification token."""
        return f"Verification token for {self.email} ({self.created_at})"
    
    def save(self, *args, **kwargs):
        """
        Override save to generate token and set expiration.
        
        Automatically generates a secure token and sets expiration time
        if not already set.
        """
        if not self.token:
            self.token = self.generate_token()
        
        if not self.expires_at:
            # Token expires in 24 hours by default
            self.expires_at = timezone.now() + timedelta(hours=24)
        
        super().save(*args, **kwargs)
    
    @staticmethod
    def generate_token(length=64):
        """
        Generate a secure random token.
        
        Args:
            length (int): Length of the token
            
        Returns:
            str: Secure random token
        """
        alphabet = string.ascii_letters + string.digits
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def is_valid(self):
        """
        Check if the token is valid (not used and not expired).
        
        Returns:
            bool: True if token is valid
        """
        return (
            not self.is_used and 
            timezone.now() < self.expires_at
        )
    
    def verify_email(self, ip_address=None):
        """
        Mark the token as used and verify the user's email.
        
        Args:
            ip_address (str): IP address from which verification was completed
        """
        # Mark token as used
        self.is_used = True
        self.used_at = timezone.now()
        if ip_address:
            self.verified_ip = ip_address
        self.save(update_fields=['is_used', 'used_at', 'verified_ip'])
        
        # Verify the user's email
        self.user.verify_email()


class UserSession(BaseModel):
    """
    Model for tracking user sessions.
    
    This model provides enhanced session management with:
    - Session tracking across devices
    - Security monitoring
    - Session invalidation capabilities
    """
    
    # User who owns the session
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        verbose_name=_("User"),
        help_text=_("User who owns this session")
    )
    
    # Django session key
    session_key = models.CharField(
        max_length=40,
        unique=True,
        verbose_name=_("Session Key"),
        help_text=_("Django session key")
    )
    
    # IP address of the session
    ip_address = models.GenericIPAddressField(
        verbose_name=_("IP Address"),
        help_text=_("IP address of the session")
    )
    
    # User agent string
    user_agent = models.TextField(
        blank=True,
        verbose_name=_("User Agent"),
        help_text=_("Browser/client user agent string")
    )
    
    # Device information
    device_type = models.CharField(
        max_length=50,
        blank=True,
        verbose_name=_("Device Type"),
        help_text=_("Type of device (mobile, desktop, tablet)")
    )
    
    browser = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("Browser"),
        help_text=_("Browser name and version")
    )
    
    operating_system = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("Operating System"),
        help_text=_("Operating system name and version")
    )
    
    # Session status
    is_active = models.BooleanField(
        default=True,
        verbose_name=_("Is Active"),
        help_text=_("Whether the session is currently active")
    )
    
    # Last activity timestamp
    last_activity = models.DateTimeField(
        auto_now=True,
        verbose_name=_("Last Activity"),
        help_text=_("When the session was last active")
    )
    
    # Session expiration
    expires_at = models.DateTimeField(
        verbose_name=_("Expires At"),
        help_text=_("When the session expires")
    )
    
    # Geographic location (optional)
    country = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("Country"),
        help_text=_("Country of the session")
    )
    
    city = models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("City"),
        help_text=_("City of the session")
    )

    class Meta:
        verbose_name = _("User Session")
        verbose_name_plural = _("User Sessions")
        ordering = ['-last_activity']
        db_table = 'auth_user_session'
        indexes = [
            models.Index(fields=['user', 'is_active']),
            models.Index(fields=['session_key']),
            models.Index(fields=['last_activity']),
        ]

    def __str__(self):
        """String representation of the user session."""
        return f"{self.user.email} - {self.ip_address} ({self.last_activity})"
    
    def is_expired(self):
        """
        Check if the session is expired.
        
        Returns:
            bool: True if session is expired
        """
        return timezone.now() > self.expires_at
    
    def invalidate(self):
        """
        Invalidate the session.
        
        Marks the session as inactive and can be used to
        force logout from specific devices.
        """
        self.is_active = False
        self.save(update_fields=['is_active'])
    
    @classmethod
    def cleanup_expired(cls):
        """
        Remove expired sessions from the database.
        
        Returns:
            int: Number of sessions deleted
        """
        expired_sessions = cls.objects.filter(
            expires_at__lt=timezone.now()
        )
        count = expired_sessions.count()
        expired_sessions.delete()
        return count
    
    @classmethod
    def get_active_sessions_for_user(cls, user):
        """
        Get all active sessions for a user.
        
        Args:
            user: User instance
            
        Returns:
            QuerySet: Active sessions for the user
        """
        return cls.objects.filter(
            user=user,
            is_active=True,
            expires_at__gt=timezone.now()
        )
