"""Minimal Aturio client for the public Pasal REST API."""

from __future__ import annotations

from dataclasses import dataclass, field
import json
from typing import Any, Dict, Mapping, MutableMapping, Optional
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen

DEFAULT_BASE_URL = "https://pasal.id/api/v1"
DEFAULT_DOCS_URL = "https://pasal.id/api"


class AturioApiError(RuntimeError):
    """Raised when the upstream API returns an error or invalid response."""

    def __init__(
        self,
        message: str,
        *,
        status: Optional[int] = None,
        url: Optional[str] = None,
        body: Any = None,
    ) -> None:
        super().__init__(message)
        self.status = status
        self.url = url
        self.body = body


def _normalize_base_url(base_url: str = DEFAULT_BASE_URL) -> str:
    normalized = str(base_url).rstrip("/")
    if normalized.endswith("/api"):
        return f"{normalized}/v1"
    return normalized


def _normalize_frbr_uri(frbr_uri: str) -> str:
    if not isinstance(frbr_uri, str) or not frbr_uri.strip():
        raise TypeError("frbr_uri must be a non-empty string.")
    return frbr_uri.lstrip("/")


def _build_url(base_url: str, path: str, params: Optional[Mapping[str, Any]]) -> str:
    normalized_path = str(path).lstrip("/")
    url = f"{base_url}/{normalized_path}"
    filtered = {
        key: value
        for key, value in (params or {}).items()
        if value is not None and value != ""
    }
    if filtered:
        return f"{url}?{urlencode(filtered)}"
    return url


@dataclass(frozen=True)
class AturioClient:
    """Small synchronous client for the public Indonesian legal REST API."""

    base_url: str = DEFAULT_BASE_URL
    timeout: float = 30.0
    headers: Mapping[str, str] = field(default_factory=dict)

    def __post_init__(self) -> None:
        object.__setattr__(self, "base_url", _normalize_base_url(self.base_url))

    @property
    def docs_url(self) -> str:
        return DEFAULT_DOCS_URL

    def request(
        self,
        path: str,
        *,
        params: Optional[Mapping[str, Any]] = None,
        headers: Optional[Mapping[str, str]] = None,
        method: str = "GET",
    ) -> Any:
        url = _build_url(self.base_url, path, params)
        request_headers: MutableMapping[str, str] = {
            "Accept": "application/json",
            **dict(self.headers),
            **dict(headers or {}),
        }
        request = Request(url, headers=dict(request_headers), method=method)

        try:
            with urlopen(request, timeout=self.timeout) as response:
                payload = response.read().decode("utf-8")
                return json.loads(payload) if payload else None
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            try:
                parsed_body = json.loads(body) if body else None
            except json.JSONDecodeError:
                parsed_body = body
            raise AturioApiError(
                f"Aturio client request failed with status {error.code}.",
                status=error.code,
                url=url,
                body=parsed_body,
            ) from error
        except URLError as error:
            raise AturioApiError(
                "Aturio client could not reach the upstream API.",
                url=url,
                body=str(error.reason),
            ) from error
        except json.JSONDecodeError as error:
            raise AturioApiError(
                "API returned a non-JSON response.",
                url=url,
            ) from error

    def search(
        self,
        query: str,
        *,
        type: Optional[str] = None,
        limit: Optional[int] = None,
        headers: Optional[Mapping[str, str]] = None,
    ) -> Dict[str, Any]:
        if not isinstance(query, str) or not query.strip():
            raise TypeError("query must be a non-empty string.")

        return self.request(
            "/search",
            params={"q": query, "type": type, "limit": limit},
            headers=headers,
        )

    def list_laws(
        self,
        *,
        type: Optional[str] = None,
        year: Optional[int] = None,
        status: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
        headers: Optional[Mapping[str, str]] = None,
    ) -> Dict[str, Any]:
        return self.request(
            "/laws",
            params={
                "type": type,
                "year": year,
                "status": status,
                "limit": limit,
                "offset": offset,
            },
            headers=headers,
        )

    def get_law(
        self,
        frbr_uri: str,
        *,
        headers: Optional[Mapping[str, str]] = None,
    ) -> Dict[str, Any]:
        return self.request(
            f"/laws/{_normalize_frbr_uri(frbr_uri)}",
            headers=headers,
        )


def create_client(
    *,
    base_url: str = DEFAULT_BASE_URL,
    timeout: float = 30.0,
    headers: Optional[Mapping[str, str]] = None,
) -> AturioClient:
    return AturioClient(base_url=base_url, timeout=timeout, headers=headers or {})


def create_aturio_client(**kwargs: Any) -> AturioClient:
    return create_client(**kwargs)


def create_indonesia_client(**kwargs: Any) -> AturioClient:
    return create_client(**kwargs)


def create_pasal_client(**kwargs: Any) -> AturioClient:
    return create_client(**kwargs)


client = create_client()


def request(
    path: str,
    *,
    params: Optional[Mapping[str, Any]] = None,
    headers: Optional[Mapping[str, str]] = None,
    method: str = "GET",
) -> Any:
    return client.request(path, params=params, headers=headers, method=method)


def search(
    query: str,
    *,
    type: Optional[str] = None,
    limit: Optional[int] = None,
    headers: Optional[Mapping[str, str]] = None,
) -> Dict[str, Any]:
    return client.search(query, type=type, limit=limit, headers=headers)


def list_laws(
    *,
    type: Optional[str] = None,
    year: Optional[int] = None,
    status: Optional[str] = None,
    limit: Optional[int] = None,
    offset: Optional[int] = None,
    headers: Optional[Mapping[str, str]] = None,
) -> Dict[str, Any]:
    return client.list_laws(
        type=type,
        year=year,
        status=status,
        limit=limit,
        offset=offset,
        headers=headers,
    )


def get_law(
    frbr_uri: str,
    *,
    headers: Optional[Mapping[str, str]] = None,
) -> Dict[str, Any]:
    return client.get_law(frbr_uri, headers=headers)
