"""Reglages techniques editables au runtime (globaux a l'app).

Contrairement a `config.py` (constantes figees lues a l'import, surchargeables
seulement par variables d'environnement au demarrage), ce module expose un objet
`Settings` *persiste* dans `data/settings.json` et modifiable depuis l'UI.

Les valeurs par defaut reprennent celles de `config.py`. Le code du pipeline
consulte `get_settings()` au moment de l'execution ; une sauvegarde invalide les
caches de modeles (backends TTS, chargement Gemma) pour que les nouveaux
identifiants/parametres prennent effet sans redemarrage.
"""
from __future__ import annotations

import threading
from typing import Optional

from pydantic import BaseModel, Field

from . import config

# --- Prompts systeme par defaut (source canonique) ---------------------------
# Ces chaines pilotent les trois taches Gemma. L'utilisateur peut les editer.
DEFAULT_PROMPT_SPEAKERS = (
    "Tu es un assistant d'analyse litteraire. Tu identifies QUI prononce chaque "
    "replique de dialogue dans un extrait de roman en francais. Une liste des "
    "personnages du chapitre t'est fournie : choisis le locuteur dans cette "
    "liste en recopiant son nom EXACTEMENT. Appuie-toi sur la narration qui "
    "PRECEDE et qui SUIT chaque replique (incise d'attribution type 'dit "
    "Marie'), sur les vocatifs (le personnage a qui l'on s'adresse) et sur "
    "l'alternance des tours de parole. Mets 'inconnu' si tu n'es pas sur. Tu "
    "reponds UNIQUEMENT en JSON valide, sans texte autour."
)
DEFAULT_PROMPT_SPEAKERS_REFINE = (
    "Tu es un assistant d'analyse litteraire. On te donne des repliques dont le "
    "locuteur est reste indetermine, avec le locuteur DEJA identifie des "
    "repliques voisines. Deduis qui parle en exploitant l'alternance des tours "
    "de parole et le contexte narratif autour. Choisis le nom dans la liste des "
    "personnages fournie, en le recopiant exactement, ou 'inconnu' si vraiment "
    "indeterminable. Tu reponds UNIQUEMENT en JSON valide, sans texte autour."
)
DEFAULT_PROMPT_CHARACTERS = (
    "Tu es un assistant d'analyse litteraire. Tu extrais la liste des "
    "personnages d'un extrait de roman et leurs attributs vocaux. Tu reponds "
    "UNIQUEMENT en JSON valide."
)
DEFAULT_PROMPT_PRONUNCIATION = (
    "Tu es un assistant de preparation de livre audio en francais. Tu reperes "
    "les mots dont la prononciation par un synthetiseur vocal francais risque "
    "d'etre incorrecte (noms propres etrangers, termes de science-fiction, "
    "acronymes). Tu reponds UNIQUEMENT en JSON valide."
)
DEFAULT_PROMPT_INCISES = (
    "Tu es un assistant d'analyse litteraire. Tu reperes les INCISES de "
    "narration inserees dans une replique de dialogue (ex: 'dit Mamie', "
    "'repondit le capitaine'). Tu reponds UNIQUEMENT en JSON valide, sans "
    "texte autour."
)
DEFAULT_PROMPT_DEDUP = (
    "Tu es un assistant d'analyse litteraire. Tu rapproches les differentes "
    "facons de nommer un meme personnage (nom complet, prenom, surnom, "
    "diminutif) pour eviter les doublons dans le casting d'un livre audio. Tu "
    "ne fusionnes deux noms que si c'est, avec certitude, la meme personne. Tu "
    "reponds UNIQUEMENT en JSON valide, sans texte autour."
)


