#!/usr/bin/env python3
"""
Duo Manager for Media Downloader
Handles Duo Security 2FA Operations

Based on backup-central's implementation
Uses Duo Universal Prompt
"""

import sys
import sqlite3
import secrets
from datetime import datetime, timedelta
from typing import Optional, Dict
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('DuoManager')


class DuoManager:
    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

        # Duo Configuration (from environment variables)
        self.client_id = os.getenv('DUO_CLIENT_ID')
        self.client_secret = os.getenv('DUO_CLIENT_SECRET')
        self.api_host = os.getenv('DUO_API_HOSTNAME')
        self.redirect_url = os.getenv('DUO_REDIRECT_URL', 'https://md.lic.ad/api/auth/2fa/duo/callback')

        # Check if Duo is configured
        self.is_configured = bool(self.client_id and self.client_secret and self.api_host)

        # State store (for OAuth flow)
        self.state_store = {}  # username → state mapping

        # Initialize database
        self._init_database()

        # Initialize Duo client if configured
        self.duo_client = None
        if self.is_configured:
            self._init_duo_client()

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

            # Ensure Duo columns exist in users table
            try:
                cursor.execute("ALTER TABLE users ADD COLUMN duo_enabled INTEGER NOT NULL DEFAULT 0")
            except sqlite3.OperationalError:
                pass  # Column already exists

            try:
                cursor.execute("ALTER TABLE users ADD COLUMN duo_username TEXT")
            except sqlite3.OperationalError:
                pass

            try:
                cursor.execute("ALTER TABLE users ADD COLUMN duo_enrolled_at TEXT")
            except sqlite3.OperationalError:
                pass

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

            conn.commit()

    def _init_duo_client(self):
        """Initialize Duo Universal Prompt client"""
        try:
            from duo_universal import Client
            self.duo_client = Client(
                client_id=self.client_id,
                client_secret=self.client_secret,
                host=self.api_host,
                redirect_uri=self.redirect_url
            )
            logger.info("Duo Universal Prompt client initialized successfully", module="Duo")
        except Exception as e:
            logger.error(f"Error initializing Duo client: {e}", module="Duo")
            self.is_configured = False

    def is_duo_configured(self) -> bool:
        """Check if Duo is properly configured"""
        return self.is_configured

    def get_configuration_status(self) -> Dict:
        """Get Duo configuration status"""
        return {
            'configured': self.is_configured,
            'hasClientId': bool(self.client_id),
            'hasClientSecret': bool(self.client_secret),
            'hasApiHostname': bool(self.api_host),
            'hasRedirectUrl': bool(self.redirect_url),
            'apiHostname': self.api_host if self.api_host else None
        }

    def generate_state(self, username: str, remember_me: bool = False) -> str:
        """
        Generate a state parameter for Duo OAuth flow

        Args:
            username: Username to associate with this state
            remember_me: Whether to remember the user for 30 days

        Returns:
            Random state string
        """
        state = secrets.token_urlsafe(32)
        self.state_store[state] = {
            'username': username,
            'remember_me': remember_me,
            'created_at': datetime.now(),
            'expires_at': datetime.now() + timedelta(minutes=10)
        }
        return state

    def verify_state(self, state: str) -> Optional[tuple]:
        """
        Verify state parameter and return associated username and remember_me

        Args:
            state: State parameter from Duo callback

        Returns:
            Tuple of (username, remember_me) if valid, None otherwise
        """
        if state not in self.state_store:
            return None

        state_data = self.state_store[state]

        # Check if expired
        if datetime.now() > state_data['expires_at']:
            del self.state_store[state]
            return None

        username = state_data['username']
        remember_me = state_data.get('remember_me', False)
        del self.state_store[state]  # One-time use

        return (username, remember_me)

    def create_auth_url(self, username: str, remember_me: bool = False) -> Dict:
        """
        Create Duo authentication URL for user

        Args:
            username: Username to authenticate
            remember_me: Whether to remember the user for 30 days

        Returns:
            Dict with authUrl and state
        """
        if not self.is_configured or not self.duo_client:
            raise Exception("Duo is not configured")

        try:
            # Generate state
            state = self.generate_state(username, remember_me)

            # Get Duo username for this user
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT duo_username FROM users WHERE username = ?", (username,))
                result = cursor.fetchone()

            duo_username = result[0] if result and result[0] else username

            # Create Duo Universal Prompt auth URL
            auth_url = self.duo_client.create_auth_url(duo_username, state)

            self._log_audit(username, 'duo_auth_url_created', True, None, None,
                          'Duo authentication URL created')

            return {
                'authUrl': auth_url,
                'state': state
            }
        except Exception as e:
            self._log_audit(username, 'duo_auth_url_failed', False, None, None,
                          f'Error: {str(e)}')
            raise

    def verify_duo_response(self, duo_code: str, username: str) -> bool:
        """
        Verify Duo authentication response (Universal Prompt)

        Args:
            duo_code: Authorization code from Duo
            username: Expected username

        Returns:
            True if authentication successful
        """
        if not self.is_configured or not self.duo_client:
            return False

        try:
            # Exchange authorization code for 2FA result
            decoded_token = self.duo_client.exchange_authorization_code_for_2fa_result(
                duo_code,
                username
            )

            # Check authentication result
            if decoded_token and decoded_token.get('auth_result', {}).get('result') == 'allow':
                authenticated_username = decoded_token.get('preferred_username')

                # Get Duo username for this user
                with sqlite3.connect(self.db_path) as conn:
                    cursor = conn.cursor()
                    cursor.execute("SELECT duo_username FROM users WHERE username = ?", (username,))
                    result = cursor.fetchone()

                duo_username = result[0] if result and result[0] else username

                # Check if username matches
                if authenticated_username == duo_username:
                    self._log_audit(username, 'duo_verify_success', True, None, None,
                                  'Duo authentication successful')
                    return True
                else:
                    self._log_audit(username, 'duo_verify_failed', False, None, None,
                                  f'Username mismatch: expected {duo_username}, got {authenticated_username}')
                    return False
            else:
                self._log_audit(username, 'duo_verify_failed', False, None, None,
                              'Duo authentication denied')
                return False

        except Exception as e:
            self._log_audit(username, 'duo_verify_failed', False, None, None,
                          f'Error: {str(e)}')
            logger.error(f"Duo verification error: {e}", module="Duo")
            return False

    def _get_application_key(self) -> str:
        """Get or generate application key for Duo"""
        key_file = Path(__file__).parent.parent.parent / '.duo_application_key'

        if key_file.exists():
            with open(key_file, 'r') as f:
                return f.read().strip()

        # Generate new key (at least 40 characters)
        key = secrets.token_urlsafe(40)
        try:
            with open(key_file, 'w') as f:
                f.write(key)
            os.chmod(key_file, 0o600)
        except Exception as e:
            logger.warning(f"Could not save Duo application key: {e}", module="Duo")

        return key

    def enroll_user(self, username: str, duo_username: Optional[str] = None) -> bool:
        """
        Enroll a user in Duo 2FA

        Args:
            username: Username
            duo_username: Optional Duo username (defaults to username)

        Returns:
            True if successful
        """
        if duo_username is None:
            duo_username = username

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

                cursor.execute("""
                    UPDATE users
                    SET duo_enabled = 1, duo_username = ?, duo_enrolled_at = ?
                    WHERE username = ?
                """, (duo_username, datetime.now().isoformat(), username))

                conn.commit()

            self._log_audit(username, 'duo_enrolled', True, None, None,
                          f'Duo enrolled with username: {duo_username}')

            return True
        except Exception as e:
            logger.error(f"Error enrolling user in Duo: {e}", module="Duo")
            return False

    def unenroll_user(self, username: str) -> bool:
        """
        Unenroll a user from Duo 2FA

        Args:
            username: Username

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

                cursor.execute("""
                    UPDATE users
                    SET duo_enabled = 0, duo_username = NULL, duo_enrolled_at = NULL
                    WHERE username = ?
                """, (username,))

                conn.commit()

            self._log_audit(username, 'duo_unenrolled', True, None, None,
                          'Duo unenrolled')

            return True
        except Exception as e:
            logger.error(f"Error unenrolling user from Duo: {e}", module="Duo")
            return False

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

        Args:
            username: Username

        Returns:
            Dict with enabled, duoUsername, enrolledAt
        """
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()

            cursor.execute("""
                SELECT duo_enabled, duo_username, duo_enrolled_at
                FROM users
                WHERE username = ?
            """, (username,))

            row = cursor.fetchone()

        if not row:
            return {'enabled': False, 'duoUsername': None, 'enrolledAt': None}

        return {
            'enabled': bool(row[0]),
            'duoUsername': row[1],
            'enrolledAt': row[2]
        }

    def preauth_user(self, username: str) -> Dict:
        """
        Perform Duo preauth to check enrollment status

        Args:
            username: Username

        Returns:
            Dict with result and status_msg
        """
        if not self.is_configured or not self.duo_client:
            return {'result': 'error', 'status_msg': 'Duo not configured'}

        try:
            # Get Duo username
            user_info = self.get_duo_status(username)
            duo_username = user_info.get('duoUsername', username)

            # Perform preauth
            preauth_result = self.duo_client.preauth(username=duo_username)

            self._log_audit(username, 'duo_preauth', True, None, None,
                          f'Preauth result: {preauth_result.get("result")}')

            return preauth_result

        except Exception as e:
            self._log_audit(username, 'duo_preauth_failed', False, None, None,
                          f'Error: {str(e)}')
            return {'result': 'error', 'status_msg': 'Failed to check Duo enrollment status'}

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

                cursor.execute("""
                    INSERT INTO duo_audit_log
                    (username, action, success, ip_address, user_agent, details, timestamp)
                    VALUES (?, ?, ?, ?, ?, ?, ?)
                """, (username, action, int(success), ip_address, user_agent, details,
                      datetime.now().isoformat()))

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

    def health_check(self) -> Dict:
        """
        Check Duo service health

        Returns:
            Dict with healthy status and details
        """
        if not self.is_configured:
            return {'healthy': False, 'error': 'Duo not configured'}

        try:
            # Simple ping to Duo API
            response = self.duo_client.ping()
            return {'healthy': True, 'response': response}
        except Exception as e:
            logger.error(f"Duo health check failed: {e}", module="Duo")
            return {'healthy': False, 'error': 'Duo service unavailable'}
