# -*- coding: utf-8 -*-
"""
Accounts Signals

This module defines Django signals for the accounts application,
handling automatic creation and updates of related models when
certain events occur (e.g., user creation, login, logout).

Signals included:
- User profile creation on user registration
- User access creation for new users
- Security logging for authentication events
- Session management signals
- API key usage tracking
- Role assignment notifications

Author: Adtlas Development Team
Version: 1.0.0
Created: 2024
"""

from django.db.models.signals import (
    post_save, post_delete, pre_save, pre_delete
)
from django.contrib.auth.signals import (
    user_logged_in, user_logged_out, user_login_failed
)
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.utils import timezone
from django.contrib.sessions.models import Session
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
from django.contrib.sites.models import Site
from django.urls import reverse
import logging
import json
from datetime import timedelta
from ipware import get_client_ip

from .models import (
    UserProfile, UserAccess, UserSession, SecurityLog,
    APIKey, Role, UserRole, Permission, RolePermission
)

# Configure logging
logger = logging.getLogger('accounts.signals')

# ============================================================================
# User Profile and Access Signals
# ============================================================================

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    """
    Automatically create a UserProfile when a new User is created.
    
    This signal ensures that every user has an associated profile
    with default settings and generates an API key for the user.
    
    Args:
        sender: The User model class
        instance: The User instance that was saved
        created: Boolean indicating if this is a new user
        **kwargs: Additional keyword arguments
    """
    if created:
        try:
            # Create user profile with default settings
            profile = UserProfile.objects.create(
                user=instance,
                user_type='viewer',  # Default user type
                timezone='UTC',
                language='en',
                theme='light',
                email_notifications=True,
                sms_notifications=False,
                two_factor_enabled=False
            )
            
            # Generate API key for the user
            profile.generate_api_key()
            
            logger.info(
                f'Created user profile for user: {instance.username} (ID: {instance.id})'
            )
            
        except Exception as e:
            logger.error(
                f'Failed to create user profile for user {instance.username}: {str(e)}'
            )

@receiver(post_save, sender=User)
def create_user_access(sender, instance, created, **kwargs):
    """
    Automatically create a UserAccess entry when a new User is created.
    
    This provides default access credentials that can be configured later.
    
    Args:
        sender: The User model class
        instance: The User instance that was saved
        created: Boolean indicating if this is a new user
        **kwargs: Additional keyword arguments
    """
    if created:
        try:
            # Create default user access entries for common services
            services = ['ftp', 'sftp']
            
            for service in services:
                UserAccess.objects.create(
                    user=instance,
                    service_type=service,
                    username=instance.username,
                    password='',  # To be set by admin or user
                    host='',
                    port=21 if service == 'ftp' else 22,
                    is_active=False,  # Inactive until configured
                    notes=f'Default {service.upper()} access for {instance.username}'
                )
            
            logger.info(
                f'Created user access entries for user: {instance.username}'
            )
            
        except Exception as e:
            logger.error(
                f'Failed to create user access for user {instance.username}: {str(e)}'
            )

@receiver(post_save, sender=UserProfile)
def user_profile_updated(sender, instance, created, **kwargs):
    """
    Handle user profile updates and log security-relevant changes.
    
    Args:
        sender: The UserProfile model class
        instance: The UserProfile instance that was saved
        created: Boolean indicating if this is a new profile
        **kwargs: Additional keyword arguments
    """
    if not created:
        # Check for security-relevant changes
        if hasattr(instance, '_state') and instance._state.adding is False:
            # Log security-relevant profile changes
            changes = []
            
            # Check if user type changed
            if hasattr(instance, '_original_user_type'):
                if instance._original_user_type != instance.user_type:
                    changes.append(f'User type changed from {instance._original_user_type} to {instance.user_type}')
            
            # Check if two-factor authentication was enabled/disabled
            if hasattr(instance, '_original_two_factor'):
                if instance._original_two_factor != instance.two_factor_enabled:
                    status = 'enabled' if instance.two_factor_enabled else 'disabled'
                    changes.append(f'Two-factor authentication {status}')
            
            # Log changes if any
            if changes:
                SecurityLog.objects.create(
                    event_type='profile_change',
                    user=instance.user,
                    severity='medium',
                    description=f'User profile updated: {", ".join(changes)}',
                    event_data=json.dumps({
                        'changes': changes,
                        'user_id': instance.user.id,
                        'timestamp': timezone.now().isoformat()
                    })
                )
                
                logger.info(
                    f'User profile updated for {instance.user.username}: {", ".join(changes)}'
                )