class Settings(BaseModel):
    """Reglages techniques globaux, persistes dans data/settings.json."""

    # --- Modeles MLX (identifiants HuggingFace) ---
    gemma_model: str = config.GEMMA_MODEL
    qwen3_model: str = config.QWEN3_TTS_MODEL
    kokoro_model: str = config.KOKORO_MODEL

    # --- Generation Gemma ---
    gemma_temperature: float = Field(0.1, ge=0.0, le=2.0)
    gemma_max_tokens: int = Field(2048, ge=64, le=16384)
    # Modeles a raisonnement (Gemma 4, DeepSeek-R1, Qwen-think...) : ils emettent
    # une chaine de pensee avant la reponse. Active le retrait de cette pensee
    # (canaux <|channel>thought.../<channel|>, balises <think>...</think>) AVANT
    # le parsing JSON, et releve le plafond de tokens (la pensee en consomme).
    gemma_reasoning: bool = False
    # Plafond de tokens en mode raisonnement (la pensee en consomme beaucoup).
    # La generation s'arrete de toute facon des que la reponse JSON post-pensee
    # est complete ; ce plafond est un garde-fou contre les boucles de pensee
    # sans fin (certains modeles tournent en rond a temperature 0).
    gemma_reasoning_max_tokens: int = Field(4096, ge=256, le=16384)
    # Temperature en mode raisonnement. Le decodage GLOUTON (temp 0) fait boucler
    # les modeles a raisonnement (repetitions sans fin) ; Qwen & co recommandent
    # un echantillonnage. Si la temperature effective est 0, on bascule sur
    # celle-ci. Rend le benchmark non deterministe en mode raisonnement (inevitable).
    gemma_reasoning_temperature: float = Field(0.6, ge=0.0, le=2.0)

    # --- Prompts systeme (analyse) ---
    prompt_speakers: str = DEFAULT_PROMPT_SPEAKERS
    prompt_speakers_refine: str = DEFAULT_PROMPT_SPEAKERS_REFINE
    prompt_characters: str = DEFAULT_PROMPT_CHARACTERS
    prompt_pronunciation: str = DEFAULT_PROMPT_PRONUNCIATION
    prompt_incises: str = DEFAULT_PROMPT_INCISES  # DEPRECIE (detection deterministe)
    prompt_dedup: str = DEFAULT_PROMPT_DEDUP

    # --- Incises ---
    # DEPRECIE : la detection d'incises est desormais deterministe et conscience
    # du casting (analysis.segmenter.detect_incises), sans fallback Gemma. Champ
    # conserve pour charger les settings.json existants sans erreur.
    split_incises_use_gemma: bool = True

    # --- Attribution retroactive (2e passe sur les repliques indeterminees) ---
    # Apres la 1re passe, une 2e passe ciblee re-resout les repliques restees
    # 'inconnu' (ou peu sures) en s'appuyant sur les voisins deja identifies.
    # Declenchee seulement s'il reste des doutes -> cout nul sinon.
    retro_pass_use_gemma: bool = True

    # --- Deduplication du casting ---
    # Heuristique (sure, deterministe) par defaut. La passe Gemma rattache en
    # plus les variantes non evidentes (diminutifs, titres) mais, avec un petit
    # modele local, produit des fusions erronees -> opt-in.
    dedup_use_gemma: bool = False

    # --- TTS ---
    default_backend: str = "kokoro"
    language: str = config.DEFAULT_LANGUAGE
    kokoro_lang_code: str = config.KOKORO_LANG_CODE
    kokoro_default_voice: str = config.KOKORO_DEFAULT_VOICE
    qwen3_default_voice: str = config.QWEN3_DEFAULT_VOICE

    # --- Audio (encodage final) ---
    target_sample_rate: int = Field(config.TARGET_SAMPLE_RATE, ge=8000, le=48000)
    mp3_bitrate: str = config.MP3_BITRATE
    target_dbfs: float = Field(config.TARGET_DBFS, ge=-40.0, le=0.0)


_LOCK = threading.Lock()
_cache: Optional[Settings] = None


def settings_path():
    return config.DATA_DIR / "settings.json"


def get_settings() -> Settings:
    """Renvoie les reglages courants (charges depuis le disque une seule fois)."""
    global _cache
    with _LOCK:
        if _cache is None:
            path = settings_path()
            if path.exists():
                try:
                    _cache = Settings.model_validate_json(
                        path.read_text(encoding="utf-8"))
                except Exception:  # noqa: BLE001 — fichier corrompu -> defauts
                    _cache = Settings()
            else:
                _cache = Settings()
        return _cache


def save_settings(settings: Settings) -> Settings:
    """Persiste les reglages et invalide les caches de modeles."""
    global _cache
    with _LOCK:
        _cache = settings
        path = settings_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(settings.model_dump_json(indent=2), encoding="utf-8")
    _invalidate_model_caches()
    return settings


def _invalidate_model_caches() -> None:
    """Force le rechargement des modeles apres un changement d'identifiant/param.

    `get_backend` est cache par *nom* de backend, pas par id de modele ; sans
    purge, un changement d'id serait ignore. Idem pour le chargement Gemma.
    """
    try:
        from .tts.factory import get_backend
        get_backend.cache_clear()
    except Exception:  # noqa: BLE001
        pass
    try:
        from .analysis.gemma import _load
        _load.cache_clear()
    except Exception:  # noqa: BLE001
        pass
