#!/usr/bin/env python3
"""
Passkey Manager for Media Downloader
Handles WebAuthn/Passkey operations

Based on backup-central's implementation
"""

import sys
import sqlite3
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from pathlib import Path
import os

# Add parent path to allow imports from modules
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from modules.universal_logger import get_logger

logger = get_logger('PasskeyManager')

from webauthn import (
    generate_registration_options,
    verify_registration_response,
    generate_authentication_options,
    verify_authentication_response,
    options_to_json
)
from webauthn.helpers import base64url_to_bytes, bytes_to_base64url
from webauthn.helpers.structs import (
    PublicKeyCredentialDescriptor,
    UserVerificationRequirement,
    AttestationConveyancePreference,
    AuthenticatorSelectionCriteria,
    ResidentKeyRequirement
)


class PasskeyManager:
    def __init__(self, db_path: str = None):
        if db_path is None:
            db_path = str(Path(__file__).parent.parent.parent / 'database' / 'auth.db')

        self.db_path = db_path

        # WebAuthn Configuration
        self.rp_name = 'Media Downloader'
        self.rp_id = self._get_rp_id()
        self.origin = self._get_origin()

        # Challenge timeout (5 minutes)
        self.challenge_timeout = timedelta(minutes=5)

        # Challenge storage (in-memory for now, could use Redis/DB for production)
        self.challenges = {}  # username → challenge mapping

        # Initialize database
        self._init_database()

    def _get_rp_id(self) -> str:
        """Get Relying Party ID (domain)"""
        return os.getenv('WEBAUTHN_RP_ID', 'md.lic.ad')

    def _get_origin(self) -> str:
        """Get Origin URL"""
        return os.getenv('WEBAUTHN_ORIGIN', 'https://md.lic.ad')

    def _init_database(self):
        """Initialize passkey-related database tables"""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()

            # Passkeys credentials table
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS passkey_credentials (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL,
                    credential_id TEXT NOT NULL UNIQUE,
                    public_key TEXT NOT NULL,
                    sign_count INTEGER NOT NULL DEFAULT 0,
                    transports TEXT,
                    device_name TEXT,
                    aaguid TEXT,
                    created_at TEXT NOT NULL,
                    last_used TEXT,
                    FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
                )
            """)

            # Passkey audit log
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS passkey_audit_log (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL,
                    action TEXT NOT NULL,
                    success INTEGER NOT NULL,
                    credential_id TEXT,
                    ip_address TEXT,
                    user_agent TEXT,
                    details TEXT,
                    timestamp TEXT NOT NULL
                )
            """)

            # Add passkey enabled column to users
            try:
                cursor.execute("ALTER TABLE users ADD COLUMN passkey_enabled INTEGER NOT NULL DEFAULT 0")
            except sqlite3.OperationalError:
                pass

            conn.commit()

    def get_user_credentials(self, username: str) -> List[Dict]:
        """
        Get all credentials for a user

        Args:
            username: Username

        Returns:
            List of credential dicts
        """
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()

            cursor.execute("""
                SELECT credential_id, public_key, sign_count, device_name,
                       created_at, last_used_at
                FROM passkey_credentials
                WHERE user_id = ?
            """, (username,))

            rows = cursor.fetchall()

        credentials = []
        for row in rows:
            credentials.append({
                'credential_id': row[0],
                'public_key': row[1],
                'sign_count': row[2],
                'transports': None,
                'device_name': row[3],
                'aaguid': None,
                'created_at': row[4],
                'last_used': row[5]
            })

        return credentials

    def generate_registration_options(self, username: str, email: Optional[str] = None) -> Dict:
        """
        Generate registration options for a new passkey

        Args:
            username: Username
            email: Optional email for display name

        Returns:
            Registration options for client
        """
        try:
            # Get existing credentials
            existing_credentials = self.get_user_credentials(username)

            # Create exclude credentials list
            exclude_credentials = []
            for cred in existing_credentials:
                exclude_credentials.append(
                    PublicKeyCredentialDescriptor(
                        id=base64url_to_bytes(cred['credential_id']),
                        transports=cred.get('transports')
                    )
                )

            # Generate registration options
            options = generate_registration_options(
                rp_id=self.rp_id,
                rp_name=self.rp_name,
                user_id=username.encode('utf-8'),
                user_name=username,
                user_display_name=email or username,
                timeout=60000,  # 60 seconds
                attestation=AttestationConveyancePreference.NONE,
                exclude_credentials=exclude_credentials,
                authenticator_selection=AuthenticatorSelectionCriteria(
                    resident_key=ResidentKeyRequirement.PREFERRED,
                    user_verification=UserVerificationRequirement.PREFERRED
                ),
                supported_pub_key_algs=[-7, -257]  # ES256, RS256
            )

            # Store challenge
            challenge_str = bytes_to_base64url(options.challenge)
            self.challenges[username] = {
                'challenge': challenge_str,
                'type': 'registration',
                'created_at': datetime.now(),
                'expires_at': datetime.now() + self.challenge_timeout
            }

            self._log_audit(username, 'passkey_registration_options_generated', True,
                          None, None, None, 'Registration options generated')

            # Convert to JSON-serializable format
            options_dict = options_to_json(options)

            return json.loads(options_dict)

        except Exception as e:
            self._log_audit(username, 'passkey_registration_options_failed', False,
                          None, None, None, f'Error: {str(e)}')
            raise

    def verify_registration(self, username: str, credential_data: Dict,
                           device_name: Optional[str] = None) -> Dict:
        """
        Verify registration response and store credential

        Args:
            username: Username
            credential_data: Registration response from client
            device_name: Optional device name

        Returns:
            Dict with success status
        """
        try:
            # Get stored challenge
            if username not in self.challenges:
                raise Exception("No registration in progress")

            challenge_data = self.challenges[username]

            # Check if expired
            if datetime.now() > challenge_data['expires_at']:
                del self.challenges[username]
                raise Exception("Challenge expired")

            if challenge_data['type'] != 'registration':
                raise Exception("Invalid challenge type")

            expected_challenge = base64url_to_bytes(challenge_data['challenge'])

            # Verify registration response
            verification = verify_registration_response(
                credential=credential_data,
                expected_challenge=expected_challenge,
                expected_rp_id=self.rp_id,
                expected_origin=self.origin
            )

            # Store credential
            credential_id = bytes_to_base64url(verification.credential_id)
            public_key = bytes_to_base64url(verification.credential_public_key)

            # Extract transports if available
            transports = credential_data.get('response', {}).get('transports', [])

            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()

                cursor.execute("""
                    INSERT INTO passkey_credentials
                    (user_id, credential_id, public_key, sign_count, device_name, created_at)
                    VALUES (?, ?, ?, ?, ?, ?)
                """, (
                    username,
                    credential_id,
                    public_key,
                    verification.sign_count,
                    device_name,
                    datetime.now().isoformat()
                ))

                # Enable passkey for user
                cursor.execute("""
                    UPDATE users SET passkey_enabled = 1
                    WHERE username = ?
                """, (username,))

                conn.commit()

            # Clean up challenge
            del self.challenges[username]

            self._log_audit(username, 'passkey_registered', True, credential_id,
                          None, None, f'Passkey registered: {device_name or "Unknown device"}')

            return {
                'success': True,
                'credentialId': credential_id
            }

        except Exception as e:
            logger.error(f"Passkey registration verification failed for {username}: {str(e)}", module="Passkey")
            logger.debug(f"Passkey registration detailed error:", exc_info=True, module="Passkey")
            self._log_audit(username, 'passkey_registration_failed', False, None,
                          None, None, f'Error: {str(e)}')
            raise

    def generate_authentication_options(self, username: Optional[str] = None) -> Dict:
        """
        Generate authentication options for passkey login

        Args:
            username: Optional username (if None, allows usernameless login)

        Returns:
            Authentication options for client
        """
        try:
            allow_credentials = []

            # If username provided, get their credentials
            if username:
                user_credentials = self.get_user_credentials(username)
                for cred in user_credentials:
                    allow_credentials.append(
                        PublicKeyCredentialDescriptor(
                            id=base64url_to_bytes(cred['credential_id']),
                            transports=cred.get('transports')
                        )
                    )

            # Generate authentication options
            options = generate_authentication_options(
                rp_id=self.rp_id,
                timeout=60000,  # 60 seconds
                allow_credentials=allow_credentials if allow_credentials else None,
                user_verification=UserVerificationRequirement.PREFERRED
            )

            # Store challenge
            challenge_str = bytes_to_base64url(options.challenge)
            self.challenges[username or '__anonymous__'] = {
                'challenge': challenge_str,
                'type': 'authentication',
                'created_at': datetime.now(),
                'expires_at': datetime.now() + self.challenge_timeout
            }

            self._log_audit(username or 'anonymous', 'passkey_authentication_options_generated',
                          True, None, None, None, 'Authentication options generated')

            # Convert to JSON-serializable format
            options_dict = options_to_json(options)

            return json.loads(options_dict)

        except Exception as e:
            self._log_audit(username or 'anonymous', 'passkey_authentication_options_failed',
                          False, None, None, None, f'Error: {str(e)}')
            raise

    def verify_authentication(self, username: str, credential_data: Dict) -> Dict:
        """
        Verify authentication response

        Args:
            username: Username
            credential_data: Authentication response from client

        Returns:
            Dict with success status and verified username
        """
        try:
            # Get stored challenge
            challenge_key = username or '__anonymous__'
            if challenge_key not in self.challenges:
                raise Exception("No authentication in progress")

            challenge_data = self.challenges[challenge_key]

            # Check if expired
            if datetime.now() > challenge_data['expires_at']:
                del self.challenges[challenge_key]
                raise Exception("Challenge expired")

            if challenge_data['type'] != 'authentication':
                raise Exception("Invalid challenge type")

            expected_challenge = base64url_to_bytes(challenge_data['challenge'])

            # Get credential ID from response
            credential_id = credential_data.get('id') or credential_data.get('rawId')
            if not credential_id:
                raise Exception("No credential ID in response")

            # Find credential in database
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()

                cursor.execute("""
                    SELECT user_id, public_key, sign_count
                    FROM passkey_credentials
                    WHERE credential_id = ?
                """, (credential_id,))

                row = cursor.fetchone()

                if not row:
                    raise Exception("Credential not found")

                verified_username, public_key_b64, current_sign_count = row

                # Verify authentication response
                verification = verify_authentication_response(
                    credential=credential_data,
                    expected_challenge=expected_challenge,
                    expected_rp_id=self.rp_id,
                    expected_origin=self.origin,
                    credential_public_key=base64url_to_bytes(public_key_b64),
                    credential_current_sign_count=current_sign_count
                )

                # Update sign count and last used
                cursor.execute("""
                    UPDATE passkey_credentials
                    SET sign_count = ?, last_used_at = ?
                    WHERE credential_id = ?
                """, (verification.new_sign_count, datetime.now().isoformat(), credential_id))

                conn.commit()

            # Clean up challenge
            del self.challenges[challenge_key]

            self._log_audit(verified_username, 'passkey_authentication_success', True,
                          credential_id, None, None, 'Passkey authentication successful')

            return {
                'success': True,
                'username': verified_username
            }

        except Exception as e:
            self._log_audit(username or 'anonymous', 'passkey_authentication_failed', False,
                          None, None, None, f'Error: {str(e)}')
            raise

    def remove_credential(self, username: str, credential_id: str) -> bool:
        """
        Remove a passkey credential

        Args:
            username: Username
            credential_id: Credential ID to remove

        Returns:
            True if successful
        """
        try:
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()

                # Debug: List all credentials for this user
                cursor.execute("SELECT credential_id FROM passkey_credentials WHERE user_id = ?", (username,))
                all_creds = cursor.fetchall()
                logger.debug(f"All credentials for {username}: {all_creds}", module="Passkey")
                logger.debug(f"Looking for credential_id: '{credential_id}'", module="Passkey")

                cursor.execute("""
                    DELETE FROM passkey_credentials
                    WHERE user_id = ? AND credential_id = ?
                """, (username, credential_id))

                deleted = cursor.rowcount > 0
                logger.debug(f"Delete operation rowcount: {deleted}", module="Passkey")

                # Check if user has any remaining credentials
                cursor.execute("""
                    SELECT COUNT(*) FROM passkey_credentials
                    WHERE user_id = ?
                """, (username,))

                remaining = cursor.fetchone()[0]

                # If no credentials left, disable passkey
                if remaining == 0:
                    cursor.execute("""
                        UPDATE users SET passkey_enabled = 0
                        WHERE username = ?
                    """, (username,))

                conn.commit()

            if deleted:
                self._log_audit(username, 'passkey_removed', True, credential_id,
                              None, None, 'Passkey credential removed')

            return deleted

        except Exception as e:
            logger.error(f"Error removing passkey for {username}: {e}", module="Passkey")
            return False

    def get_passkey_status(self, username: str) -> Dict:
        """
        Get passkey status for a user

        Args:
            username: Username

        Returns:
            Dict with enabled status and credential count
        """
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()

            cursor.execute("""
                SELECT COUNT(*) FROM passkey_credentials
                WHERE user_id = ?
            """, (username,))

            count = cursor.fetchone()[0]

        return {
            'enabled': count > 0,
            'credentialCount': count
        }

    def list_credentials(self, username: str) -> List[Dict]:
        """
        List all credentials for a user (without sensitive data)

        Args:
            username: Username

        Returns:
            List of credential info dicts
        """
        credentials = self.get_user_credentials(username)

        # Remove sensitive data
        safe_credentials = []
        for cred in credentials:
            safe_credentials.append({
                'credentialId': cred['credential_id'],
                'deviceName': cred.get('device_name', 'Unknown device'),
                'createdAt': cred['created_at'],
                'lastUsed': cred.get('last_used'),
                'transports': cred.get('transports', [])
            })

        return safe_credentials

    def _log_audit(self, username: str, action: str, success: bool,
                   credential_id: Optional[str], ip_address: Optional[str],
                   user_agent: Optional[str], details: Optional[str] = None):
        """Log passkey audit event"""
        try:
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()

                cursor.execute("""
                    INSERT INTO passkey_audit_log
                    (user_id, action, success, credential_id, ip_address, user_agent, details, created_at)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """, (username, action, int(success), credential_id, ip_address, user_agent, details,
                      datetime.now().isoformat()))

                conn.commit()
        except Exception as e:
            logger.error(f"Error logging passkey audit: {e}", module="Passkey")
