"""Outils appelables par Asa (function calling) — Phase 1 : LECTURE SEULE.

Chaque outil = un schéma OpenAI (exposé au LLM) + un exécuteur async qui renvoie un
**texte court** réinjecté dans la boucle (`brain.ask_with_tools`). Tout est best-effort :
un exécuteur n'élève jamais d'exception fatale — il renvoie une note d'erreur lisible que
le modèle peut relayer à l'utilisateur.

Les outils réutilisent l'infra existante (Prometheus via `sources`, RAG via `knowledge`).
Aucune action d'écriture ici : le volet « agir sur le homelab » passe par `agent.py`
(hermes-exec, avec confirmation) et sera branché comme outil en Phase 3.
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass

import httpx

from stt_server import sources
from stt_server.config import settings

# job Prometheus du node_exporter par hôte hors-cluster (cf. sources.cluster_block).
_HOST_JOBS = {"gpu-01": "gpu-01-node", "storage-01": "storage-01"}


async def _q1(client: httpx.AsyncClient, expr: str) -> float | None:
    """Valeur scalaire d'une requête PromQL (1re série) ou None. Ne lève jamais."""
    try:
        rows = await sources._prom_query(client, expr)
    except httpx.HTTPError:
        return None
    return rows[0][1] if rows else None


@dataclass
class ToolDeps:
    """Dépendances injectées aux exécuteurs (client HTTP partagé, RAG, vecteur de requête)."""

    client: httpx.AsyncClient
    knowledge: object | None = None        # stt_server.knowledge.Knowledge | None
    qvec: list[float] | None = None        # embedding de la requête (réutilisé par search_docs)


# --- Exécuteurs -----------------------------------------------------------------

async def _search_docs(args: dict, deps: ToolDeps) -> str:
    query = (args.get("query") or "").strip()
    if not query:
        return "Aucune requête fournie."
    if deps.knowledge is None:
        return "La recherche documentaire est désactivée."
    # Réutilise le vecteur déjà calculé si la requête de l'outil == requête utilisateur.
    docs = await deps.knowledge.search(query, deps.qvec)
    if not docs:
        return "Aucun passage pertinent dans la documentation Funk."
    return "\n\n".join(docs[:4])


def _fmt_series(rows: list[tuple[dict, float]], label_key: str | None = None, limit: int = 10) -> str:
    out = []
    for metric, val in rows[:limit]:
        v = int(val) if float(val).is_integer() else round(val, 3)
        if label_key and metric.get(label_key):
            out.append(f"{metric[label_key]}={v}")
        elif metric:
            ident = metric.get("__name__") or next(iter(metric.values()), "")
            out.append(f"{ident}={v}")
        else:
            out.append(str(v))
    return ", ".join(out)


