"""
core/security.py — Validation des entrées et détection d'injections.

Deux points d'intégration :
  - valider_input(texte)  → appelé sur chaque question utilisateur
  - valider_sql(sql)      → appelé avant chaque exécution SQL (query_db)

Protections couvertes :
  1. Validation basique          — vide, trop long, caractères de contrôle
  2. Normalisation Unicode       — \xa0, espaces insécables, zero-width chars
  3. Injection de prompt         — tentative de réécrire les instructions de l'agent
  4. Exfiltration de prompt      — tentative de lire le system prompt ou les règles
  5. Injection de contexte       — faux marqueurs de rôle, fausse mémoire injectée
  6. Injection indirecte         — instructions cachées dans du "contenu" (document,
                                   note, résumé…) avec marqueurs de priorité ou
                                   overrides comportementaux
  7. Injection SQL directe       — patterns DDL/DML dangereux dans l'entrée libre
  8. Actions non autorisées      — accès fichiers, variables d'env, exécution de code
"""

from __future__ import annotations

import logging
import re

logger = logging.getLogger("security")

# ── Constantes ────────────────────────────────────────────────────────────────

MAX_INPUT_LENGTH = 2_000   # caractères

MSG_GENERIQUE = (
    "Entrée non autorisée. "
    "Posez une question sur un bien immobilier, un marché ou une rentabilité locative."
)


# ── Exception ─────────────────────────────────────────────────────────────────

class InputSecurityError(Exception):
    """Levée quand une entrée est considérée comme malveillante ou invalide."""


# ── Normalisation Unicode ─────────────────────────────────────────────────────

_UNICODE_SPACES = re.compile(
    r"[\xa0\u00a0\u1680\u2000-\u200b\u200c\u200d\u202f\u205f\u2060\u3000\ufeff]"
)

def _normaliser(texte: str) -> str:
    """
    Normalise les espaces et caractères invisibles Unicode avant détection.
    Retourne une version nettoyée utilisée UNIQUEMENT pour les contrôles de sécurité
    (l'original est transmis à l'agent si la validation passe).
    """
    normalise = _UNICODE_SPACES.sub(" ", texte)
    normalise = re.sub(r" {2,}", " ", normalise)
    return normalise


# ── Catalogue de patterns ─────────────────────────────────────────────────────

_PATTERNS_INPUT: list[tuple[str, re.Pattern, str]] = [

    # ── 1. Injection de prompt ─────────────────────────────────────────────────
    (
        "injection_prompt",
        re.compile(
            r"(?:"
            r"ignore\s+(?:tes?|vos?|les?|all|previous|your)\s+(?:instructions?|consignes?|r[eè]gles?|rules?|prompt)"
            r"|oublie\s+(?:tes?|vos?|les?|tout|everything)"
            r"|forget\s+(?:your|all|previous|everything)"
            r"|disregard\s+(?:your|all|previous)"
            r"|new\s+(?:instructions?|consignes?|rules?|directives?)"
            r"|nouvelles?\s+(?:instructions?|consignes?|r[eè]gles?)"
            r"|tu\s+(?:es|n[''']es)\s+(?:plus|maintenant|d[ée]sormais)"
            r"|you\s+are\s+now\s+(?:a|an)"
            r"|act\s+as\s+(?:a|an|if)"
            r"|pretend\s+(?:you\s+are|to\s+be)"
            r"|fais\s+semblant\s+d[''']être"
            r"|jailbreak"
            r"|(?:dan|dude|based\s*ai|evil\s*ai)\b"
            r")",
            re.IGNORECASE,
        ),
        "Tentative d'injection de prompt détectée",
    ),

    # ── 2. Exfiltration de prompt ──────────────────────────────────────────────
    (
        "exfiltration_prompt",
        re.compile(
            r"(?:"
            r"(?:r[eé]p[eè]te|affiche|montre|imprime|print|show|reveal|display|output|dump)"
            r"\s+.{0,40}(?:(?:system\s*)?prompt|instructions?|consignes?|r[eè]gles?|contexte\s+initial)"
            r"|what\s+(?:are|were)\s+your\s+(?:instructions?|rules?|prompt|directives?)"
            r"|(?:quel(?:les?)?\s+(?:sont|[ée]taient)\s+(?:tes?|vos?)\s+(?:instructions?|consignes?|r[eè]gles?))"
            r"|(?:ton|votre)\s+(?:system\s*)?prompt"
            r"|quelles?\s+(?:sont|[ée]taient)\s+tes?\s+consignes?"
            r")",
            re.IGNORECASE,
        ),
        "Tentative d'exfiltration de prompt détectée",
    ),

    # ── 3. Injection de contexte ───────────────────────────────────────────────
    (
        "injection_contexte",
        re.compile(
            r"(?:"
            r"\[\s*(?:system|user|assistant|human|ai|inst)\s*\]"
            r"|<\s*(?:system|user|assistant|human|s)\s*>"
            r"|#{1,4}\s*(?:system|instruction|context|prompt)"
            r"|role\s*:\s*(?:system|assistant|user)"
            r"|(?:<<|>>)\s*(?:SYS|INST|system)"
            r"|\|\s*(?:im_start|im_end)\s*\|"
            r")",
            re.IGNORECASE,
        ),
        "Injection de marqueur de contexte détectée",
    ),

    # ── 4. Injection indirecte ─────────────────────────────────────────────────
    (
        "injection_indirecte",
        re.compile(
            r"(?:"
            r"INSTRUCTION\s+(?:SYST[EÈ]ME|SYSTEM|PRIORITAIRE|IMPORTANTE?|URGENTE?|OVERRIDE)"
            r"|(?:a\s+partir\s+de\s+maintenant|from\s+now\s+on|d[eé]sormais|henceforth)"
              r".{0,80}(?:r[eé]ponds?\s+(?:toujours|syst[eé]matiquement)|always\s+(?:respond|say|reply|answer)|dis\s+(?:toujours|syst[eé]matiquement)|dois\s+(?:toujours|syst[eé]matiquement))"
            r"|r[eé]ponds?\s+(?:toujours|syst[eé]matiquement)\s+['\"\u00ab\u2018\u2019\u201c\u201d]"
            r"|always\s+(?:respond|say|reply|answer)\s+['\"\u00ab\u2018\u2019\u201c\u201d]"
            r"|(?:tu\s+dois|vous\s+devez)\s+(?:maintenant|d[eé]sormais|toujours|syst[eé]matiquement)"
            r"|(?:maintenant|d[eé]sormais)\s+(?:tu\s+dois|vous\s+devez)\s+(?:toujours|syst[eé]matiquement)"
            r"|(?:note|remarque|attention|important|avertissement)\s*:"
              r".{0,60}(?:d[eé]sormais|maintenant|tu\s+dois|r[eé]ponds?|always|from\s+now)"
            r"|(?:fin\s+du\s+document|end\s+of\s+(?:document|context|text))"
            r")",
            re.IGNORECASE | re.DOTALL,
        ),
        "Injection indirecte (instruction cachée dans du contenu) détectée",
    ),

    # ── 5. Injection SQL directe ───────────────────────────────────────────────
    (
        "injection_sql",
        re.compile(
            r"(?:"
            r"\b(?:drop|delete|truncate|alter|create|replace)\s+(?:table|database|schema|index|view)\b"
            r"|\binsert\s+into\b"
            r"|\bupdate\s+\w+\s+set\b"
            r"|\bunion\s+(?:all\s+)?select\b"
            r"|--\s*(?:--|$)"
            r"|;\s*(?:drop|delete|insert|update|exec|execute)\b"
            r"|'\s*or\s*'\w*'\s*=\s*'\w*'"
            r"|\bor\b\s+\d+\s*=\s*\d+"
            r"|\bxp_cmdshell\b"
            r"|\bexec\s*\("
            r"|/\*.*\*/"
            r")",
            re.IGNORECASE,
        ),
        "Pattern d'injection SQL détecté dans l'entrée",
    ),

    # ── 6. Actions non autorisées ──────────────────────────────────────────────
    (
        "action_non_autorisee",
        re.compile(
            r"(?:"
            r"\bos\.(?:system|popen|exec|listdir|remove|rename)\b"
            r"|\bsubprocess\b"
            r"|\beval\s*\("
            r"|\bexec\s*\("
            r"|\b__import__\s*\("
            r"|open\s*\(['\"](?:\.env|config\.py|.*secret)"
            r"|(?:api[_\-]?key|openai[_\-]?key|secret[_\-]?key|access[_\-]?token)"
            r"\s*(?:=|:)"
            r"|(?:mot\s*de\s*passe|password)\s*(?:=|:)\s*['\"]"
            r")",
            re.IGNORECASE,
        ),
        "Tentative d'action non autorisée détectée",
    ),
]

