"""Eskalations-Kaskade: qwen2.5:7b → OVH 120b → DOWN_FOR_REVIEW.

User-Vorgabe 2026-06-08:
  - Standard: deterministisch (Pattern + MC + KB)
  - Eskalation nur wenn deterministisches Ergebnis unklar bleibt.
  - Stage 1: lokales qwen2.5:7b (schnell, billig)
  - Stage 2: OVH 120b (groß, externalisiert)
  - Stage 3: Claude API mit Anonymisierung — NICHT in Phase 1 aktiv.
  - Bei Total-Fail: Output "DOWN_FOR_REVIEW" (kein Halluzinieren).

Cost-Cap: jeder Agent meldet sein Token-Budget. Wenn überschritten:
abbrechen und "manuelle Prüfung empfohlen" emittieren.
"""

from __future__ import annotations

import json
import logging
import os
import time
from typing import Any

import httpx

from ._base import EscalationLog, SourceType

logger = logging.getLogger(__name__)

OLLAMA_URL = os.environ.get(
    "OLLAMA_URL", "http://host.docker.internal:11434",
)
# Default ist das Self-Hosted-LLM des Stacks (qwen3.5:35b-a3b auf
# macmini, gesetzt via SELF_HOSTED_LLM_MODEL). Kann pro Agent via
# AGENT_MODEL_LOCAL überschrieben werden.
OLLAMA_MODEL_LOCAL = os.environ.get(
    "AGENT_MODEL_LOCAL",
    os.environ.get("SELF_HOSTED_LLM_MODEL", "qwen3.5:35b-a3b"),
)
OVH_URL = os.environ.get("OVH_LLM_URL", "")
OVH_MODEL = os.environ.get("OVH_LLM_MODEL", "Meta-Llama-3.1-70B-Instruct")
OVH_API_KEY = os.environ.get("OVH_LLM_API_KEY", "")
OVH_TIMEOUT = float(os.environ.get("OVH_LLM_TIMEOUT", "60"))
LOCAL_TIMEOUT = float(os.environ.get("AGENT_LOCAL_TIMEOUT", "60"))


class EscalationResult:
    """Wrapper für ein Eskalations-Resultat."""

    def __init__(
        self,
        content: str | None,
        stage: SourceType,
        model: str,
        log: EscalationLog,
        parsed: Any = None,
    ) -> None:
        self.content = content
        self.stage = stage
        self.model = model
        self.log = log
        self.parsed = parsed

    @property
    def success(self) -> bool:
        return bool(self.content)


async def call_local_llm(
    system_prompt: str,
    user_prompt: str,
    expect_json: bool = True,
    model: str | None = None,
    timeout: float | None = None,
) -> EscalationResult:
    """Stage 1: lokales Ollama-Modell."""
    mdl = model or OLLAMA_MODEL_LOCAL
    body = {
        "model": mdl,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        "stream": False,
        "options": {"temperature": 0.0, "seed": 42, "num_predict": 1200},
    }
    if expect_json:
        body["format"] = "json"
    t0 = time.time()
    log = EscalationLog(
        stage=SourceType.LLM_LOCAL,
        model=mdl,
        success=False,
    )
    try:
        async with httpx.AsyncClient(timeout=timeout or LOCAL_TIMEOUT) as c:
            r = await c.post(f"{OLLAMA_URL}/api/chat", json=body)
            r.raise_for_status()
            j = r.json()
            content = (j.get("message") or {}).get("content", "") or ""
            log.tokens_in = int(j.get("prompt_eval_count") or 0)
            log.tokens_out = int(j.get("eval_count") or 0)
    except Exception as e:
        log.error = f"{type(e).__name__}: {str(e)[:120]}"
        logger.warning("call_local_llm fail: %s", log.error)
        log.duration_ms = int((time.time() - t0) * 1000)
        return EscalationResult(None, SourceType.LLM_LOCAL, mdl, log)
    log.duration_ms = int((time.time() - t0) * 1000)
    parsed = None
    if content and expect_json:
        parsed = _try_parse_json(content)
        if parsed is None:
            log.error = "json_parse_fail"
            content = None
    if content:
        log.success = True
    return EscalationResult(content, SourceType.LLM_LOCAL, mdl, log, parsed)


