"""
Security Module
Authentication, authorization, rate limiting, and input sanitization
"""

import os
import base64
import hashlib
import secrets
import logging
from typing import Optional, Dict, Tuple
from datetime import datetime, timedelta
from collections import defaultdict
from functools import wraps

logger = logging.getLogger(__name__)


class TokenManager:
    """Gestionnaire de tokens API sécurisé"""

    def __init__(self, script_dir: str):
        self.script_dir = script_dir
        self.token_file = os.path.join(script_dir, '.api_token')
        self._token: Optional[str] = None
        self._token_hash: Optional[str] = None

    def get_token(self) -> str:
        """Récupérer ou générer le token API"""
        if self._token:
            return self._token

        # 1. Essayer variable d'environnement
        token = os.environ.get('DASHBOARD_API_TOKEN')

        # 2. Essayer fichier
        if not token and os.path.exists(self.token_file):
            try:
                with open(self.token_file, 'r') as f:
                    token = f.read().strip()
            except Exception as e:
                logger.error(f"Erreur lecture token: {e}")

        # 3. Générer nouveau token sécurisé
        if not token:
            token = secrets.token_urlsafe(32)
            try:
                # Sauvegarder avec permissions restreintes
                with open(self.token_file, 'w') as f:
                    f.write(token)
                # Restreindre les permissions (Unix)
                if os.name != 'nt':
                    os.chmod(self.token_file, 0o600)
                logger.warning(f"Nouveau token généré: {token[:8]}...")
            except Exception as e:
                logger.error(f"Erreur sauvegarde token: {e}")

        self._token = token
        self._token_hash = hashlib.sha256(token.encode()).hexdigest()
        return token

    def verify_token(self, provided_token: str) -> bool:
        """Vérifier un token avec protection timing attack"""
        expected = self.get_token()
        return secrets.compare_digest(provided_token, expected)


