"""
MFA Services for Two-Factor Authentication

Provides services for:
1. Twilio SMS verification
2. TOTP (Time-based One-Time Password) for authenticator apps
3. Backup code generation and verification
4. Device trust management
"""

import base64
import hashlib
import hmac
import logging
import secrets
import struct
import time
from typing import Optional, Tuple, List

from django.conf import settings
from django.utils import timezone

logger = logging.getLogger(__name__)


# ============================================================================
# TWILIO SMS SERVICE
# ============================================================================

class TwilioSMSService:
    """
    Service for sending SMS verification codes via Twilio.

    Environment variables required:
    - TWILIO_ACCOUNT_SID
    - TWILIO_AUTH_TOKEN
    - TWILIO_PHONE_NUMBER
    """

    def __init__(self):
        self.account_sid = getattr(settings, 'TWILIO_ACCOUNT_SID', None)
        self.auth_token = getattr(settings, 'TWILIO_AUTH_TOKEN', None)
        self.from_number = getattr(settings, 'TWILIO_PHONE_NUMBER', None)
        self._client = None

    @property
    def client(self):
        """Lazy load Twilio client"""
        if self._client is None:
            try:
                from twilio.rest import Client
                if self.account_sid and self.auth_token:
                    self._client = Client(self.account_sid, self.auth_token)
                else:
                    logger.warning("Twilio credentials not configured")
            except ImportError:
                logger.error("Twilio library not installed. Run: pip install twilio")
        return self._client

    def is_configured(self) -> bool:
        """Check if Twilio is properly configured"""
        return bool(self.account_sid and self.auth_token and self.from_number)

    def send_verification_code(self, to_number: str, code: str,
                                purpose: str = 'verification') -> Tuple[bool, str]:
        """
        Send an SMS verification code.

        Args:
            to_number: Recipient phone number (E.164 format, e.g., +14155551234)
            code: The verification code to send
            purpose: Purpose description for the message

        Returns:
            Tuple of (success: bool, message: str)
        """
        if not self.is_configured():
            return False, "SMS service not configured"

        if not self.client:
            return False, "Failed to initialize SMS client"

        # Format the message
        app_name = getattr(settings, 'APP_NAME', 'SmoothSchedule')
        message_body = f"Your {app_name} verification code is: {code}\n\nThis code expires in 10 minutes."

        try:
            message = self.client.messages.create(
                body=message_body,
                from_=self.from_number,
                to=to_number
            )
            logger.info(f"SMS sent to {to_number[-4:]}, SID: {message.sid}")
            return True, message.sid
        except Exception as e:
            logger.error(f"Failed to send SMS to {to_number[-4:]}: {str(e)}")
            return False, str(e)

    def format_phone_number(self, phone: str, country_code: str = '+1') -> str:
        """
        Format phone number to E.164 format.

        Args:
            phone: Phone number (various formats accepted)
            country_code: Default country code if not provided

        Returns:
            Phone number in E.164 format (e.g., +14155551234)
        """
        # Remove all non-digit characters except leading +
        if phone.startswith('+'):
            cleaned = '+' + ''.join(filter(str.isdigit, phone[1:]))
        else:
            cleaned = ''.join(filter(str.isdigit, phone))

        # Add country code if not present
        if not cleaned.startswith('+'):
            if len(cleaned) == 10:  # US number without country code
                cleaned = country_code + cleaned
            elif len(cleaned) == 11 and cleaned.startswith('1'):
                cleaned = '+' + cleaned
            else:
                cleaned = country_code + cleaned

        return cleaned


# ============================================================================
# TOTP SERVICE (Authenticator App)
# ============================================================================