# ============================================================================
# Authentication Signals
# ============================================================================

@receiver(user_logged_in)
def user_login_handler(sender, request, user, **kwargs):
    """
    Handle successful user login events.
    
    Creates a user session record and logs the login event.
    
    Args:
        sender: The sender of the signal
        request: The HTTP request object
        user: The User instance that logged in
        **kwargs: Additional keyword arguments
    """
    try:
        # Get client IP address
        client_ip, is_routable = get_client_ip(request)
        if client_ip is None:
            client_ip = '127.0.0.1'
        
        # Get user agent
        user_agent = request.META.get('HTTP_USER_AGENT', '')
        
        # Determine device type from user agent
        device_type = 'desktop'
        if 'Mobile' in user_agent:
            device_type = 'mobile'
        elif 'Tablet' in user_agent:
            device_type = 'tablet'
        
        # Create or update user session
        session_key = request.session.session_key
        if session_key:
            user_session, created = UserSession.objects.get_or_create(
                user=user,
                session_key=session_key,
                defaults={
                    'ip_address': client_ip,
                    'user_agent': user_agent,
                    'device_type': device_type,
                    'location': '',  # Could be populated with GeoIP
                    'is_active': True,
                    'last_activity': timezone.now()
                }
            )
            
            if not created:
                # Update existing session
                user_session.last_activity = timezone.now()
                user_session.is_active = True
                user_session.save()
        
        # Reset failed login attempts on successful login
        try:
            profile = user.userprofile
            if profile.failed_login_attempts > 0:
                profile.failed_login_attempts = 0
                profile.account_locked_until = None
                profile.save()
        except UserProfile.DoesNotExist:
            pass
        
        # Update last login IP in profile
        try:
            profile = user.userprofile
            profile.last_login_ip = client_ip
            profile.save()
        except UserProfile.DoesNotExist:
            pass
        
        # Log successful login
        SecurityLog.objects.create(
            event_type='login_success',
            user=user,
            severity='low',
            description=f'User {user.username} logged in successfully',
            ip_address=client_ip,
            user_agent=user_agent,
            event_data=json.dumps({
                'user_id': user.id,
                'username': user.username,
                'ip_address': client_ip,
                'device_type': device_type,
                'timestamp': timezone.now().isoformat()
            })
        )
        
        logger.info(
            f'User {user.username} logged in from {client_ip} using {device_type}'
        )
        
    except Exception as e:
        logger.error(
            f'Error handling login for user {user.username}: {str(e)}'
        )

@receiver(user_logged_out)
def user_logout_handler(sender, request, user, **kwargs):
    """
    Handle user logout events.
    
    Updates the user session and logs the logout event.
    
    Args:
        sender: The sender of the signal
        request: The HTTP request object
        user: The User instance that logged out (may be None)
        **kwargs: Additional keyword arguments
    """
    try:
        if user:
            # Get client IP address
            client_ip, is_routable = get_client_ip(request)
            if client_ip is None:
                client_ip = '127.0.0.1'
            
            # Update user session
            session_key = request.session.session_key
            if session_key:
                try:
                    user_session = UserSession.objects.get(
                        user=user,
                        session_key=session_key,
                        is_active=True
                    )
                    user_session.is_active = False
                    user_session.last_activity = timezone.now()
                    user_session.save()
                except UserSession.DoesNotExist:
                    pass
            
            # Log logout
            SecurityLog.objects.create(
                event_type='logout',
                user=user,
                severity='low',
                description=f'User {user.username} logged out',
                ip_address=client_ip,
                user_agent=request.META.get('HTTP_USER_AGENT', ''),
                event_data=json.dumps({
                    'user_id': user.id,
                    'username': user.username,
                    'ip_address': client_ip,
                    'timestamp': timezone.now().isoformat()
                })
            )
            
            logger.info(
                f'User {user.username} logged out from {client_ip}'
            )
    
    except Exception as e:
        logger.error(
            f'Error handling logout: {str(e)}'
        )