async def _host_health(args: dict, deps: ToolDeps) -> str:
    host = (args.get("host") or "").strip()
    job = _HOST_JOBS.get(host)
    if not job:
        return f"Hôte inconnu : {host!r} (attendu : {', '.join(_HOST_JOBS)})."
    c = deps.client
    # Sondes hôte (concurrentes) : up, CPU%, RAM libre%, disque /%, temp CPU max, uptime h.
    up, cpu, mem, disk, temp, uptime = await asyncio.gather(
        _q1(c, f'up{{job="{job}"}}'),
        _q1(c, f'100-100*avg(rate(node_cpu_seconds_total{{job="{job}",mode="idle"}}[5m]))'),
        _q1(c, f'100*node_memory_MemAvailable_bytes{{job="{job}"}}'
               f'/node_memory_MemTotal_bytes{{job="{job}"}}'),
        _q1(c, f'100-100*node_filesystem_avail_bytes{{job="{job}",mountpoint="/"}}'
               f'/node_filesystem_size_bytes{{job="{job}",mountpoint="/"}}'),
        _q1(c, f'max(node_hwmon_temp_celsius{{job="{job}"}})'),
        _q1(c, f'(node_time_seconds{{job="{job}"}}-node_boot_time_seconds{{job="{job}"}})/3600'),
    )
    if up is None:
        return f"{host} : aucune métrique (cible Prometheus absente ou hôte éteint)."
    parts = [f"{host} : {'en ligne' if up == 1 else 'HORS LIGNE'}"]
    if cpu is not None:
        parts.append(f"CPU {round(cpu)}%")
    if mem is not None:
        parts.append(f"RAM libre {round(mem)}%")
    if disk is not None:
        parts.append(f"disque / {round(disk)}% utilisé")
    if temp is not None:
        parts.append(f"temp CPU {round(temp)}°C")
    if uptime is not None:
        parts.append(f"uptime {round(uptime)} h")
    if host == "gpu-01":
        # Métriques GPU (rocm_scraper → textfile collector node_exporter).
        gtemp, gutil, gvram, gpow, llama = await asyncio.gather(
            _q1(c, "rocm_gpu_temperature_celsius"),
            _q1(c, "rocm_gpu_utilization_percent"),
            _q1(c, "100*rocm_vram_used_bytes/rocm_vram_total_bytes"),
            _q1(c, "rocm_gpu_power_watts"),
            _q1(c, 'up{job="llama-server-gpu"}'),
        )
        gpu = []
        if gutil is not None:
            gpu.append(f"util {round(gutil)}%")
        if gtemp is not None:
            gpu.append(f"{round(gtemp)}°C")
        if gvram is not None:
            gpu.append(f"VRAM {round(gvram)}%")
        if gpow is not None:
            gpu.append(f"{round(gpow)} W")
        if gpu:
            parts.append("GPU (" + ", ".join(gpu) + ")")
        if llama is not None:
            parts.append("llama-server " + ("up" if llama == 1 else "DOWN"))
    return ", ".join(parts) + "."


async def _cluster_status(args: dict, deps: ToolDeps) -> str:
    # Réutilise le résumé déjà éprouvé (nœuds joignables, pods prêts / non prêts).
    return await sources.cluster_block(deps.client)


async def _prometheus_query(args: dict, deps: ToolDeps) -> str:
    expr = (args.get("expr") or "").strip()
    if not expr:
        return "Aucune expression PromQL fournie."
    try:
        rows = await sources._prom_query(deps.client, expr)
    except httpx.HTTPError:
        return "Données indisponibles : Prometheus injoignable."
    except Exception as e:  # noqa: BLE001 — PromQL invalide, etc.
        return f"Requête refusée par Prometheus : {e}"
    if not rows:
        return "La requête ne renvoie aucune série (résultat vide)."
    return _fmt_series(rows)


async def _web_search(args: dict, deps: ToolDeps) -> str:
    query = (args.get("query") or "").strip()
    if not query:
        return "Aucune requête de recherche fournie."
    url = settings.searxng_url.rstrip("/") + "/search"
    try:
        r = await deps.client.get(
            url,
            params={"q": query, "format": "json", "safesearch": 0, "language": "fr"},
            timeout=settings.web_search_timeout,
        )
        r.raise_for_status()
        data = r.json()
    except httpx.HTTPError:
        return "Recherche web indisponible : SearXNG injoignable."
    except ValueError:
        return "Recherche web indisponible : réponse SearXNG inattendue (API JSON activée ?)."
    results = data.get("results") or []
    if not results:
        answers = data.get("answers") or []
        if answers:
            return " ".join(str(a) for a in answers[:2])
        return "Aucun résultat web pour cette recherche."
    lines = []
    for res in results[: settings.web_search_results]:
        title = (res.get("title") or "").strip()
        content = (res.get("content") or "").strip()
        src = res.get("url") or ""
        lines.append(f"- {title} : {content} ({src})" if content else f"- {title} ({src})")
    return "\n".join(lines)


async def _admin_action_placeholder(args: dict, deps: ToolDeps) -> str:
    # Jamais atteint : app.py intercepte admin_action dans son dispatch (handshake de
    # confirmation). Présent uniquement pour que le schéma soit exposé au LLM via le registre.
    return "Action en attente de confirmation."