class TOTPService:
    """
    Service for TOTP (Time-based One-Time Password) generation and verification.
    Compatible with Google Authenticator, Authy, Microsoft Authenticator, etc.

    Uses HMAC-SHA1 with 6-digit codes and 30-second time steps (RFC 6238).
    """

    DIGITS = 6
    TIME_STEP = 30  # seconds
    DRIFT_TOLERANCE = 1  # Allow 1 step drift in either direction

    def __init__(self, issuer: str = None):
        self.issuer = issuer or getattr(settings, 'TOTP_ISSUER', 'SmoothSchedule')

    def generate_secret(self) -> str:
        """
        Generate a new TOTP secret.

        Returns:
            Base32-encoded secret (32 characters)
        """
        # Generate 20 random bytes (160 bits)
        secret_bytes = secrets.token_bytes(20)
        # Encode as base32 (standard for TOTP)
        return base64.b32encode(secret_bytes).decode('utf-8')

    def get_provisioning_uri(self, secret: str, email: str) -> str:
        """
        Generate the provisioning URI for QR code generation.

        Args:
            secret: Base32-encoded secret
            email: User's email (used as account name)

        Returns:
            otpauth:// URI for QR code
        """
        import urllib.parse

        # URL encode the email and issuer
        account = urllib.parse.quote(email)
        issuer = urllib.parse.quote(self.issuer)

        return f"otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&digits={self.DIGITS}"

    def generate_qr_code(self, secret: str, email: str) -> str:
        """
        Generate a QR code image as a data URL.

        Args:
            secret: Base32-encoded secret
            email: User's email

        Returns:
            Data URL string (data:image/png;base64,...)
        """
        try:
            import qrcode
            import io

            uri = self.get_provisioning_uri(secret, email)

            # Generate QR code
            qr = qrcode.QRCode(
                version=1,
                error_correction=qrcode.constants.ERROR_CORRECT_L,
                box_size=10,
                border=4,
            )
            qr.add_data(uri)
            qr.make(fit=True)

            img = qr.make_image(fill_color="black", back_color="white")

            # Convert to base64 data URL
            buffer = io.BytesIO()
            img.save(buffer, format='PNG')
            buffer.seek(0)
            img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')

            return f"data:image/png;base64,{img_base64}"
        except ImportError:
            logger.warning("qrcode library not installed. Run: pip install qrcode[pil]")
            return ""

    def _get_time_counter(self, timestamp: float = None) -> int:
        """Get the time counter for TOTP calculation"""
        if timestamp is None:
            timestamp = time.time()
        return int(timestamp // self.TIME_STEP)

    def _generate_code(self, secret: str, counter: int) -> str:
        """
        Generate a TOTP code for a given counter value.

        Args:
            secret: Base32-encoded secret
            counter: Time-based counter

        Returns:
            6-digit TOTP code
        """
        # Decode the base32 secret
        try:
            key = base64.b32decode(secret.upper())
        except Exception:
            return ""

        # Pack the counter as big-endian 8-byte integer
        counter_bytes = struct.pack('>Q', counter)

        # Generate HMAC-SHA1
        hmac_result = hmac.new(key, counter_bytes, hashlib.sha1).digest()

        # Dynamic truncation
        offset = hmac_result[-1] & 0x0F
        truncated = struct.unpack('>I', hmac_result[offset:offset + 4])[0] & 0x7FFFFFFF

        # Generate code with leading zeros
        code = truncated % (10 ** self.DIGITS)
        return str(code).zfill(self.DIGITS)

    def generate_code(self, secret: str) -> str:
        """
        Generate the current TOTP code.

        Args:
            secret: Base32-encoded secret

        Returns:
            Current 6-digit TOTP code
        """
        counter = self._get_time_counter()
        return self._generate_code(secret, counter)

    def verify_code(self, secret: str, code: str, tolerance: int = None) -> bool:
        """
        Verify a TOTP code with time drift tolerance.

        Args:
            secret: Base32-encoded secret
            code: The code to verify
            tolerance: Number of time steps to check on either side (default: 1)

        Returns:
            True if code is valid within tolerance window
        """
        if tolerance is None:
            tolerance = self.DRIFT_TOLERANCE

        if not code or len(code) != self.DIGITS:
            return False

        counter = self._get_time_counter()

        # Check current time step and adjacent ones for drift tolerance
        for offset in range(-tolerance, tolerance + 1):
            expected_code = self._generate_code(secret, counter + offset)
            if hmac.compare_digest(expected_code, code):
                return True

        return False


# ============================================================================
# BACKUP CODES SERVICE
# ============================================================================

class BackupCodesService:
    """
    Service for generating and verifying backup/recovery codes.
    """

    CODE_LENGTH = 8
    CODE_COUNT = 10

    def generate_codes(self) -> List[str]:
        """
        Generate a set of backup codes.

        Returns:
            List of 10 backup codes (8 characters each, alphanumeric)
        """
        codes = []
        for _ in range(self.CODE_COUNT):
            # Generate code in format: XXXX-XXXX
            part1 = secrets.token_hex(2).upper()
            part2 = secrets.token_hex(2).upper()
            codes.append(f"{part1}-{part2}")
        return codes

    def hash_code(self, code: str) -> str:
        """
        Hash a backup code for secure storage.

        Args:
            code: The backup code to hash

        Returns:
            SHA-256 hash of the code
        """
        # Normalize code (remove dashes, uppercase)
        normalized = code.replace('-', '').upper()
        return hashlib.sha256(normalized.encode()).hexdigest()

    def hash_codes(self, codes: List[str]) -> List[str]:
        """
        Hash multiple backup codes.

        Args:
            codes: List of backup codes

        Returns:
            List of hashed codes
        """
        return [self.hash_code(code) for code in codes]

    def verify_code(self, code: str, hashed_codes: List[str]) -> Tuple[bool, int]:
        """
        Verify a backup code against stored hashes.

        Args:
            code: The backup code to verify
            hashed_codes: List of hashed codes to check against

        Returns:
            Tuple of (is_valid: bool, index: int)
            index is -1 if not found, otherwise the index of the used code
        """
        code_hash = self.hash_code(code)

        for i, stored_hash in enumerate(hashed_codes):
            if hmac.compare_digest(code_hash, stored_hash):
                return True, i

        return False, -1


# ============================================================================
# DEVICE TRUST SERVICE
# ============================================================================

class DeviceTrustService:
    """
    Service for managing trusted device fingerprinting.
    """

    def generate_device_hash(self, ip_address: str, user_agent: str,
                             user_id: int, salt: str = None) -> str:
        """
        Generate a hash to identify a device.

        Args:
            ip_address: Client IP address
            user_agent: Browser user agent string
            user_id: User's ID
            salt: Optional additional salt

        Returns:
            Device fingerprint hash
        """
        # Combine factors for fingerprint
        factors = [
            str(user_id),
            user_agent or '',
            # Note: We use a simplified fingerprint. For production,
            # consider using more factors like canvas fingerprint, etc.
        ]

        if salt:
            factors.append(salt)

        combined = '|'.join(factors)
        return hashlib.sha256(combined.encode()).hexdigest()

    def get_device_name(self, user_agent: str) -> str:
        """
        Extract a friendly device name from user agent.

        Args:
            user_agent: Browser user agent string

        Returns:
            Human-readable device name
        """
        ua = user_agent.lower()

        # Detect browser
        browser = 'Unknown Browser'
        if 'chrome' in ua and 'edg' not in ua:
            browser = 'Chrome'
        elif 'firefox' in ua:
            browser = 'Firefox'
        elif 'safari' in ua and 'chrome' not in ua:
            browser = 'Safari'
        elif 'edg' in ua:
            browser = 'Edge'
        elif 'opera' in ua or 'opr' in ua:
            browser = 'Opera'

        # Detect OS
        os_name = 'Unknown OS'
        if 'windows' in ua:
            os_name = 'Windows'
        elif 'macintosh' in ua or 'mac os' in ua:
            os_name = 'macOS'
        elif 'linux' in ua and 'android' not in ua:
            os_name = 'Linux'
        elif 'android' in ua:
            os_name = 'Android'
        elif 'iphone' in ua or 'ipad' in ua:
            os_name = 'iOS'

        return f"{browser} on {os_name}"


# ============================================================================
# MFA MANAGER (Facade)
# ============================================================================

class MFAManager:
    """
    Main facade for all MFA operations.
    Coordinates between SMS, TOTP, and backup codes.
    """

    def __init__(self):
        self.sms_service = TwilioSMSService()
        self.totp_service = TOTPService()
        self.backup_service = BackupCodesService()
        self.device_service = DeviceTrustService()

    # SMS Methods
    def send_sms_code(self, user, purpose: str = 'LOGIN') -> Tuple[bool, str]:
        """
        Send an SMS verification code to the user.

        Args:
            user: User model instance
            purpose: Purpose of the code (LOGIN, SETUP, etc.)

        Returns:
            Tuple of (success: bool, message: str)
        """
        from .models import MFAVerificationCode

        if not user.phone:
            return False, "No phone number on file"

        if not self.sms_service.is_configured():
            return False, "SMS service not configured"

        # Create verification code record
        verification = MFAVerificationCode.create_for_user(
            user=user,
            purpose=purpose,
            method='SMS'
        )

        # Format phone number
        phone = self.sms_service.format_phone_number(user.phone)

        # Send SMS
        success, message = self.sms_service.send_verification_code(
            phone,
            verification.code,
            purpose=purpose.lower()
        )

        if not success:
            verification.used = True
            verification.save()

        return success, message

    # TOTP Methods
    def setup_totp(self, user) -> dict:
        """
        Initialize TOTP setup for a user.

        Args:
            user: User model instance

        Returns:
            Dict with secret, qr_code, and provisioning_uri
        """
        secret = self.totp_service.generate_secret()
        qr_code = self.totp_service.generate_qr_code(secret, user.email)
        uri = self.totp_service.get_provisioning_uri(secret, user.email)

        # Store secret temporarily (not verified yet)
        user.totp_secret = secret
        user.totp_verified = False
        user.save(update_fields=['totp_secret', 'totp_verified'])

        return {
            'secret': secret,
            'qr_code': qr_code,
            'provisioning_uri': uri,
        }

    def verify_totp_setup(self, user, code: str) -> bool:
        """
        Verify TOTP code during setup.

        Args:
            user: User model instance
            code: The code entered by user

        Returns:
            True if code is valid and setup is complete
        """
        if not user.totp_secret:
            return False

        if self.totp_service.verify_code(user.totp_secret, code):
            user.totp_verified = True
            user.mfa_enabled = True
            if user.mfa_method == 'NONE':
                user.mfa_method = 'TOTP'
            elif user.mfa_method == 'SMS':
                user.mfa_method = 'BOTH'
            user.save(update_fields=['totp_verified', 'mfa_enabled', 'mfa_method'])
            return True

        return False

    def verify_totp(self, user, code: str) -> bool:
        """
        Verify a TOTP code during login.

        Args:
            user: User model instance
            code: The code to verify

        Returns:
            True if code is valid
        """
        if not user.totp_secret or not user.totp_verified:
            return False

        return self.totp_service.verify_code(user.totp_secret, code)

    # Backup Codes Methods
    def generate_backup_codes(self, user) -> List[str]:
        """
        Generate new backup codes for a user.

        Args:
            user: User model instance

        Returns:
            List of backup codes (UNHASHED - show to user once!)
        """
        codes = self.backup_service.generate_codes()
        hashed_codes = self.backup_service.hash_codes(codes)

        user.mfa_backup_codes = hashed_codes
        user.mfa_backup_codes_generated_at = timezone.now()
        user.save(update_fields=['mfa_backup_codes', 'mfa_backup_codes_generated_at'])

        return codes

    def verify_backup_code(self, user, code: str) -> bool:
        """
        Verify and consume a backup code.

        Args:
            user: User model instance
            code: The backup code to verify

        Returns:
            True if code is valid (and now consumed)
        """
        if not user.mfa_backup_codes:
            return False

        is_valid, index = self.backup_service.verify_code(code, user.mfa_backup_codes)

        if is_valid:
            # Remove used code
            codes = list(user.mfa_backup_codes)
            codes.pop(index)
            user.mfa_backup_codes = codes
            user.save(update_fields=['mfa_backup_codes'])
            return True

        return False

    # Device Trust Methods
    def trust_device(self, user, request, trust_days: int = 30):
        """
        Trust the current device for future logins.

        Args:
            user: User model instance
            request: HTTP request object
            trust_days: Number of days to trust the device

        Returns:
            TrustedDevice instance
        """
        from .models import TrustedDevice

        ip = self._get_client_ip(request)
        user_agent = request.META.get('HTTP_USER_AGENT', '')

        device_hash = self.device_service.generate_device_hash(
            ip_address=ip,
            user_agent=user_agent,
            user_id=user.id
        )

        device_name = self.device_service.get_device_name(user_agent)

        return TrustedDevice.create_or_update(
            user=user,
            device_hash=device_hash,
            name=device_name,
            ip_address=ip,
            user_agent=user_agent,
            trust_days=trust_days
        )

    def is_device_trusted(self, user, request) -> bool:
        """
        Check if the current device is trusted.

        Args:
            user: User model instance
            request: HTTP request object

        Returns:
            True if device is trusted
        """
        from .models import TrustedDevice

        ip = self._get_client_ip(request)
        user_agent = request.META.get('HTTP_USER_AGENT', '')

        device_hash = self.device_service.generate_device_hash(
            ip_address=ip,
            user_agent=user_agent,
            user_id=user.id
        )

        try:
            device = TrustedDevice.objects.get(user=user, device_hash=device_hash)
            return device.is_valid()
        except TrustedDevice.DoesNotExist:
            return False

    def _get_client_ip(self, request) -> str:
        """Extract client IP from request"""
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            return x_forwarded_for.split(',')[0].strip()
        return request.META.get('REMOTE_ADDR', '')

    # MFA Status
    def requires_mfa(self, user) -> bool:
        """
        Check if user requires MFA for login.

        Args:
            user: User model instance

        Returns:
            True if MFA is required
        """
        return user.mfa_enabled and user.mfa_method != 'NONE'

    def get_available_methods(self, user) -> List[str]:
        """
        Get available MFA methods for user.

        Args:
            user: User model instance

        Returns:
            List of available methods ('SMS', 'TOTP', 'BACKUP')
        """
        methods = []

        if user.mfa_method in ('SMS', 'BOTH') and user.phone:
            methods.append('SMS')

        if user.mfa_method in ('TOTP', 'BOTH') and user.totp_verified:
            methods.append('TOTP')

        if user.mfa_backup_codes:
            methods.append('BACKUP')

        return methods

    def disable_mfa(self, user):
        """
        Disable all MFA for a user.

        Args:
            user: User model instance
        """
        user.mfa_enabled = False
        user.mfa_method = 'NONE'
        user.totp_secret = ''
        user.totp_verified = False
        user.mfa_backup_codes = []
        user.mfa_backup_codes_generated_at = None
        user.save(update_fields=[
            'mfa_enabled', 'mfa_method', 'totp_secret',
            'totp_verified', 'mfa_backup_codes', 'mfa_backup_codes_generated_at'
        ])

        # Remove all trusted devices
        from .models import TrustedDevice
        TrustedDevice.objects.filter(user=user).delete()


# Create a singleton instance for easy access
mfa_manager = MFAManager()