_PATTERNS_SQL: list[tuple[str, re.Pattern, str]] = [
    (
        "sql_ecriture",
        re.compile(
            r"^\s*(?:insert|update|delete|drop|alter|create|truncate|replace|attach|detach)",
            re.IGNORECASE,
        ),
        "Requête SQL d'écriture/modification bloquée",
    ),
    (
        "sql_commentaire_dangereux",
        re.compile(r"(?:--\s*$|/\*|\*/|;\s*(?:drop|delete|insert|update)\b)", re.IGNORECASE),
        "Commentaire ou chaîne dangereuse dans la requête SQL",
    ),
    (
        "sql_stacked_query",
        re.compile(r";\s*\w", re.IGNORECASE),
        "Requête SQL empilée détectée (stacked query)",
    ),
]


# ── Fonctions publiques ────────────────────────────────────────────────────────

def valider_input(texte: str) -> str:
    """
    Valide l'entrée utilisateur avant de la transmettre à l'agent.

    Raises:
        InputSecurityError: Si l'entrée est vide, trop longue ou contient
                            un pattern malveillant.
    """
    if not texte or not texte.strip():
        raise InputSecurityError("L'entrée ne peut pas être vide.")

    if len(texte) > MAX_INPUT_LENGTH:
        raise InputSecurityError(
            f"Entrée trop longue ({len(texte)} caractères). "
            f"Maximum autorisé : {MAX_INPUT_LENGTH} caractères."
        )

    if re.search(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", texte):
        logger.warning("Caractère de contrôle détecté dans l'entrée.")
        raise InputSecurityError(MSG_GENERIQUE)

    texte_normalise = _normaliser(texte)

    for categorie, pattern, msg_log in _PATTERNS_INPUT:
        match = pattern.search(texte_normalise)
        if match:
            logger.warning(
                f"[SECURITY] {msg_log} | catégorie={categorie} "
                f"| match={match.group()!r} | input={texte[:120]!r}"
            )
            raise InputSecurityError(MSG_GENERIQUE)

    return texte


def valider_sql(sql: str) -> str:
    """
    Valide une requête SQL générée par le LLM avant exécution.

    Raises:
        InputSecurityError: Si la requête contient des patterns dangereux.
    """
    for categorie, pattern, msg_log in _PATTERNS_SQL:
        match = pattern.search(sql)
        if match:
            logger.warning(
                f"[SECURITY] {msg_log} | catégorie={categorie} "
                f"| match={match.group()!r} | sql={sql[:200]!r}"
            )
            raise InputSecurityError(f"Requête SQL non autorisée ({categorie}).")

    return sql
