"""
DOOBAT - Service de sécurité
Validation webhook Telegram, protection injections, rate limiting.
Copyright 2026 Vincent Gallet - Vincesoft
"""

import hashlib
import hmac
import logging
import re
import time
from collections import defaultdict
from configparser import ConfigParser
from typing import Optional

logger = logging.getLogger("doobat.security")


class SecurityService:
    """
    Centralise toutes les vérifications de sécurité :
    - Validation du secret Telegram webhook
    - Protection anti-injection
    - Rate limiting par chat_id
    - Sanitisation des entrées
    """

    def __init__(self, config: ConfigParser):
        self._webhook_secret = config.get("TELEGRAM", "webhook_secret", fallback="")
        self._admin_token = config.get("SECURITY", "admin_token", fallback="")
        rate_limit = config.getint("SECURITY", "rate_limit_per_minute", fallback=20)

        # Rate limiting : {chat_id: [(timestamp, ...), ...]}
        self._rate_store: dict[int, list[float]] = defaultdict(list)
        self._rate_limit = rate_limit
        self._rate_window = 60  # secondes

        logger.info(
            f"SecurityService initialisé, rate_limit={rate_limit}/min, "
            f"webhook_secret={'configuré' if self._webhook_secret else 'ABSENT'}"
        )

    # ── Validation Webhook Telegram ────────────────────────────────────────────

    def validate_telegram_secret(self, provided_secret: Optional[str]) -> bool:
        """
        Vérifie le secret token Telegram envoyé dans l'en-tête
        X-Telegram-Bot-Api-Secret-Token.
        Utilise comparaison hmac.compare_digest pour éviter timing attacks.
        """
        if not self._webhook_secret:
            logger.warning("Webhook secret non configuré — validation ignorée.")
            return True  # Permissif si non configuré

        if not provided_secret:
            logger.warning("En-tête webhook secret absent.")
            return False

        return hmac.compare_digest(
            provided_secret.encode("utf-8"),
            self._webhook_secret.encode("utf-8"),
        )

    def validate_admin_token(self, token: Optional[str]) -> bool:
        """Vérifie le token d'administration."""
        if not self._admin_token or not token:
            return False
        return hmac.compare_digest(
            token.encode("utf-8"),
            self._admin_token.encode("utf-8"),
        )

    # ── Rate Limiting ──────────────────────────────────────────────────────────

    def check_rate_limit(self, chat_id: int) -> bool:
        """
        Vérifie si un chat_id a dépassé le rate limit.
        Retourne True si la requête est autorisée, False si bloquée.
        """
        now = time.time()
        window_start = now - self._rate_window

        # Nettoyer les anciennes entrées
        self._rate_store[chat_id] = [
            ts for ts in self._rate_store[chat_id] if ts > window_start
        ]

        count = len(self._rate_store[chat_id])
        if count >= self._rate_limit:
            logger.warning(
                f"Rate limit dépassé pour chat_id={chat_id} : "
                f"{count}/{self._rate_limit} requêtes/minute"
            )
            return False

        self._rate_store[chat_id].append(now)
        return True

    # ── Sanitisation des entrées ───────────────────────────────────────────────

    @staticmethod
    def sanitize_text_input(text: str, max_length: int = 500) -> str:
        """
        Nettoie un texte utilisateur :
        - Supprime les caractères de contrôle
        - Limite la longueur
        - Préserve les caractères unicode courants
        """
        if not text:
            return ""

        # Supprimer les caractères de contrôle (sauf newline et tab)
        clean = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", text)

        # Limiter la longueur
        clean = clean[:max_length]

        return clean.strip()

    @staticmethod
    def sanitize_partner_name(name: str) -> str:
        """
        Nettoie un nom de partenaire pour éviter les injections XML-RPC.
        Autorise : lettres, chiffres, espaces, tirets, points, &.
        """
        if not name:
            return ""
        clean = re.sub(r"[^\w\s\-\.&àâäéèêëîïôùûüÿæœÀÂÄÉÈÊËÎÏÔÙÛÜŸÆŒ]", "", name)
        return clean[:100].strip()

    @staticmethod
    def sanitize_article_name(name: str) -> str:
        """Nettoie un nom d'article pour la recherche Odoo."""
        if not name:
            return ""
        clean = re.sub(r"[^\w\s\-_\.\/àâäéèêëîïôùûüÿæœÀÂÄÉÈÊËÎÏÔÙÛÜŸÆŒ]", "", name)
        return clean[:100].strip()

    @staticmethod
    def sanitize_phone(phone: str) -> str:
        """Nettoie un numéro de téléphone."""
        if not phone:
            return ""
        return re.sub(r"[^\d\+\-\s\(\)]", "", phone)[:20]

    # ── Masquage des données sensibles ─────────────────────────────────────────

    @staticmethod
    def mask_token(token: str) -> str:
        """Masque un token pour les logs : affiche seulement les 6 premiers et 4 derniers caractères."""
        if not token or len(token) < 12:
            return "***"
        return f"{token[:6]}...{token[-4:]}"

    @staticmethod
    def mask_phone(phone: str) -> str:
        """Masque un numéro de téléphone pour les logs."""
        if not phone or len(phone) < 4:
            return "***"
        return f"{'*' * (len(phone) - 4)}{phone[-4:]}"

    # ── Validation d'entrées structurelles ────────────────────────────────────

    @staticmethod
    def is_valid_chat_id(chat_id) -> bool:
        """Valide qu'un chat_id Telegram est un entier non nul."""
        try:
            return isinstance(int(chat_id), int) and chat_id != 0
        except (TypeError, ValueError):
            return False

    @staticmethod
    def is_valid_partner_id(partner_id) -> bool:
        """Valide un partner_id Odoo."""
        try:
            return isinstance(int(partner_id), int) and int(partner_id) > 0
        except (TypeError, ValueError):
            return False