@receiver(user_login_failed)
def user_login_failed_handler(sender, credentials, request, **kwargs):
    """
    Handle failed login attempts.
    
    Tracks failed attempts and implements account lockout if necessary.
    
    Args:
        sender: The sender of the signal
        credentials: The credentials used in the failed attempt
        request: The HTTP request object
        **kwargs: Additional keyword arguments
    """
    try:
        username = credentials.get('username', '')
        
        # Get client IP address
        client_ip, is_routable = get_client_ip(request)
        if client_ip is None:
            client_ip = '127.0.0.1'
        
        # Try to find the user
        user = None
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            pass
        
        # Increment failed login attempts if user exists
        if user:
            try:
                profile = user.userprofile
                profile.failed_login_attempts += 1
                
                # Lock account if too many failed attempts
                max_attempts = getattr(settings, 'MAX_LOGIN_ATTEMPTS', 5)
                lockout_duration = getattr(settings, 'ACCOUNT_LOCKOUT_DURATION', 30)  # minutes
                
                if profile.failed_login_attempts >= max_attempts:
                    profile.account_locked_until = timezone.now() + timedelta(minutes=lockout_duration)
                    
                    # Log account lockout
                    SecurityLog.objects.create(
                        event_type='account_locked',
                        user=user,
                        severity='high',
                        description=f'Account locked due to {max_attempts} failed login attempts',
                        ip_address=client_ip,
                        user_agent=request.META.get('HTTP_USER_AGENT', ''),
                        event_data=json.dumps({
                            'user_id': user.id,
                            'username': username,
                            'failed_attempts': profile.failed_login_attempts,
                            'locked_until': profile.account_locked_until.isoformat(),
                            'ip_address': client_ip,
                            'timestamp': timezone.now().isoformat()
                        })
                    )
                    
                    logger.warning(
                        f'Account {username} locked due to {max_attempts} failed login attempts from {client_ip}'
                    )
                
                profile.save()
                
            except UserProfile.DoesNotExist:
                pass
        
        # Log failed login attempt
        SecurityLog.objects.create(
            event_type='login_failure',
            user=user,
            severity='medium',
            description=f'Failed login attempt for username: {username}',
            ip_address=client_ip,
            user_agent=request.META.get('HTTP_USER_AGENT', ''),
            event_data=json.dumps({
                'username': username,
                'ip_address': client_ip,
                'user_exists': user is not None,
                'timestamp': timezone.now().isoformat()
            })
        )
        
        logger.warning(
            f'Failed login attempt for {username} from {client_ip}'
        )
        
    except Exception as e:
        logger.error(
            f'Error handling failed login: {str(e)}'
        )

# ============================================================================
# Role and Permission Signals
# ============================================================================

