"""
Lane-Routing für den eingebauten MC2-Gateway (:9001/v1).

Zwei virtuelle Lanes, die Clients/IDEs auswählen — der Router pickt das echte Modell:
- **chat**  (= altes `auto`): Alltag → `fast`, schwer/lang → `heavy`.
- **coding**: Code-Arbeit → `coder` (Qwen3-Coder-Next); riesiger/architektonischer Kontext → `heavy`;
  triviale Kurzfrage ohne Code → `fast` (Tempo).

Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases liegen in einer UI-editierbaren Policy
(routing_policy.py, hot-reload; Env = Defaults). Lucy läuft NICHT hierüber — die ist der
Hermes-Agent (:8642), eigene Ebene.
"""

import re

from services.routing_policy import load_policy

# Modell-Aliases & Zeichen-Schwellen liegen jetzt in der UI-editierbaren Policy
# (routing_policy.py) und kommen pro Request via load_policy() (hot-reload). Die Env-Vars
# sind dort die Defaults. Die Regex-Keyword-Listen unten bleiben bewusst im Code.

# Virtuelle Lanes, die im Gateway als „Modelle" sichtbar sind.
LANES = ["coding", "chat"]
LANE_ALIASES = {"auto": "chat"}  # Rückwärtskompatibel: model:auto == chat

_HEAVY_KW = re.compile(
    r"\b(beweis|prove|theorem|komplex|complex|schwierig|"
    r"think\s*hard|reason\s*carefully|tief\s*nachdenk|optimi[sz]e|"
    r"root\s*cause|analy[sz]e\s+deeply|step[-\s]?by[-\s]?step)\b",
    re.IGNORECASE,
)
# Coding-spezifische „das ist groß/architektonisch" Signale → heavy statt coder.
_CODING_HEAVY_KW = re.compile(
    r"\b(architekt|architect|system[-\s]?design|refactor\s+the\s+(whole|entire)|"
    r"ganze[ns]?\s+(architektur|codebase|projekt)|migrat\w+\s+(the\s+)?(whole|entire|gesamte)|"
    r"entwirf\s+(eine\s+)?architektur|plane?\s+(die\s+)?architektur)\b",
    re.IGNORECASE,
)
# Code-Indikatoren — verhindert, dass echte Code-Anfragen als „trivial" auf fast abrutschen.
# Bewusst breit (inkl. natürlichsprachiger Coding-Begriffe DE+EN): in der coding-Lane soll im Zweifel
# `coder` gewinnen; nur echte Nicht-Code-Kürze ("hallo", "wie spät") rutscht auf fast.
_CODE_HINT = re.compile(
    r"```|\bdef \b|\bclass \b|\bimport \b|\bfunction\b|=>|;\s*$|"
    r"\.(py|ts|tsx|js|jsx|go|rs|java|cpp|c|rb|php|sql)\b|/src/|traceback|stack\s*trace|"
    r"\b(funktion|function|bug|fix|fehler|error|exception|implementier\w*|schreib\w*|"
    r"code\w*|coden|test\w*|klasse|method\w*|methode|refactor\w*|kompil\w*|compile|"
    r"build|deploy|debug|script|skript|api|endpoint|query|regex|json|yaml|"
    r"npm|pip|git|docker|terminal|shell|command)\b",
    re.IGNORECASE | re.MULTILINE,
)


# Bild-Weiche (Faden 11): Aliase, die selbst Bilder koennen — die werden NIE umgeroutet.
VISION_CAPABLE = {"vision", "scout"}
_IMAGE_PART_TYPES = {"image_url", "input_image", "image"}


def has_image(body: dict) -> bool:
    """True, wenn irgendeine Nachricht einen Bild-Part enthaelt (OpenAI-multimodaler
    content: eine Liste mit einem {"type": "image_url"|"input_image"|"image", ...}-Teil)."""
    for m in body.get("messages") or []:
        if not isinstance(m, dict):
            continue
        content = m.get("content")
        if isinstance(content, list):
            for part in content:
                if isinstance(part, dict) and part.get("type") in _IMAGE_PART_TYPES:
                    return True
    return False


def _text_of(body: dict) -> str:
    msgs = body.get("messages") or []
    # Multimodaler content ist eine Liste — nur die Text-Parts fuers Komplexitaets-Routing
    # zusammenziehen (ein dict/Liste als str() wuerde die Zeichen-Schwelle verfaelschen).
    out = []
    for m in msgs:
        if not isinstance(m, dict):
            continue
        c = m.get("content")
        if isinstance(c, str):
            out.append(c)
        elif isinstance(c, list):
            for part in c:
                if isinstance(part, dict) and part.get("type") == "text":
                    out.append(str(part.get("text") or ""))
    return "\n".join(out)


def _route_chat(text: str, n: int) -> tuple[str, str]:
    p = load_policy()
    if n > p["heavy_chars"]:
        return p["heavy"], f"langer Kontext ({n} > {p['heavy_chars']} Zeichen)"
    if _HEAVY_KW.search(text):
        return p["heavy"], "Komplexitäts-Schlüsselwort erkannt"
    return p["fast"], "Standard"


def _route_coding(text: str, n: int) -> tuple[str, str]:
    # Agentisches Coden (OpenCode/RooCode/…) bleibt IMMER beim dedizierten Coder — NIE heavy/fast
    # (das sind Allzweck-Modelle, schwächer bei Code). Die Qwen-Coder packen 256K–1M Kontext selbst,
    # langer Repo-Kontext ist bei Agenten der Normalfall und darf NICHT zu heavy umrouten.
    # (Phase 2b: warme schnelle Coder-Stufe coder_lite als Default + coder als Eskalation.)
    p = load_policy()
    if p["coder_lite"] and not _CODING_HEAVY_KW.search(text) and n <= p["coding_escalate_chars"]:
        return p["coder_lite"], "Coding (schneller Coder)"
    return p["coder"], "Coding -> starker Coder"


def choose_for_lane(lane: str, body: dict) -> tuple[str, str]:
    """Wählt das echte Modell-Alias für eine Lane. Gibt (alias, begründung) zurück."""
    lane = LANE_ALIASES.get((lane or "chat").lower(), (lane or "chat").lower())
    text = _text_of(body)
    n = len(text)
    if lane == "coding":
        return _route_coding(text, n)
    return _route_chat(text, n)  # chat + alles Unbekannte


def choose_model(body: dict) -> tuple[str, str]:
    """Rückwärtskompatibel: altes `model:auto` == chat-Lane."""
    return choose_for_lane("chat", body)
