"""
UI-editierbare Routing-Policy für die Gateway-Lanes (coding/chat).

Persistiert als JSON unter MC_ROUTING_POLICY_PATH (Default MODELS_DIR/mc2-routing.json —
gleiche Konvention wie mc2-discover.json). **Hot-reload:** load_policy() liest die Datei nur
bei Änderung neu (mtime-Cache) → UI-Edits greifen ohne Dienst-Neustart. Die Env-Vars (bisher
einzige Stellschraube in router_logic.py) bleiben als Defaults/Fallback erhalten.

Bewusst NICHT editierbar (v1): die Regex-Keyword-Listen (heavy/coding-heavy/code-hint) — die
bleiben in router_logic.py im Code.
"""

import json
import os
import threading
from pathlib import Path

from config import MODELS_DIR

POLICY_PATH = Path(os.environ.get("MC_ROUTING_POLICY_PATH", str(MODELS_DIR / "mc2-routing.json")))


def _env_bool(name: str, default: str) -> bool:
    return os.environ.get(name, default) not in ("0", "false", "")


# Defaults aus den Env-Vars — Quelle der Wahrheit, solange keine Policy-Datei existiert.
DEFAULTS: dict = {
    "fast": os.environ.get("MC_ROUTE_FAST", "fast"),
    "heavy": os.environ.get("MC_ROUTE_HEAVY", "heavy"),
    "coder": os.environ.get("MC_ROUTE_CODER", "coder"),
    "coder_lite": os.environ.get("MC_ROUTE_CODER_LITE", ""),
    # Bild-Weiche (Faden 11): Requests mit Bild-Anhang werden automatisch hierhin geroutet
    # (die MTP-Hirn-Config kann keine Bilder). "" schaltet die Weiche ab (Passthrough).
    "vision": os.environ.get("MC_ROUTE_VISION", "vision"),
    "heavy_chars": int(os.environ.get("MC_GATEWAY_HEAVY_CHARS", "8000")),
    "coding_escalate_chars": int(os.environ.get("MC_CODING_ESCALATE_CHARS", "120000")),
    "fast_no_think": _env_bool("MC_FAST_NO_THINK", "1"),
}

# Feld-Spezifikation für die UI (Typ + Grenzen + Label). Treibt Editor & Validierung.
FIELDS: list[dict] = [
    {"key": "fast", "label": "fast-Alias (chat: Standard)", "type": "str"},
    {"key": "heavy", "label": "heavy-Alias (chat: lang/komplex)", "type": "str"},
    {"key": "coder", "label": "coder-Alias (coding: stark / Eskalation)", "type": "str"},
    {"key": "coder_lite", "label": "coder-lite-Alias (coding: schneller Default; leer = aus)", "type": "str"},
    {"key": "vision", "label": "vision-Alias (Bild-Weiche: Requests mit Bild; leer = aus)", "type": "str"},
    {"key": "heavy_chars", "label": "chat → heavy ab N Zeichen", "type": "int", "min": 500, "max": 1_000_000},
    {"key": "coding_escalate_chars", "label": "coding → starker Coder ab N Zeichen", "type": "int", "min": 1000, "max": 4_000_000},
    {"key": "fast_no_think", "label": "fast-Spur: Thinking aus (flotte Antworten)", "type": "bool"},
]

_LOCK = threading.Lock()
_CACHE: dict = {"mtime": None, "policy": None}


def _read_file() -> dict:
    try:
        with open(POLICY_PATH, "r", encoding="utf-8") as f:
            data = json.load(f)
        return data if isinstance(data, dict) else {}
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return {}


def _coerce(patch: dict) -> dict:
    """Nur bekannte Keys, typ-/bereichsvalidiert. Wirft ValueError bei ungültigen Werten."""
    spec = {f["key"]: f for f in FIELDS}
    out: dict = {}
    for k, v in (patch or {}).items():
        f = spec.get(k)
        if not f:
            continue  # unbekannte Keys still verwerfen
        if f["type"] == "int":
            iv = int(v)
            lo, hi = f.get("min", 1), f.get("max", 10**9)
            if not (lo <= iv <= hi):
                raise ValueError(f"{k}={iv} außerhalb [{lo}, {hi}]")
            out[k] = iv
        elif f["type"] == "bool":
            out[k] = bool(v)
        else:  # str
            sv = str(v).strip()
            if k not in ("coder_lite", "vision") and not sv:
                raise ValueError(f"{k} darf nicht leer sein")
            out[k] = sv
    return out


def _coerce_safe(patch: dict) -> dict:
    """Wie _coerce, aber schluckt Fehler — kaputte Datei darf den Betrieb nicht stoppen."""
    try:
        return _coerce(patch)
    except (ValueError, TypeError):
        return {}


def load_policy() -> dict:
    """Aktuelle Policy (Datei über DEFAULTS gemerged). Hot-reload via mtime-Cache, pro Request billig."""
    try:
        mtime = POLICY_PATH.stat().st_mtime
    except OSError:
        mtime = None
    with _LOCK:
        if _CACHE["policy"] is None or _CACHE["mtime"] != mtime:
            merged = {**DEFAULTS}
            if mtime is not None:
                merged.update(_coerce_safe(_read_file()))
            _CACHE["mtime"] = mtime
            _CACHE["policy"] = merged
        return dict(_CACHE["policy"])


def save_policy(patch: dict) -> dict:
    """Validiert + persistiert atomar. Gibt die neue, vollständige Policy zurück."""
    clean = _coerce(patch)  # wirft bei ungültigem Input
    with _LOCK:
        current = {**DEFAULTS, **_coerce_safe(_read_file()), **clean}
        POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
        tmp = POLICY_PATH.with_suffix(".json.tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(current, f, ensure_ascii=False, indent=2)
        os.replace(tmp, POLICY_PATH)
        _CACHE["mtime"] = None  # nächster load_policy() lädt frisch
        _CACHE["policy"] = None
    return current


def policy_meta() -> dict:
    """Für den UI-Editor: aktuelle Werte + Defaults (für „Zurücksetzen“) + Feld-Spezifikation."""
    return {"policy": load_policy(), "defaults": dict(DEFAULTS), "fields": FIELDS}