async def call_ovh_llm(
    system_prompt: str,
    user_prompt: str,
    expect_json: bool = True,
) -> EscalationResult:
    """Stage 2: OVH 120b (extern). Deaktiviert wenn OVH_URL leer."""
    log = EscalationLog(
        stage=SourceType.LLM_LOCAL_BIG,
        model=OVH_MODEL,
        success=False,
    )
    if not OVH_URL:
        log.error = "ovh_disabled"
        return EscalationResult(None, SourceType.LLM_LOCAL_BIG, OVH_MODEL, log)
    body = {
        "model": OVH_MODEL,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        "temperature": 0.0,
        "max_tokens": 1500,
    }
    if expect_json:
        body["response_format"] = {"type": "json_object"}
    headers = {"Content-Type": "application/json"}
    if OVH_API_KEY:
        headers["Authorization"] = f"Bearer {OVH_API_KEY}"
    t0 = time.time()
    try:
        async with httpx.AsyncClient(timeout=OVH_TIMEOUT) as c:
            r = await c.post(
                f"{OVH_URL.rstrip('/')}/v1/chat/completions",
                json=body,
                headers=headers,
            )
            r.raise_for_status()
            j = r.json()
            content = (
                ((j.get("choices") or [{}])[0]
                 .get("message") or {})
                .get("content", "")
            ) or ""
            usage = j.get("usage") or {}
            log.tokens_in = int(usage.get("prompt_tokens") or 0)
            log.tokens_out = int(usage.get("completion_tokens") or 0)
    except Exception as e:
        log.error = f"{type(e).__name__}: {str(e)[:120]}"
        logger.warning("call_ovh_llm fail: %s", log.error)
        log.duration_ms = int((time.time() - t0) * 1000)
        return EscalationResult(None, SourceType.LLM_LOCAL_BIG, OVH_MODEL, log)
    log.duration_ms = int((time.time() - t0) * 1000)
    parsed = None
    if content and expect_json:
        parsed = _try_parse_json(content)
        if parsed is None:
            log.error = "json_parse_fail"
            content = None
    if content:
        log.success = True
    return EscalationResult(content, SourceType.LLM_LOCAL_BIG, OVH_MODEL,
                            log, parsed)


def _try_parse_json(content: str) -> Any | None:
    """Robust JSON extract (handles fences, prose-wrap)."""
    if not content:
        return None
    s = content.strip()
    if s.startswith("```"):
        s = s.strip("`")
        if s.lower().startswith("json"):
            s = s[4:]
        s = s.strip()
    first_o, last_o = s.find("{"), s.rfind("}")
    first_a, last_a = s.find("["), s.rfind("]")
    candidates = []
    if first_o >= 0 and last_o > first_o:
        candidates.append(s[first_o:last_o + 1])
    if first_a >= 0 and last_a > first_a:
        candidates.append(s[first_a:last_a + 1])
    for c in candidates:
        try:
            return json.loads(c)
        except Exception:
            continue
    return None


async def cascade(
    system_prompt: str,
    user_prompt: str,
    expect_json: bool = True,
    skip_ovh: bool = False,
) -> tuple[EscalationResult | None, list[EscalationLog]]:
    """Default-Kaskade: Local → OVH. Bei Local-Erfolg KEIN OVH-Call."""
    logs: list[EscalationLog] = []
    res = await call_local_llm(system_prompt, user_prompt,
                                 expect_json=expect_json)
    logs.append(res.log)
    if res.success:
        return res, logs
    if skip_ovh or not OVH_URL:
        return None, logs
    res2 = await call_ovh_llm(system_prompt, user_prompt,
                                expect_json=expect_json)
    logs.append(res2.log)
    if res2.success:
        return res2, logs
    return None, logs
