import io as _io
import json
import os as _os
import re as _re
import subprocess as _subprocess
import zipfile as _zipfile
from pathlib import Path

import requests
from fastapi import APIRouter, Request
from starlette.responses import JSONResponse

from backend.config import LLAMACPP_URL, _load_settings, _save_settings
import backend.models as _models_mod


router = APIRouter()


@router.get("/api/llamacpp/status")
def api_llamacpp_status():
    running = _models_mod._llamacpp_proc is not None and _models_mod._llamacpp_proc.poll() is None
    reachable = False
    if running:
        try:
            settings = _load_settings()
            lc_url = (settings.get("llamacpp_url") or LLAMACPP_URL).rstrip("/")
            response = requests.get(f"{lc_url}/v1/models", timeout=2)
            reachable = response.status_code == 200
        except Exception:
            pass
    return {"running": running, "reachable": reachable}


@router.post("/api/llamacpp/start")
async def api_llamacpp_start(request: Request):
    if _models_mod._llamacpp_proc is not None and _models_mod._llamacpp_proc.poll() is None:
        return {"ok": True, "msg": "Already running"}
    body = await request.json()
    settings = _load_settings()
    model_path = (body.get("model_path") or settings.get("llamacpp_model_path") or "").strip()
    extra_args = (body.get("extra_args") or settings.get("llamacpp_extra_args") or "").strip()
    if not model_path:
        return {"ok": False, "error": "No model path configured. Fill in the Model Path field first."}
    model_path_obj = Path(model_path)
    if not model_path_obj.exists():
        return {"ok": False, "error": f"Model file not found: {model_path}"}
    server_exe = _models_mod._find_llama_server(settings)
    if not server_exe:
        return {"ok": False, "error": "llama-server niet gevonden.", "needs_install": True}
    command = [server_exe, "-m", str(model_path_obj)]
    if extra_args:
        import shlex

        command += shlex.split(extra_args)
    try:
        _models_mod._llamacpp_proc = _subprocess.Popen(
            command,
            stdout=_subprocess.DEVNULL,
            stderr=_subprocess.DEVNULL,
            creationflags=_subprocess.CREATE_NO_WINDOW if hasattr(_subprocess, "CREATE_NO_WINDOW") else 0,
        )
        return {"ok": True, "pid": _models_mod._llamacpp_proc.pid}
    except FileNotFoundError:
        return {"ok": False, "error": "llama-server niet gevonden.", "needs_install": True}
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


@router.get("/api/llamacpp/install-status")
def api_llamacpp_install_status():
    settings = _load_settings()
    exe = _models_mod._find_llama_server(settings)
    return {"installed": exe is not None, "exe": exe or "", "local_dir": str(_models_mod._LLAMACPP_LOCAL_DIR)}


@router.post("/api/llamacpp/install")
async def api_llamacpp_install():
    """Download and install the latest llama.cpp Windows release into tools/llama-cpp/."""
    try:
        gh_resp = requests.get(
            "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest",
            headers={"Accept": "application/vnd.github+json", "User-Agent": "AgentOrchestrator"},
            timeout=15,
        )
        gh_resp.raise_for_status()
        release = gh_resp.json()
        assets = release.get("assets", [])
        preferred_patterns = [
            r"llama-.*-bin-win-avx2-x64\.zip$",
            r"llama-.*-bin-win-avx-x64\.zip$",
            r"llama-.*-bin-win-noavx-x64\.zip$",
            r"llama-.*-bin-win-.*x64\.zip$",
        ]
        chosen = None
        for pattern in preferred_patterns:
            for asset in assets:
                if _re.search(pattern, asset["name"], _re.IGNORECASE):
                    chosen = asset
                    break
            if chosen:
                break
        if not chosen:
            return {"ok": False, "error": "Geen geschikte Windows release gevonden op GitHub."}
        download_url = chosen["browser_download_url"]
        if not download_url.startswith("https://github.com/") and not download_url.startswith("https://objects.githubusercontent.com/"):
            return {"ok": False, "error": "Onverwachte download-URL, installatie geannuleerd."}
        zip_resp = requests.get(download_url, timeout=120, stream=True)
        zip_resp.raise_for_status()
        zip_data = _io.BytesIO(zip_resp.content)
        _models_mod._LLAMACPP_LOCAL_DIR.mkdir(parents=True, exist_ok=True)
        with _zipfile.ZipFile(zip_data) as zip_file:
            for member in zip_file.namelist():
                name = _os.path.basename(member)
                if not name or ".." in member:
                    continue
                if name.lower().endswith((".exe", ".dll")):
                    dest = _models_mod._LLAMACPP_LOCAL_DIR / name
                    with zip_file.open(member) as src, open(dest, "wb") as dst:
                        dst.write(src.read())
        exe_path = _models_mod._LLAMACPP_LOCAL_DIR / "llama-server.exe"
        if not exe_path.is_file():
            return {"ok": False, "error": "llama-server.exe niet gevonden in het gedownloade archief."}
        settings = _load_settings()
        settings["llamacpp_server_exe"] = str(exe_path)
        _save_settings(settings)
        return {"ok": True, "exe": str(exe_path), "version": release.get("tag_name", "?")}
    except requests.exceptions.Timeout:
        return {"ok": False, "error": "Download timeout. Controleer je internetverbinding."}
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


@router.get("/api/llamacpp/detect-ollama")
def api_llamacpp_detect_ollama():
    ollama_models = (
        _os.environ.get("OLLAMA_MODELS")
        or _os.path.join(_os.path.expanduser("~"), ".ollama", "models")
    )
    blobs_dir = _os.path.join(ollama_models, "blobs")
    manifests_root = _os.path.join(ollama_models, "manifests")
    if not _os.path.isdir(blobs_dir):
        return {"found": False, "models_dir": ollama_models, "models": []}
    models = []
    if _os.path.isdir(manifests_root):
        for root, _dirs, files in _os.walk(manifests_root):
            for fname in files:
                manifest_path = _os.path.join(root, fname)
                try:
                    with open(manifest_path, "r", encoding="utf-8") as handle:
                        manifest = json.load(handle)
                    rel = _os.path.relpath(manifest_path, manifests_root).replace("\\", "/")
                    parts = rel.split("/")
                    model_name = "/".join(parts[1:-1]) + ":" + parts[-1] if len(parts) >= 3 else fname
                    for layer in manifest.get("layers", []):
                        if layer.get("mediaType") == "application/vnd.ollama.image.model":
                            digest = layer.get("digest", "").replace(":", "-")
                            blob_path = _os.path.join(blobs_dir, digest)
                            if _os.path.isfile(blob_path):
                                models.append({"name": model_name, "path": blob_path})
                            break
                except Exception:
                    pass
    return {"found": True, "models_dir": ollama_models, "models": models}


@router.post("/api/llamacpp/stop")
def api_llamacpp_stop():
    if _models_mod._llamacpp_proc is None or _models_mod._llamacpp_proc.poll() is not None:
        _models_mod._llamacpp_proc = None
        return {"ok": True, "msg": "Not running"}
    try:
        _models_mod._llamacpp_proc.terminate()
        try:
            _models_mod._llamacpp_proc.wait(timeout=5)
        except Exception:
            _models_mod._llamacpp_proc.kill()
        _models_mod._llamacpp_proc = None
        return {"ok": True}
    except Exception as exc:
        return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)