"""
Rollen-Empfehlung: welches INSTALLIERTE Modell passt am besten auf eine Serving-Rolle?
Capability-getrieben (Vision/Coder/Tools/MoE aus services.caps) + setup-bewusster Fit
(services.budget). Speist den 'Empfohlen'-Hinweis + Auto-Pick im Rollen-Zuweisungs-Modal.

EINE Quelle der Wahrheit mit der ctx-/Fit-Logik: nutzt budget.setup_aware_ctx_for_model
und fit.evaluate_fit — dieselbe Mathematik wie Install-Automatik und Auto-ctx-Button.
"""

import psutil

from services import budget, catalog, llamaswap
from services.fit import evaluate_fit


def _ram_gb() -> float:
    return psutil.virtual_memory().total / (1024 ** 3)


def _capability_suit(role: str, caps: dict, name: str) -> float:
    """0..1 — Capability-Eignung (HARTE Gates). 0 = grundsätzlich falsch für die Rolle.
    Größe/Tempo bewertet getrennt _pref(), damit z.B. 'fast' nicht das größte Modell zieht."""
    role = (role or "").lower()
    low = (name or "").lower()
    vision = bool(caps.get("vision"))
    coder = bool(caps.get("coder"))
    tools = caps.get("tools") != "no"

    if role == "vision":
        if not vision:
            return 0.0                                 # harte Anforderung
        return 1.0 if ("vl" in low or "llava" in low or "pixtral" in low) else 0.7  # dediziert > omni
    if role == "coder":
        return 1.0 if coder else 0.4                   # Coder-Modell Pflicht für Empfehlung
    if role == "hermes":
        # Agent-Hirn: Hermes-Familie am robustesten; sonst natives Tool-Calling Pflicht.
        if "hermes" in low:
            return 1.0
        return 0.6 if tools else 0.1
    if role == "fast":
        # Alltags-Hirn braucht zuverlässige Tools; Größe/Tempo macht _pref.
        return 1.0 if tools else 0.6
    if role == "heavy":
        return 1.0
    if role == "scout":
        return 0.9 if vision else 0.7
    return 0.5


def _pref(role: str, params: float, tps: float) -> float:
    """0..1 — rollengerechte GRÖSSEN-/TEMPO-Präferenz. 'fast' belohnt Tempo & Kleinheit,
    'heavy' Größe (Wissen), 'hermes' moderate Größe (muss warm + ko-resident bleiben)."""
    role = (role or "").lower()
    if role == "fast":
        speed = min(tps / 25.0, 1.0)
        size_ok = 1.0 if params <= 50 else 50.0 / params
        return speed * size_ok
    if role == "heavy":
        return min(params / 120.0, 1.0)
    if role == "hermes":
        return 1.0 if params <= 24 else max(0.15, 24.0 / params)   # 7–24B ideal als Hirn
    if role == "vision":
        return 1.0 if params <= 12 else 0.7                        # klein/günstig bevorzugt
    if role == "coder":
        return 0.5 + 0.5 * min(params / 80.0, 1.0)
    if role == "scout":
        return 1.0 if params <= 40 else 0.5
    return 0.5


def _catalog_role_match(role: str, name: str) -> bool:
    """Ist dieses Modell im kuratierten Katalog (Cookbook) genau für DIESE Rolle gelistet?
    Dann ist es der prinzipien-konforme Pick → starker Bonus."""
    meta = catalog.meta_for_name(name)
    return bool(meta and (meta.get("role") or "").lower() == (role or "").lower())


def _reason(role: str, caps: dict, name: str, fit: dict, fits: bool,
            incomplete: bool, cat_match: bool) -> str:
    if incomplete:
        return "Download unvollständig"
    if role == "vision" and not caps.get("vision"):
        return "keine Vision-Fähigkeit"
    if role == "coder" and not caps.get("coder"):
        return "kein Coder-Modell"
    if role == "hermes" and "hermes" not in (name or "").lower() and caps.get("tools") == "no":
        return "kein natives Tool-Calling"
    if not fits:
        return "passt nicht ins Budget (OOM)"
    bits = []
    if cat_match:
        bits.append("Katalog-Pick ✓")
    if role == "vision":
        bits.append("Vision ✓")
    if role == "coder" and caps.get("coder"):
        bits.append("Coder ✓")
    if role == "hermes":
        bits.append("Hermes" if "hermes" in (name or "").lower()
                    else ("Tools ✓" if caps.get("tools") != "no" else "ohne Tools"))
    if caps.get("moe"):
        bits.append("MoE")
    bits.append(f"{fit['text']}, ~{fit['tps']:.0f} t/s")
    return " · ".join(bits)


def recommend_for_role(role: str) -> dict:
    """Rankt alle installierten Modelle für eine Rolle. Empfohlen = bester geeigneter,
    passender Eintrag. Liefert pro Modell Fit/Eignung/Begründung fürs UI."""
    role = (role or "").strip().lower()
    ram = _ram_gb()
    out = []
    for m in llamaswap.list_models():
        caps = m.get("capabilities") or {}
        params = budget.params_of_model(m)
        quant = m.get("quant") or "Q4_K_M"
        ctx = budget.setup_aware_ctx_for_model(m)["ctx"]
        fit = evaluate_fit(params, quant, ctx, ram, name=m["name"])
        incomplete = bool(m.get("incomplete"))
        fits = (fit["level"] != "too_tight") and not incomplete
        tps = fit["tps"] or 0
        suit = _capability_suit(role, caps, m["name"])
        cat_match = _catalog_role_match(role, m["name"])
        suitable = suit >= 0.5 and fits

        fit_term = {"perfect": 1.0, "marginal": 0.3}.get(fit["level"], -2.0)
        # Eignung dominiert (×2), rollengerechte Größe/Tempo (_pref), Katalog-Anker, dann Fit.
        score = (2.0 * suit) + _pref(role, params, tps) + (0.6 if cat_match else 0.0) + fit_term
        if not fits:
            score -= 5.0

        out.append({
            "name": m["name"], "current_role": m.get("role"),
            "params_b": round(params, 1), "quant": quant,
            "fit": fit, "suitable": suitable, "incomplete": incomplete,
            "score": round(score, 3),
            "reason": _reason(role, caps, m["name"], fit, fits, incomplete, cat_match),
        })

    out.sort(key=lambda x: -x["score"])
    rec = next((o["name"] for o in out if o["suitable"]), None)
    for o in out:
        o["recommended"] = (o["name"] == rec)
    return {"role": role, "recommended": rec, "models": out}