# --- Registre : nom → (schéma OpenAI, exécuteur) --------------------------------

_TOOLS: dict[str, tuple[dict, object]] = {
    "search_docs": (
        {
            "type": "function",
            "function": {
                "name": "search_docs",
                "description": "Recherche dans la documentation du homelab Funk "
                "(machines, services, configuration, procédures). À utiliser pour toute "
                "question sur la conception ou la config du homelab.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Question ou mots-clés"}
                    },
                    "required": ["query"],
                },
            },
        },
        _search_docs,
    ),
    "host_health": (
        {
            "type": "function",
            "function": {
                "name": "host_health",
                "description": "État de santé LIVE d'une machine hors-cluster (en ligne ?, CPU, "
                "RAM libre, disque, température, uptime ; pour gpu-01 aussi GPU température/"
                "utilisation/VRAM/puissance et l'état de llama-server). À utiliser pour "
                "« est-ce que gpu-01 / storage-01 tourne bien ? » ou une métrique machine précise.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "host": {
                            "type": "string",
                            "enum": list(_HOST_JOBS),
                            "description": "Nom de la machine",
                        }
                    },
                    "required": ["host"],
                },
            },
        },
        _host_health,
    ),
    "cluster_status": (
        {
            "type": "function",
            "function": {
                "name": "cluster_status",
                "description": "État LIVE du cluster Kubernetes Funk : hôtes joignables, "
                "nombre de pods prêts et liste des pods non prêts. À utiliser pour "
                "« est-ce que le cluster va bien ? ».",
                "parameters": {"type": "object", "properties": {}},
            },
        },
        _cluster_status,
    ),
    "prometheus_query": (
        {
            "type": "function",
            "function": {
                "name": "prometheus_query",
                "description": "Exécute une requête PromQL arbitraire (lecture seule) contre "
                "Prometheus et renvoie les séries. À utiliser pour une métrique précise non "
                "couverte par les autres outils.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expr": {"type": "string", "description": "Expression PromQL"}
                    },
                    "required": ["expr"],
                },
            },
        },
        _prometheus_query,
    ),
    "web_search": (
        {
            "type": "function",
            "function": {
                "name": "web_search",
                "description": "Recherche sur INTERNET (méta-moteur SearXNG) : actualité, faits "
                "généraux, documentation externe, sujets hors du homelab Funk. À utiliser quand "
                "l'information n'est ni dans le homelab ni dans ta connaissance, ou pour vérifier "
                "un fait récent.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Termes de recherche"}
                    },
                    "required": ["query"],
                },
            },
        },
        _web_search,
    ),
    "admin_action": (
        {
            "type": "function",
            "function": {
                "name": "admin_action",
                "description": "PROPOSE une action d'administration qui MODIFIE le homelab "
                "(redémarrer un service/déploiement, relancer un pod, appliquer un changement…). "
                "N'exécute RIEN directement : l'utilisateur devra confirmer vocalement. À utiliser "
                "uniquement pour une demande explicite d'AGIR, jamais pour une simple lecture.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "description": {
                            "type": "string",
                            "description": "Action à effectuer, en une phrase claire et impérative "
                            "(ex. « redémarrer le déploiement open-webui »).",
                        }
                    },
                    "required": ["description"],
                },
            },
        },
        _admin_action_placeholder,
    ),
}


def schemas_for(names: tuple[str, ...]) -> list[dict]:
    """Schémas OpenAI des outils demandés (ignore les inconnus)."""
    return [_TOOLS[n][0] for n in names if n in _TOOLS]


async def run(name: str, args: dict, deps: ToolDeps) -> str:
    """Exécute un outil ; ne lève jamais — renvoie une note d'erreur lisible."""
    entry = _TOOLS.get(name)
    if entry is None:
        return f"Outil inconnu : {name}."
    try:
        return await entry[1](args or {}, deps)
    except Exception as e:  # noqa: BLE001 — robustesse : la boucle ne doit jamais casser
        return f"Erreur de l'outil {name} : {e}"