class RateLimiter:
    """Rate limiter simple basé sur IP"""

    def __init__(self, max_requests: int = 100, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window = timedelta(seconds=window_seconds)
        self.requests: Dict[str, list] = defaultdict(list)

    def is_allowed(self, client_ip: str) -> Tuple[bool, Optional[int]]:
        """
        Vérifier si une requête est autorisée
        Returns: (allowed, retry_after_seconds)
        """
        now = datetime.now()

        # Nettoyer les anciennes requêtes
        cutoff = now - self.window
        self.requests[client_ip] = [
            req_time for req_time in self.requests[client_ip]
            if req_time > cutoff
        ]

        # Vérifier la limite
        if len(self.requests[client_ip]) >= self.max_requests:
            oldest = min(self.requests[client_ip])
            retry_after = int((oldest + self.window - now).total_seconds())
            return False, max(1, retry_after)

        # Ajouter la requête
        self.requests[client_ip].append(now)
        return True, None

    def reset(self, client_ip: str):
        """Réinitialiser le compteur pour une IP"""
        if client_ip in self.requests:
            del self.requests[client_ip]


class SecurityValidator:
    """Validateur de sécurité pour les inputs"""

    @staticmethod
    def is_safe_filename(filename: str) -> bool:
        """Vérifier qu'un nom de fichier est sûr"""
        if not filename:
            return False

        # Interdire caractères dangereux
        dangerous = ['..', '/', '\\', '\0', '\n', '\r']
        for char in dangerous:
            if char in filename:
                return False

        # Autoriser seulement certains caractères
        import re
        if not re.match(r'^[a-zA-Z0-9_\-\.]+$', filename):
            return False

        return True

    @staticmethod
    def is_safe_symbol(symbol: str) -> bool:
        """Vérifier qu'un symbole crypto est valide"""
        if not symbol or len(symbol) > 20:
            return False

        import re
        return bool(re.match(r'^[A-Z0-9]+USDT$', symbol))

    @staticmethod
    def sanitize_log_message(message: str) -> str:
        """Nettoyer un message de log pour éviter injection"""
        if not message:
            return ""

        # Remplacer les caractères de contrôle
        sanitized = message.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')

        # Limiter la longueur
        if len(sanitized) > 500:
            sanitized = sanitized[:497] + "..."

        return sanitized


class AuthMiddleware:
    """Middleware d'authentification — HTTP Basic Auth (user/password)"""

    # Routes publiques — pas d'auth requise
    # /api/mobile-summary et /api/crypto-news : lecture seule, pas de données sensibles
    # Nécessaire car les navigateurs mobiles (Safari iOS) interceptent le 401+WWW-Authenticate
    # avant que le JS puisse le gérer, ce qui génère "Failed to fetch"
    PUBLIC_PATHS = {'/api/health', '/api/mobile-summary', '/api/crypto-news', '/mobile.html',
                     '/crypto_trading_ia_logo.png', '/crypto_trading_ia_icon.png',
                     '/manifest.json', '/manifest-mobile.json', '/favicon.ico'}

    def __init__(self, token_manager: TokenManager, rate_limiter: RateLimiter):
        self.token_manager = token_manager
        self.rate_limiter = rate_limiter
        self._credentials: Optional[Tuple[str, str]] = None  # (username, password_hash)
        self._cred_file = os.path.join(token_manager.script_dir, '.dashboard_auth')
        self._load_or_create_credentials()

    # ── Gestion des credentials ──────────────────────────────────────────────

    def _hash_password(self, password: str, salt: str) -> str:
        """Hash le mot de passe avec sel SHA-256"""
        return hashlib.sha256(f"{salt}:{password}".encode()).hexdigest()

    def _load_or_create_credentials(self):
        """Charge ou génère le couple login/mot de passe"""
        # 1. Variables d'environnement (prioritaires)
        env_user = os.environ.get('DASHBOARD_USER')
        env_pass = os.environ.get('DASHBOARD_PASSWORD')
        if env_user and env_pass:
            salt = 'env'
            self._credentials = (env_user, self._hash_password(env_pass, salt), salt)
            logger.info(f"🔐 Auth: credentials depuis variables d'environnement (user={env_user})")
            return

        # 2. Fichier .dashboard_auth
        if os.path.exists(self._cred_file):
            try:
                with open(self._cred_file, 'r') as f:
                    parts = f.read().strip().split(':')
                if len(parts) == 3:
                    username, salt, password_hash = parts
                    self._credentials = (username, password_hash, salt)
                    logger.info(f"🔐 Auth: credentials chargés (user={username})")
                    return
            except Exception as e:
                logger.error(f"Erreur lecture credentials: {e}")

        # 3. Générer de nouveaux credentials
        username = 'admin'
        password = secrets.token_urlsafe(12)  # Ex: "Xk9mP2rQvL4n_B7w"
        salt = secrets.token_hex(8)
        password_hash = self._hash_password(password, salt)
        self._credentials = (username, password_hash, salt)

        try:
            with open(self._cred_file, 'w') as f:
                f.write(f"{username}:{salt}:{password_hash}")
            if os.name != 'nt':
                os.chmod(self._cred_file, 0o600)
        except Exception as e:
            logger.error(f"Erreur sauvegarde credentials: {e}")

        logger.warning("=" * 60)
        logger.warning("🔐 NOUVEAUX CREDENTIALS DASHBOARD GÉNÉRÉS")
        logger.warning(f"   Utilisateur : {username}")
        logger.warning(f"   Mot de passe: {password}")
        logger.warning(f"   Fichier     : {self._cred_file}")
        logger.warning("   Conservez ces informations en lieu sûr !")
        logger.warning("=" * 60)
        print("\n" + "=" * 60)
        print("🔐 CREDENTIALS DASHBOARD (première utilisation)")
        print(f"   Utilisateur : {username}")
        print(f"   Mot de passe: {password}")
        print("=" * 60 + "\n")

    def change_password(self, new_password: str, new_username: Optional[str] = None) -> bool:
        """Changer le mot de passe (et optionnellement le login)"""
        try:
            username = new_username or (self._credentials[0] if self._credentials else 'admin')
            salt = secrets.token_hex(8)
            password_hash = self._hash_password(new_password, salt)
            self._credentials = (username, password_hash, salt)
            with open(self._cred_file, 'w') as f:
                f.write(f"{username}:{salt}:{password_hash}")
            if os.name != 'nt':
                os.chmod(self._cred_file, 0o600)
            logger.info(f"🔐 Mot de passe changé pour l'utilisateur {username}")
            return True
        except Exception as e:
            logger.error(f"Erreur changement mot de passe: {e}")
            return False

    def verify_basic_auth(self, auth_header: str) -> bool:
        """Vérifie un header Authorization: Basic <base64(user:pass)>"""
        if not auth_header.startswith('Basic '):
            return False
        try:
            decoded = base64.b64decode(auth_header[6:]).decode('utf-8')
            if ':' not in decoded:
                return False
            provided_user, provided_pass = decoded.split(':', 1)
        except Exception:
            return False

        if not self._credentials:
            return False
        username, password_hash, salt = self._credentials

        # Comparaison timing-safe
        user_ok = secrets.compare_digest(provided_user, username)
        pass_hash = self._hash_password(provided_pass, salt)
        pass_ok = secrets.compare_digest(pass_hash, password_hash)
        return user_ok and pass_ok

    # ── Vérification auth ────────────────────────────────────────────────────

    def check_auth(self, headers: dict, client_ip: str, allow_local: bool = True) -> Tuple[bool, Optional[str]]:
        """
        Vérifier l'authentification HTTP Basic Auth.
        Returns: (authorized, error_message)
        """
        # Connexions locales exemptées
        if allow_local and client_ip in ('127.0.0.1', '::1', 'localhost'):
            return True, None

        auth_header = headers.get('Authorization', '')

        # Support Basic Auth (navigateur) — prioritaire
        if auth_header.startswith('Basic '):
            if self.verify_basic_auth(auth_header):
                return True, None
            logger.warning(f"Échec authentication Basic depuis {client_ip}")
            return False, "Identifiants incorrects"

        # Support Bearer token (compatibilité API existante)
        if auth_header.startswith('Bearer '):
            token = auth_header[7:]
            if self.token_manager.verify_token(token):
                return True, None
            logger.warning(f"Token Bearer invalide depuis {client_ip}")
            return False, "Token invalide"

        return False, "Authentification requise"

    def check_rate_limit(self, client_ip: str) -> Tuple[bool, Optional[str], Optional[int]]:
        """
        Vérifier le rate limit
        Returns: (allowed, error_message, retry_after)
        """
        allowed, retry_after = self.rate_limiter.is_allowed(client_ip)

        if not allowed:
            return False, f"Rate limit exceeded. Retry after {retry_after}s", retry_after

        return True, None, None


def require_auth(token_manager: TokenManager):
    """Décorateur pour exiger l'authentification"""
    def decorator(handler_method):
        @wraps(handler_method)
        def wrapper(self, *args, **kwargs):
            client_ip = self.client_address[0]

            # Check auth
            if not self.check_auth():
                self.send_json_response({'error': 'Unauthorized'}, 401)
                return

            return handler_method(self, *args, **kwargs)
        return wrapper
    return decorator