@receiver(post_save, sender=UserRole)
def user_role_assigned(sender, instance, created, **kwargs):
    """
    Handle user role assignment events.
    
    Logs role assignments and sends notifications if configured.
    
    Args:
        sender: The UserRole model class
        instance: The UserRole instance that was saved
        created: Boolean indicating if this is a new assignment
        **kwargs: Additional keyword arguments
    """
    if created:
        try:
            # Log role assignment
            SecurityLog.objects.create(
                event_type='role_assigned',
                user=instance.user,
                severity='medium',
                description=f'Role "{instance.role.name}" assigned to user {instance.user.username}',
                event_data=json.dumps({
                    'user_id': instance.user.id,
                    'username': instance.user.username,
                    'role_id': instance.role.id,
                    'role_name': instance.role.name,
                    'assigned_by': instance.assigned_by.username if instance.assigned_by else 'System',
                    'expires_at': instance.expires_at.isoformat() if instance.expires_at else None,
                    'timestamp': timezone.now().isoformat()
                })
            )
            
            # Send email notification if enabled
            if hasattr(settings, 'SEND_ROLE_ASSIGNMENT_EMAILS') and settings.SEND_ROLE_ASSIGNMENT_EMAILS:
                try:
                    send_role_assignment_email(instance)
                except Exception as e:
                    logger.error(f'Failed to send role assignment email: {str(e)}')
            
            logger.info(
                f'Role "{instance.role.name}" assigned to user {instance.user.username}'
            )
            
        except Exception as e:
            logger.error(
                f'Error handling role assignment: {str(e)}'
            )

@receiver(post_delete, sender=UserRole)
def user_role_removed(sender, instance, **kwargs):
    """
    Handle user role removal events.
    
    Args:
        sender: The UserRole model class
        instance: The UserRole instance that was deleted
        **kwargs: Additional keyword arguments
    """
    try:
        # Log role removal
        SecurityLog.objects.create(
            event_type='role_removed',
            user=instance.user,
            severity='medium',
            description=f'Role "{instance.role.name}" removed from user {instance.user.username}',
            event_data=json.dumps({
                'user_id': instance.user.id,
                'username': instance.user.username,
                'role_id': instance.role.id,
                'role_name': instance.role.name,
                'timestamp': timezone.now().isoformat()
            })
        )
        
        logger.info(
            f'Role "{instance.role.name}" removed from user {instance.user.username}'
        )
        
    except Exception as e:
        logger.error(
            f'Error handling role removal: {str(e)}'
        )

# ============================================================================
# API Key Signals
# ============================================================================

@receiver(post_save, sender=APIKey)
def api_key_created(sender, instance, created, **kwargs):
    """
    Handle API key creation events.
    
    Args:
        sender: The APIKey model class
        instance: The APIKey instance that was saved
        created: Boolean indicating if this is a new key
        **kwargs: Additional keyword arguments
    """
    if created:
        try:
            # Log API key creation
            SecurityLog.objects.create(
                event_type='api_key_created',
                user=instance.user,
                severity='medium',
                description=f'API key "{instance.name}" created for user {instance.user.username}',
                event_data=json.dumps({
                    'user_id': instance.user.id,
                    'username': instance.user.username,
                    'key_name': instance.name,
                    'key_id': instance.id,
                    'expires_at': instance.expires_at.isoformat() if instance.expires_at else None,
                    'timestamp': timezone.now().isoformat()
                })
            )
            
            logger.info(
                f'API key "{instance.name}" created for user {instance.user.username}'
            )
            
        except Exception as e:
            logger.error(
                f'Error handling API key creation: {str(e)}'
            )

@receiver(pre_delete, sender=APIKey)
def api_key_deleted(sender, instance, **kwargs):
    """
    Handle API key deletion events.
    
    Args:
        sender: The APIKey model class
        instance: The APIKey instance being deleted
        **kwargs: Additional keyword arguments
    """
    try:
        # Log API key deletion
        SecurityLog.objects.create(
            event_type='api_key_deleted',
            user=instance.user,
            severity='medium',
            description=f'API key "{instance.name}" deleted for user {instance.user.username}',
            event_data=json.dumps({
                'user_id': instance.user.id,
                'username': instance.user.username,
                'key_name': instance.name,
                'key_id': instance.id,
                'usage_count': instance.usage_count,
                'timestamp': timezone.now().isoformat()
            })
        )
        
        logger.info(
            f'API key "{instance.name}" deleted for user {instance.user.username}'
        )
        
    except Exception as e:
        logger.error(
            f'Error handling API key deletion: {str(e)}'
        )

