from __future__ import annotations

from dataclasses import dataclass
import json
import re
from typing import Any, BinaryIO, Callable
from urllib.parse import parse_qs, urlsplit


class HttpError(Exception):
    def __init__(self, status: int, message: str, *, code: str = "error") -> None:
        super().__init__(message)
        self.status = status
        self.code = code
        self.message = message


@dataclass(frozen=True)
class Response:
    status: int
    headers: dict[str, str]
    body: bytes = b""
    file_path: str | None = None


@dataclass
class Request:
    method: str
    raw_path: str
    path: str
    query: dict[str, list[str]]
    headers: dict[str, str]
    client_ip: str
    rfile: BinaryIO
    content_length: int
    path_params: dict[str, str]
    user: dict[str, Any] | None = None
    session: dict[str, Any] | None = None
    _body: bytes | None = None

    def header(self, name: str) -> str | None:
        return self.headers.get(name.lower())

    def read_body(self, *, max_bytes: int) -> bytes:
        if self._body is not None:
            return self._body
        if self.content_length < 0:
            raise HttpError(411, "Missing Content-Length", code="length_required")
        if self.content_length > max_bytes:
            raise HttpError(413, "Body too large", code="payload_too_large")
        self._body = self.rfile.read(self.content_length)
        return self._body

    def json(self, *, max_bytes: int = 1_000_000) -> Any:
        body = self.read_body(max_bytes=max_bytes)
        try:
            return json.loads(body.decode("utf-8"))
        except Exception as exc:  # noqa: BLE001
            raise HttpError(400, "Invalid JSON", code="invalid_json") from exc


Handler = Callable[[Request], Response]


@dataclass(frozen=True)
class _Route:
    method: str
    template: str
    pattern: re.Pattern[str]
    param_names: tuple[str, ...]
    handler: Handler


class Router:
    def __init__(self) -> None:
        self._routes: list[_Route] = []

    def add(self, method: str, template: str, handler: Handler) -> None:
        method = method.upper()
        if not template.startswith("/"):
            raise ValueError("route template must start with /")

        param_names: list[str] = []
        regex = ""
        for part in template.strip("/").split("/"):
            if part.startswith("{") and part.endswith("}"):
                name = part[1:-1].strip()
                if not name:
                    raise ValueError("empty path param")
                param_names.append(name)
                regex += rf"/(?P<{name}>[^/]+)"
            else:
                regex += "/" + re.escape(part)
        if template == "/":
            regex = ""
        pattern = re.compile(rf"^{regex or '/'}$")
        self._routes.append(
            _Route(
                method=method,
                template=template,
                pattern=pattern,
                param_names=tuple(param_names),
                handler=handler,
            )
        )

    def match(self, method: str, path: str) -> tuple[Handler, dict[str, str]] | None:
        method = method.upper()
        for route in self._routes:
            if route.method != method:
                continue
            m = route.pattern.match(path)
            if not m:
                continue
            return route.handler, {k: v for k, v in m.groupdict().items() if v is not None}
        return None


def parse_request_target(raw_path: str) -> tuple[str, dict[str, list[str]]]:
    parts = urlsplit(raw_path)
    path = parts.path or "/"
    query = parse_qs(parts.query, keep_blank_values=True)
    return path, query


def json_response(status: int, data: Any, *, headers: dict[str, str] | None = None) -> Response:
    body = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
    base_headers = {
        "content-type": "application/json; charset=utf-8",
        "content-length": str(len(body)),
    }
    if headers:
        base_headers.update(headers)
    return Response(status=status, headers=base_headers, body=body)
