"""_Handler — HTTP utilities, routing, and static file serving."""
from __future__ import annotations

import json
import mimetypes
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from typing import Any

from ._audio import AudioMixin
from ._chat import ChatMixin
from ._images import ImagesMixin

# Serve the pre-built React app from dist/.  Run `npm run build` inside
# chat_ui/ once to populate this directory.
_CHAT_UI_DIR = Path(__file__).parent.parent / "chat_ui" / "dist"


class _Handler(ChatMixin, ImagesMixin, AudioMixin, BaseHTTPRequestHandler):
    """Single HTTP handler for all model endpoints.

    ``_contexts`` (dict[model_id, HandlerContext]) is injected as a class
    variable by ``_make_handler`` before the server starts.  Every per-request
    instance shares the same frozen dict.

    POST endpoints read the optional ``model`` field from the JSON body and
    route to the matching context; if absent the first loaded model is used.
    """

    # Populated by _make_handler(); type annotation only — no default value.
    _contexts: dict[str, Any]  # {model_id: HandlerContext}

    def log_message(self, *_args: Any) -> None:
        """Suppress default per-request access log."""

    # ── context resolution ───────────────────────────────────────────────────

    @property
    def _context(self) -> Any:
        """Return the first/default context (single-model or fallback)."""
        return next(iter(self._contexts.values()))

    def _resolve_ctx(self, model_id: str | None) -> Any:
        """Return the HandlerContext for *model_id*, or the default."""
        if model_id and model_id in self._contexts:
            return self._contexts[model_id]
        return self._context

    # ── low-level helpers ────────────────────────────────────────────────────

    def _send_json(self, status: int, body: dict) -> None:
        data = json.dumps(body).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def _send_bytes(self, status: int, data: bytes, mime: str) -> None:
        self.send_response(status)
        self.send_header("Content-Type", mime)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def _read_body(self) -> dict | None:
        length = int(self.headers.get("Content-Length", 0))
        try:
            return json.loads(self.rfile.read(length))
        except json.JSONDecodeError:
            self._send_json(400, {"error": "invalid JSON body"})
            return None

    def _read_raw_body(self) -> bytes:
        length = int(self.headers.get("Content-Length", 0))
        return self.rfile.read(length)

    def _start_sse(self) -> None:
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("X-Accel-Buffering", "no")
        self.end_headers()

    def _sse_write(self, payload: dict) -> None:
        self.wfile.write(f"data: {json.dumps(payload)}\n\n".encode())
        self.wfile.flush()

    def _not_supported(self, endpoint: str) -> None:
        self._send_json(501, {
            "error": (
                f"'{endpoint}' is not supported by this model. "
                "Load the correct model type."
            )
        })

    # ── static file serving ──────────────────────────────────────────────────

    def _serve_static(self, rel_path: str) -> None:
        """Serve a file from chat_ui/ with path-traversal protection."""
        try:
            ui_root = _CHAT_UI_DIR.resolve()
            target  = (ui_root / rel_path).resolve()
            target.relative_to(ui_root)  # raises ValueError if outside
        except (ValueError, OSError):
            self._send_json(403, {"error": "forbidden"})
            return

        if not target.exists() or not target.is_file():
            self._send_json(404, {"error": "not found"})
            return

        mime, _ = mimetypes.guess_type(str(target))
        mime = mime or "application/octet-stream"
        self._send_bytes(200, target.read_bytes(), mime)

    # ── GET ──────────────────────────────────────────────────────────────────

    def do_GET(self) -> None:  # pylint: disable=invalid-name
        path = self.path.split("?", 1)[0]  # strip query string

        if path == "/health":
            self._send_json(200, {
                "status": "ok",
                "models": [ctx.model_id for ctx in self._contexts.values()],
            })

        elif path in ("/chat", "/chat/"):
            idx = _CHAT_UI_DIR / "index.html"
            try:
                first_ctx   = self._context
                models_json = json.dumps([
                    {"id": ctx.model_id, "category": ctx.category}
                    for ctx in self._contexts.values()
                ])
                html = (
                    idx.read_text(encoding="utf-8")
                    .replace("{{MODELS_JSON}}", models_json)
                    .replace("{{MODEL_ID}}", first_ctx.model_id)
                    .replace("{{MODEL_CATEGORY}}", first_ctx.category)
                )
                self._send_bytes(200, html.encode(), "text/html; charset=utf-8")
            except OSError as exc:
                self._send_json(500, {"error": str(exc)})

        elif path.startswith("/static/"):
            self._serve_static(path[len("/static/"):])

        elif path == "/v1/models":
            data = [ctx.model_obj for ctx in self._contexts.values()]
            self._send_json(200, {"object": "list", "data": data})

        elif path.startswith("/v1/models/"):
            req_id = path[len("/v1/models/"):]
            ctx = self._contexts.get(req_id)
            if ctx:
                self._send_json(200, ctx.model_obj)
            else:
                self._send_json(404, {"error": f"model '{req_id}' not found"})

        else:
            self._send_json(404, {"error": "not found"})

    # ── POST ─────────────────────────────────────────────────────────────────

    def do_POST(self) -> None:  # pylint: disable=invalid-name
        path   = self.path.split("?", 1)[0]
        routes = {
            "/v1/chat/completions":     self._handle_chat_completions,
            "/v1/completions":          self._handle_completions,
            "/v1/embeddings":           self._handle_embeddings,
            "/v1/images/generations":   self._handle_image_generations,
            "/v1/audio/transcriptions": self._handle_audio_transcriptions,
            "/v1/audio/speech":         self._handle_audio_speech,
        }
        handler_fn = routes.get(path)
        if handler_fn:
            handler_fn()
        else:
            self._send_json(404, {"error": "not found"})