# ============================================================================
# Session Cleanup Signals
# ============================================================================

@receiver(post_delete, sender=Session)
def session_deleted(sender, instance, **kwargs):
    """
    Handle Django session deletion to clean up UserSession records.
    
    Args:
        sender: The Session model class
        instance: The Session instance that was deleted
        **kwargs: Additional keyword arguments
    """
    try:
        # Mark corresponding UserSession as inactive
        UserSession.objects.filter(
            session_key=instance.session_key,
            is_active=True
        ).update(
            is_active=False,
            last_activity=timezone.now()
        )
        
    except Exception as e:
        logger.error(
            f'Error handling session deletion: {str(e)}'
        )

# ============================================================================
# Helper Functions
# ============================================================================

def send_role_assignment_email(user_role):
    """
    Send email notification for role assignment.
    
    Args:
        user_role: The UserRole instance
    """
    try:
        user = user_role.user
        role = user_role.role
        
        # Get site information
        try:
            site = Site.objects.get_current()
            domain = site.domain
            site_name = site.name
        except:
            domain = getattr(settings, 'ALLOWED_HOSTS', ['localhost'])[0]
            site_name = 'Adtlas'
        
        # Prepare email context
        context = {
            'user': user,
            'role': role,
            'user_role': user_role,
            'site_name': site_name,
            'domain': domain,
            'login_url': f'https://{domain}{reverse("accounts:login")}',
        }
        
        # Render email templates
        subject = f'Role Assignment: {role.name}'
        html_message = render_to_string('accounts/emails/role_assigned.html', context)
        text_message = render_to_string('accounts/emails/role_assigned.txt', context)
        
        # Send email
        send_mail(
            subject=subject,
            message=text_message,
            html_message=html_message,
            from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@adtlas.com'),
            recipient_list=[user.email],
            fail_silently=False
        )
        
        logger.info(
            f'Role assignment email sent to {user.email} for role {role.name}'
        )
        
    except Exception as e:
        logger.error(
            f'Failed to send role assignment email: {str(e)}'
        )
        raise

def cleanup_expired_sessions():
    """
    Clean up expired user sessions.
    
    This function should be called periodically (e.g., via a cron job)
    to clean up old and expired sessions.
    """
    try:
        # Mark sessions as inactive if they haven't been active for a while
        inactive_threshold = timezone.now() - timedelta(
            hours=getattr(settings, 'SESSION_INACTIVE_HOURS', 24)
        )
        
        updated = UserSession.objects.filter(
            is_active=True,
            last_activity__lt=inactive_threshold
        ).update(
            is_active=False
        )
        
        logger.info(
            f'Marked {updated} user sessions as inactive due to inactivity'
        )
        
        # Delete very old session records
        delete_threshold = timezone.now() - timedelta(
            days=getattr(settings, 'SESSION_CLEANUP_DAYS', 30)
        )
        
        deleted_count, _ = UserSession.objects.filter(
            created_at__lt=delete_threshold
        ).delete()
        
        logger.info(
            f'Deleted {deleted_count} old user session records'
        )
        
    except Exception as e:
        logger.error(
            f'Error during session cleanup: {str(e)}'
        )

def cleanup_old_security_logs():
    """
    Clean up old security log entries.
    
    This function should be called periodically to prevent
    the security log table from growing too large.
    """
    try:
        # Delete old security logs (keep for configured number of days)
        retention_days = getattr(settings, 'SECURITY_LOG_RETENTION_DAYS', 90)
        delete_threshold = timezone.now() - timedelta(days=retention_days)
        
        deleted_count, _ = SecurityLog.objects.filter(
            created_at__lt=delete_threshold
        ).delete()
        
        logger.info(
            f'Deleted {deleted_count} old security log entries'
        )
        
    except Exception as e:
        logger.error(
            f'Error during security log cleanup: {str(e)}'
        )