"""Modelle-Endpoints: Liste (mit Caps), Discover, Fit, Register, Groups."""

import psutil
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from config import HF_DOWNLOAD_ENV, MODELS_DIR
from services import budget, discover, hf, jobengine, llamaswap
from services.fit import evaluate_fit, max_ctx_for

router = APIRouter(prefix="/api")


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


@router.get("/models")
def models() -> dict:
    items = llamaswap.list_models()
    return {"models": items, "count": len(items), "running": llamaswap.get_running_models()}


@router.get("/discover")
def discover_models(force: bool = False) -> dict:
    ram = _ram_gb()
    data = discover.refresh_discover(ram) if force else discover.safe_discover(ram)
    if not data:
        raise HTTPException(502, "Modell-Quellen gerade nicht erreichbar — später erneut.")
    return {**data, "sys_ram_gb": round(ram, 1)}


@router.get("/fit")
def fit(params_b: float = 0, quant: str = "Q4_K_M", ctx: int = 8192,
        name: str = "", role: str = "") -> dict:
    """Hardware-Fit-Vorschau. params_b<=0 → aus KATALOG (echte Metadaten, MoE-bewusst)
    oder sonst aus dem Namen geschätzt. assigned_ctx = der ctx, der TATSÄCHLICH vergeben
    würde: SETUP-BEWUSST (neben Hirn/warmem Set), nicht nur gegen den Gesamt-RAM.
    So sieht die 'Erweiterte Ansicht' vor dem Download Ampel + echten ctx."""
    ram = _ram_gb()
    pb = params_b if params_b > 0 else budget.params_b_for(name)
    saw = budget.setup_aware_ctx(pb, quant, role=role or None)
    return {
        "params_b": round(pb, 1),
        "fit": evaluate_fit(pb, quant, ctx, ram, name=name),
        "optimal_ctx": max_ctx_for(pb, quant, ram),   # Roh-Obergrenze (Modell allein)
        "assigned_ctx": saw["ctx"],                     # setup-bewusst vergeben
        "budget": {"gtt_gb": saw["gtt_gb"], "reserved_gb": saw["reserved_gb"],
                   "budget_gb": saw["budget_gb"], "mode": saw["mode"]},
        "sys_ram_gb": round(ram, 1),
    }


class RegisterReq(BaseModel):
    model_path: str
    role: str | None = None
    ctx: int = 8192
    ttl: int | None = None
    mmproj_path: str | None = None
    jinja: bool = False


@router.post("/models/register")
def register(req: RegisterReq) -> dict:
    try:
        model_id = llamaswap.register_model(
            req.model_path, role=req.role, ctx=req.ctx, ttl=req.ttl,
            mmproj_path=req.mmproj_path, jinja=req.jinja,
        )
    except PermissionError as exc:
        raise HTTPException(500, str(exc))
    return {"ok": True, "model_id": model_id}


class InstallReq(BaseModel):
    repo: str
    role: str | None = None
    quant: str = "Q4_K_M"
    ctx: int | None = None
    jinja: bool = False
    hf_token: str | None = None


@router.get("/hf/search")
def hf_search(q: str = "") -> dict:
    return {"results": hf.search(q)}


@router.get("/hf/quants")
def hf_quants(repo: str) -> dict:
    repo = hf.normalize_repo(repo)
    return {"repo": repo, "quants": hf.list_quants(repo)}


@router.post("/models/install")
def install(req: InstallReq) -> dict:
    """Lädt ein Modell von HuggingFace (Hintergrund-Job) UND trägt es sofort in
    llama-swap ein (cmd + Rolle-Alias). llama-swap (-watch-config) lädt es, sobald
    die Datei da ist. Split-GGUFs werden komplett geladen, registriert wird der
    erste Teil (-00001-of-…). Akzeptiert volle HF-URL ODER org/repo."""
    repo = hf.normalize_repo(req.repo)
    info = hf.resolve_gguf(repo, req.quant)
    if not info["first"]:
        raise HTTPException(404, f"Keine GGUF-Datei für Quant '{req.quant}' in {repo} gefunden.")

    subdir = repo.split("/")[-1]
    target = MODELS_DIR / subdir
    target.mkdir(parents=True, exist_ok=True)
    model_path = str(target / info["first"])
    mmproj_path = str(target / info["mmproj"]) if info["mmproj"] else None

    ctx = req.ctx
    if ctx is None:
        # SETUP-BEWUSST: größter ctx, der neben Hirn/warmem Set passt (nicht nur Modell allein).
        ctx = budget.setup_aware_ctx(budget.params_b_for(repo), req.quant, role=req.role)["ctx"]

    # Sofort registrieren (Datei kommt gleich) — robust gegen -watch-config.
    try:
        model_id = llamaswap.register_model(
            model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja)
    except PermissionError as exc:
        raise HTTPException(500, str(exc))

    # Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
    args = [hf.hf_bin(), "download", repo]
    for f in info["files"]:
        args.append(f)
    if info["mmproj"]:
        args.append(info["mmproj"])
    args += ["--local-dir", str(target)]
    env = dict(HF_DOWNLOAD_ENV)
    if req.hf_token:
        env["HF_TOKEN"] = req.hf_token
    job_id = jobengine.start_job(args, f"download {req.repo}", env=env)
    jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
    return {"ok": True, "job_id": job_id, "model_id": model_id, "model_path": model_path,
            "total_bytes": info["total_bytes"], "files": len(info["files"])}


