from __future__ import annotations

import json
import re
from typing import Any

import httpx

from .models import RouteDecision, RouteRequest


_CANCEL_RE = re.compile(
    r"^(?:bitte\s+)?(?:stopp?|stoppe|abbrechen|brich\s+ab|hör\s+auf|cancel)(?:\s|[.!?])*$",
    re.IGNORECASE,
)
_VIDEO_RE = re.compile(
    r"\b(?:erstell|generier|mach|produzier|render|create|generate|make)\w*\b[^\n]{0,80}"
    r"\b(?:video|clip|film|animation)\w*\b|"
    r"\b(?:video|clip|film|animation)\w*\b[^\n]{0,80}"
    r"\b(?:erstell|generier|mach|produzier|render|create|generate|make)\w*\b",
    re.IGNORECASE,
)
_IMAGE_RE = re.compile(
    r"\b(?:erstell|generier|mach|zeichne|render|create|generate|make|draw)\w*\b[^\n]{0,80}"
    r"\b(?:bild|foto|image|picture|illustration|grafik)\w*\b|"
    r"\b(?:bild|foto|image|picture|illustration|grafik)\w*\b[^\n]{0,80}"
    r"\b(?:erstell|generier|mach|zeichne|render|create|generate|make|draw)\w*\b",
    re.IGNORECASE,
)
_HERMES_RE = re.compile(
    r"\b(?:öffne|klick|tippe|fülle|starte|schließe|verschiebe|kopiere|speichere|"
    r"browse|open|click|type|fill|launch|close|move|copy|save)\w*\b|"
    r"\b(?:terminal|powershell|browser|maus|cursor|datei|ordner|computer|desktop|"
    r"webcam|kamera|camera|screenshot|bildschirm|screen)\b|"
    r"\b(?:such|recherchier|search|research)\w*\b[^\n]{0,80}\b(?:internet|web|online)\b|"
    r"\b(?:code|skript|script|programm)\b[^\n]{0,60}\b(?:schreib|änder|fix|debug|bau|create|write|edit)\w*\b",
    re.IGNORECASE,
)
_HIGH_RISK_RE = re.compile(
    r"\b(?:lösch|delete|entfern|remove|kauf|buy|zahl|pay|überweis|transfer|send|"
    r"install|deinstall|uninstall|format|shutdown|herunterfahr)\w*\b",
    re.IGNORECASE,
)
_CLARIFY_RE = re.compile(
    r"^(?:mach|tu|do|make|erledige)(?:\s+(?:das|es|it|this))?[.!?\s]*$",
    re.IGNORECASE,
)
_DEEP_THINKING_RE = re.compile(
    r"\b(?:analysier|begründe|beweis|vergleich|strategie|architektur|optimi|debug|"
    r"komplex|mehrstufig|trade-?off|sicherheit|kryptograf|research|investigate|"
    r"analy[sz]e|reason|plan)\w*\b",
    re.IGNORECASE,
)


