"""Registre des services du homelab — chargement depuis la config + résolution floue.

Chaque service est une entrée `[[services]]` de `stt.toml` :

    [[services]]
    id      = "grafana"
    name    = "Grafana"
    url     = "http://grafana.lab.local"
    icon    = "📊"
    aliases = ["graphana", "metrics", "monitoring", "dashboard"]

`match_service()` résout un texte libre (clic ou voix : « ouvre grafana »,
« montre les métriques »…) vers un service, par id / nom / alias, en tolérant les
accents, la casse et les fautes légères.
"""

from __future__ import annotations

import difflib
import unicodedata
from dataclasses import dataclass, field


@dataclass(frozen=True)
class Component:
    """Brique dont dépend un service (App, Redis, hôte DB…).

    `prom` : requête PromQL renvoyant 1 = en ligne / 0 = en panne (vide = pas de
    sonde, le composant reste informatif). Évaluée par le StatusPoller.
    """
    name: str
    prom: str = ""


@dataclass(frozen=True)
class Metric:
    """Chiffre live affiché sur la page détail (redémarrages, uptime, mémoire…).

    `prom` : requête PromQL qui DOIT s'agréger à une seule valeur (sum/max/…). Le
    StatusPoller prend la première valeur retournée.
    `unit` : formatage côté HUD — "count" | "duration" (secondes) | "bytes" |
    "cpu" (cœurs) | "percent" | "" (nombre brut).
    `hide_if_empty` : carte masquée si la requête ne renvoie aucune série (ex. cAdvisor
    absent) → pas de carte « — » disgracieuse.
    """
    label: str
    prom: str
    unit: str = ""
    hide_if_empty: bool = False


@dataclass(frozen=True)
class Service:
    id: str
    name: str
    url: str
    icon: str = "🔗"
    aliases: tuple[str, ...] = field(default_factory=tuple)
    # Métadonnées de la page détail (toutes optionnelles, config-driven)
    description: str = ""
    health_url: str = ""                               # endpoint sondé (défaut : url)
    components: tuple[Component, ...] = ()
    metrics: tuple[Metric, ...] = ()                   # chiffres live (page détail)
    # Une alerte Alertmanager est « pertinente » si TOUS ces (label, regex) matchent.
    alerts_match: tuple[tuple[str, str], ...] = ()

    @property
    def probe_url(self) -> str:
        return self.health_url or self.url

    @property
    def keywords(self) -> list[str]:
        """Tous les libellés qui peuvent désigner ce service (normalisés)."""
        raw = [self.id, self.name, *self.aliases]
        return [k for k in (_norm(x) for x in raw) if k]


def _norm(s: str) -> str:
    """minuscule + sans accents + espaces compactés (comparaison robuste)."""
    s = unicodedata.normalize("NFKD", str(s))
    s = "".join(c for c in s if not unicodedata.combining(c))
    return " ".join(s.lower().split())


def load_services(cfg: dict) -> list[Service]:
    """Construit la liste de services depuis `cfg['services']` (entrées invalides ignorées)."""
    out: list[Service] = []
    seen: set[str] = set()
    for raw in cfg.get("services") or []:
        if not isinstance(raw, dict):
            continue
        url = str(raw.get("url") or "").strip()
        sid = str(raw.get("id") or "").strip()
        if not url or not sid or sid in seen:
            continue  # url + id uniques obligatoires
        seen.add(sid)
        aliases = tuple(
            str(a).strip() for a in (raw.get("aliases") or []) if str(a).strip()
        )
        components = tuple(
            Component(name=str(c.get("name") or "").strip(),
                      prom=str(c.get("prom") or "").strip())
            for c in (raw.get("components") or [])
            if isinstance(c, dict) and str(c.get("name") or "").strip()
        )
        metrics = tuple(
            Metric(label=str(m.get("label") or "").strip(),
                   prom=str(m.get("prom") or "").strip(),
                   unit=str(m.get("unit") or "").strip(),
                   hide_if_empty=bool(m.get("hide_if_empty")))
            for m in (raw.get("metrics") or [])
            if isinstance(m, dict) and str(m.get("label") or "").strip()
            and str(m.get("prom") or "").strip()
        )
        alerts_match = tuple(
            (str(k), str(v))
            for k, v in (raw.get("alerts_match") or {}).items()
        ) if isinstance(raw.get("alerts_match"), dict) else ()
        out.append(
            Service(
                id=sid,
                name=str(raw.get("name") or sid).strip(),
                url=url,
                icon=str(raw.get("icon") or "🔗").strip() or "🔗",
                aliases=aliases,
                description=str(raw.get("description") or "").strip(),
                health_url=str(raw.get("health_url") or "").strip(),
                components=components,
                metrics=metrics,
                alerts_match=alerts_match,
            )
        )
    return out


def match_service(services: list[Service], query: str) -> Service | None:
    """Résout `query` vers un service ; None si rien d'assez proche.

    Ordre : correspondance exacte d'un mot-clé > inclusion (la requête contient un
    mot-clé, ou inversement) > rapprochement flou (difflib, fautes légères).
    """
    q = _norm(query)
    if not q or not services:
        return None

    # 1) exact : un des mots-clés == la requête entière
    for svc in services:
        if q in svc.keywords:
            return svc

    # 2) inclusion : « ouvre le grafana » contient « grafana » ; ou requête ⊂ mot-clé
    best_incl: tuple[int, Service] | None = None
    for svc in services:
        for kw in svc.keywords:
            if len(kw) >= 3 and (kw in q or q in kw):
                # on préfère le mot-clé le plus long (le plus spécifique)
                if best_incl is None or len(kw) > best_incl[0]:
                    best_incl = (len(kw), svc)
    if best_incl:
        return best_incl[1]

    # 3) flou : tolère « graphana », « argo cd » → difflib sur chaque mot-clé
    best_ratio = 0.0
    best_svc: Service | None = None
    for svc in services:
        for kw in svc.keywords:
            r = difflib.SequenceMatcher(None, q, kw).ratio()
            if r > best_ratio:
                best_ratio, best_svc = r, svc
    return best_svc if best_ratio >= 0.72 else None
