"""Couche « capabilities moteurs » — vérité testable sur les moteurs réellement
disponibles dans l'environnement courant (dev ou build frozen).

Un moteur n'est *disponible* que si ses dépendances chargent réellement (et, pour
CamemBERT-bio, si son modèle ONNX embarqué est présent). Cette couche est
consommée par :
  - la GUI V6 (afficher / désactiver les moteurs optionnels honnêtement) ;
  - le CLI (`--engines`, diagnostic des moteurs réellement embarqués).

Objectif produit : ne jamais afficher / promettre un moteur que le build
n'embarque pas. Dans les builds frozen Windows, `edsnlp`/`spacy`/`gliner` ne sont
pas embarqués → leur spec d'import est introuvable → moteur marqué indisponible.

Les sondes (probes) sont **légères** (`importlib.util.find_spec`, pas d'import
lourd d'edsnlp/spacy au démarrage de la GUI) et **injectables** pour les tests
(aucun modèle, aucun réseau).
"""

from __future__ import annotations

import importlib.util
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Dict, List, Optional

# Probe = () -> (available: bool, reason: str)
Probe = Callable[[], "tuple[bool, str]"]


@dataclass(frozen=True)
class EngineCapability:
    """État d'un moteur : disponible ou non, requis ou optionnel, raison courte."""

    key: str          # "camembert" | "eds" | "gliner"
    label: str        # libellé présentable (GUI / CLI)
    available: bool
    required: bool    # CamemBERT-bio = moteur standard requis
    reason: str       # explication courte (FR), surtout si indisponible


# -- Métadonnées des moteurs (libellé + caractère requis) --------------------

_ENGINES = [
    ("camembert", "CamemBERT-bio (standard)", True),
    ("eds", "EDS-Pseudo (optionnel)", False),
    ("gliner", "GLiNER (optionnel)", False),
]


# -- Sondes par défaut (environnement réel) ----------------------------------

def _has_module(name: str) -> bool:
    """Vrai si le module est importable sans l'importer réellement.

    `find_spec` interroge les importateurs (y compris ceux de PyInstaller en
    frozen) sans exécuter le module → léger et sûr au démarrage.
    """
    try:
        return importlib.util.find_spec(name) is not None
    except Exception:
        return False


def _app_dir() -> Path:
    """Racine des ressources (modèles) : `_MEIPASS` en frozen, sinon ce dossier."""
    if getattr(sys, "frozen", False):
        return Path(getattr(sys, "_MEIPASS", Path(sys.executable).parent))
    return Path(__file__).resolve().parent


def _camembert_model_path() -> Path:
    return _app_dir() / "models" / "camembert-bio-deid" / "onnx" / "model.onnx"


def _probe_camembert() -> "tuple[bool, str]":
    if not _has_module("onnxruntime"):
        return False, "onnxruntime non embarqué dans cette version"
    if not _camembert_model_path().exists():
        return False, "modèle CamemBERT-bio ONNX absent du build"
    return True, "modèle ONNX embarqué (moteur standard)"


def _probe_eds() -> "tuple[bool, str]":
    missing = [m for m in ("edsnlp", "spacy") if not _has_module(m)]
    if missing:
        return False, "non embarqué dans cette version (manque : " + ", ".join(missing) + ")"
    return True, "edsnlp + spacy disponibles"


def _probe_gliner() -> "tuple[bool, str]":
    if not _has_module("gliner"):
        return False, "non embarqué dans cette version (manque : gliner)"
    return True, "gliner disponible"


def _default_probes() -> Dict[str, Probe]:
    return {"camembert": _probe_camembert, "eds": _probe_eds, "gliner": _probe_gliner}


# -- API publique ------------------------------------------------------------

def capabilities_map(probes: Optional[Dict[str, Probe]] = None) -> Dict[str, EngineCapability]:
    """Retourne {key: EngineCapability} pour chaque moteur connu.

    `probes` (injectable) mappe chaque clé moteur vers une sonde
    `() -> (available, reason)`. Par défaut : sondes réelles de l'environnement.
    Une sonde qui lève est traitée comme « indisponible » (fail-closed).
    """
    probes = probes if probes is not None else _default_probes()
    caps: Dict[str, EngineCapability] = {}
    for key, label, required in _ENGINES:
        probe = probes.get(key)
        if probe is None:
            available, reason = False, "aucune sonde fournie pour ce moteur"
        else:
            try:
                available, reason = probe()
            except Exception as exc:  # noqa: BLE001 — fail-closed
                available, reason = False, f"sonde en échec : {exc}"
        caps[key] = EngineCapability(
            key=key, label=label, available=bool(available), required=required, reason=str(reason)
        )
    return caps


def available_engines(probes: Optional[Dict[str, Probe]] = None) -> List[EngineCapability]:
    """Liste des moteurs réellement disponibles (ordre stable des moteurs connus)."""
    caps = capabilities_map(probes)
    return [caps[key] for key, _, _ in _ENGINES if caps[key].available]