@router.get("/jobs")
def jobs() -> dict:
    return {"jobs": jobengine.public_jobs()}


@router.post("/jobs/{job_id}/cancel")
def cancel(job_id: str) -> dict:
    return {"ok": jobengine.cancel_job(job_id)}


class RoleReq(BaseModel):
    role: str | None = None


@router.get("/roles/{role}/recommend")
def recommend_role(role: str) -> dict:
    """Welches installierte Modell passt am besten auf diese Rolle? (Capability + setup-
    bewusster Fit). Basis für 'Empfohlen'-Hinweis + Auto-Pick im Rollen-Zuweisungs-Modal."""
    from services import roles
    return roles.recommend_for_role(role)


@router.post("/models/{model_id}/role")
def set_model_role(model_id: str, body: RoleReq) -> dict:
    # Das Agent-Hirn (Rolle 'hermes') braucht den warm-bewussten Flow (Alias + brains-Gruppe +
    # ttl 0 + Hermes config.default + Gateway-Restart) — Single Source of Truth UI ↔ Hermes.
    if (body.role or "").strip().lower() == "hermes":
        from services.agent import set_agent_brain
        res = set_agent_brain(model_id)
        if not res.get("ok"):
            raise HTTPException(400, res.get("reason", "Fehler beim Setzen des Agent-Hirns"))
        return res
    if not llamaswap.set_role(model_id, body.role):
        raise HTTPException(404, "Modell nicht gefunden")
    return {"ok": True}


class CtxReq(BaseModel):
    ctx: int


@router.get("/models/{model_id}/ctx/auto")
def auto_ctx(model_id: str) -> dict:
    """Setup-bewusster Optimal-ctx für ein bestehendes Modell (Rolle/Params/Quant +
    aktuelles Setup). Basis für den 'Auto'-Button an der Modellkarte."""
    m = next((x for x in llamaswap.list_models() if x["name"] == model_id), None)
    if not m:
        raise HTTPException(404, "Modell nicht gefunden")
    saw = budget.setup_aware_ctx_for_model(m)
    return {"model_id": model_id, "current_ctx": m.get("ctx"),
            "params_b": round(budget.params_of_model(m), 1), "quant": m.get("quant"),
            "role": m.get("role"), **saw}


@router.post("/models/{model_id}/ctx")
def set_model_ctx(model_id: str, body: CtxReq) -> dict:
    if not llamaswap.set_ctx(model_id, body.ctx):
        raise HTTPException(404, "Modell nicht gefunden")
    return {"ok": True}


@router.get("/models/drafts")
def list_drafts(target: str = "") -> dict:
    """Verfügbare Draft-Modelle + ihre Vocab-Kompatibilität zum Ziel-Modell
    (target = GGUF-Pfad). Basis für die idiotensichere Spec-Draft-Auswahl im UI."""
    return llamaswap.drafts_for(target)


class DraftReq(BaseModel):
    draft_path: str | None = None


@router.post("/models/{model_id}/draft")
def set_model_draft(model_id: str, body: DraftReq) -> dict:
    """Setzt/entfernt den Speculative-Decoding-Draft eines Modells. Inkompatible
    (oder nicht prüfbare) Drafts werden serverseitig abgelehnt."""
    try:
        res = llamaswap.set_spec_draft(model_id, body.draft_path)
    except PermissionError as exc:
        raise HTTPException(500, str(exc))
    if not res["ok"]:
        raise HTTPException(400 if "kompatib" in res["reason"].lower() else 404, res["reason"])
    return res


@router.post("/models/unload")
def unload_all_models() -> dict:
    import httpx
    from config import LLAMA_SWAP_URL
    try:
        with httpx.Client(timeout=10.0) as c:
            r = c.post(f"{LLAMA_SWAP_URL}/api/models/unload")
            return {"ok": r.status_code == 200}
    except Exception as exc:
        raise HTTPException(500, str(exc))


@router.post("/models/{model_id}/unload")
def unload_model(model_id: str) -> dict:
    import httpx
    from config import LLAMA_SWAP_URL
    try:
        with httpx.Client(timeout=10.0) as c:
            r = c.post(f"{LLAMA_SWAP_URL}/api/models/unload/{model_id}")
            return {"ok": r.status_code == 200}
    except Exception as exc:
        raise HTTPException(500, str(exc))


@router.post("/models/{model_id}/load")
def load_model(model_id: str) -> dict:
    import httpx
    from config import LLAMA_SWAP_URL
    try:
        # Trigger load by sending a lightweight completion request.
        body = {
            "model": model_id,
            "messages": [{"role": "user", "content": "ping"}],
            "max_tokens": 1
        }
        # High timeout because model loading might take time
        with httpx.Client(timeout=60.0) as c:
            c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=body)
            return {"ok": True}
    except Exception as exc:
        raise HTTPException(500, str(exc))


@router.delete("/models/{model_id}")
def delete(model_id: str) -> dict:
    if not llamaswap.delete_model(model_id):
        raise HTTPException(404, "Modell nicht gefunden")
    return {"ok": True}


@router.get("/groups")
def groups() -> dict:
    return {"groups": llamaswap.list_groups()}


class GroupReq(BaseModel):
    group: str
    members: list[str]
    swap: bool = False
    persist: bool = False


@router.put("/groups")
def set_group(req: GroupReq) -> dict:
    try:
        llamaswap.set_group(req.group, req.members, swap=req.swap, persist=req.persist)
    except PermissionError as exc:
        raise HTTPException(500, str(exc))
    return {"ok": True}