class Router:
    """Deterministic first-stage router with an optional local OpenAI endpoint."""

    def __init__(
        self,
        *,
        endpoint: str,
        model: str,
        enabled: bool = False,
        connect_timeout_s: float = 0.75,
        request_timeout_s: float = 2.0,
        client: httpx.AsyncClient | None = None,
    ) -> None:
        self.endpoint = endpoint.rstrip("/")
        self.model = model
        self.enabled = enabled
        self._owns_client = client is None
        self.client = client or httpx.AsyncClient(
            timeout=httpx.Timeout(request_timeout_s, connect=connect_timeout_s)
        )

    async def close(self) -> None:
        if self._owns_client:
            await self.client.aclose()

    async def decide(self, request: RouteRequest) -> RouteDecision:
        deterministic = self.decide_deterministic(request)
        if deterministic is not None:
            return deterministic.model_copy(
                update={"thinking_oz": self.decide_thinking_oz(request, deterministic.route)}
            )
        if self.enabled:
            try:
                return await self._decide_with_model(request)
            except (httpx.HTTPError, ValueError, KeyError, TypeError):
                pass
        return RouteDecision(
            route="dialogue",
            confidence=0.72,
            needs_vision=request.needs_vision,
            risk="low",
            reason_code="dialogue_default",
            ack="Ich höre zu.",
            thinking_oz=self.decide_thinking_oz(request, "dialogue"),
        )

    @staticmethod
    def decide_thinking_oz(request: RouteRequest, route: str = "dialogue") -> int:
        """Choose the smallest useful reasoning budget before work starts.

        The local router model supplies this value when enabled. This fast
        fallback keeps automatic mode deterministic while the router is
        offline, so the UI never has to ask the user for an effort level.
        """
        text = " ".join(request.text.strip().split())
        if route in {"cancel", "clarify"}:
            return 2
        score = 0
        if len(text) > 700:
            score += 2
        elif len(text) > 180:
            score += 1
        if len(re.findall(r"[?;]|\b(?:und dann|danach|außerdem|zusätzlich)\b", text, re.I)) >= 3:
            score += 1
        if _DEEP_THINKING_RE.search(text):
            score += 2
        if route == "hermes":
            score += 1
        if score >= 2:
            return 8
        if score >= 1:
            return 4
        return 2

    @staticmethod
    def decide_deterministic(request: RouteRequest) -> RouteDecision | None:
        text = " ".join(request.text.strip().split())
        if _CANCEL_RE.search(text):
            return RouteDecision(
                route="cancel", confidence=0.99, needs_vision=False, risk="low",
                reason_code="explicit_cancel", ack="Ich stoppe den laufenden Vorgang.",
            )
        if _CLARIFY_RE.search(text):
            return RouteDecision(
                route="clarify", confidence=0.97, needs_vision=request.needs_vision,
                risk="low", reason_code="missing_object", ack="Was genau soll ich tun?",
            )
        if _VIDEO_RE.search(text):
            return RouteDecision(
                route="video", confidence=0.97, needs_vision=request.needs_vision,
                risk="low", reason_code="explicit_video_generation",
                ack="Ich erstelle das Video im Hintergrund.",
            )
        if _IMAGE_RE.search(text):
            return RouteDecision(
                route="image", confidence=0.97, needs_vision=request.needs_vision,
                risk="low", reason_code="explicit_image_generation",
                ack="Ich erstelle das Bild im Hintergrund.",
            )
        if _HERMES_RE.search(text):
            high_risk = bool(_HIGH_RISK_RE.search(text))
            return RouteDecision(
                route="hermes", confidence=0.94, needs_vision=request.needs_vision,
                risk="high" if high_risk else "medium",
                reason_code="computer_action_high_risk" if high_risk else "computer_action",
                ack=(
                    "Ich bereite den Vorgang vor und frage vor kritischen Schritten nach."
                    if high_risk else "Ich kümmere mich darum."
                ),
            )
        return None

    async def _decide_with_model(self, request: RouteRequest) -> RouteDecision:
        response = await self.client.post(
            f"{self.endpoint}/chat/completions",
            json={
                "model": self.model,
                "temperature": 0,
                "max_tokens": 160,
                "messages": [
                    {
                        "role": "system",
                        "content": (
                            "Route the user request. Return only a JSON object with keys: "
                            "route (dialogue|hermes|video|image|cancel|clarify), confidence "
                            "(0..1), needs_vision (bool), risk (low|medium|high), "
                            "reason_code, ack, thinking_oz (2|4|8). Choose the smallest "
                            "thinking_oz sufficient for the request before any work starts: "
                            "2 for simple, 4 for normal multi-step, 8 for deep or complex. "
                            "Never execute a tool."
                        ),
                    },
                    {"role": "user", "content": request.text},
                ],
            },
        )
        response.raise_for_status()
        payload: dict[str, Any] = response.json()
        content = payload["choices"][0]["message"]["content"]
        if not isinstance(content, str):
            raise ValueError("Router response content is not text")
        content = content.strip()
        if content.startswith("```"):
            content = re.sub(r"^```(?:json)?\s*|\s*```$", "", content, flags=re.I)
        decision = RouteDecision.model_validate(json.loads(content))
        if request.needs_vision and not decision.needs_vision:
            decision = decision.model_copy(update={"needs_vision": True})
        return decision
