from __future__ import annotations

import json
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any, Callable, Mapping

from .errors import create_http_error


@dataclass(frozen=True)
class HttpClientOptions:
    base_url: str
    api_key: str
    timeout_s: float
    user_agent: str | None
    get_retries: int
    transport: Callable[[urllib.request.Request, float], tuple[int, Mapping[str, str], str]] | None = None


def _normalize_base_url(base_url: str) -> str:
    return base_url.rstrip("/")


def _join_url(base_url: str, path: str) -> str:
    base = _normalize_base_url(base_url)
    if not path.startswith("/"):
        return f"{base}/{path}"
    return f"{base}{path}"


def _build_url(
    base_url: str,
    path: str,
    query: Mapping[str, str | int | float | bool | None] | None,
) -> str:
    url = _join_url(base_url, path)
    if not query:
        return url
    parts = list(urllib.parse.urlsplit(url))
    current = urllib.parse.parse_qsl(parts[3], keep_blank_values=True)
    for k, v in query.items():
        if v is None:
            continue
        current.append((k, str(v)))
    parts[3] = urllib.parse.urlencode(current)
    return urllib.parse.urlunsplit(parts)


def _parse_json_if_possible(text: str) -> Any:
    trimmed = text.strip()
    if not trimmed:
        return None
    try:
        return json.loads(trimmed)
    except Exception:
        return None


def _default_transport(req: urllib.request.Request, timeout_s: float) -> tuple[int, Mapping[str, str], str]:
    with urllib.request.urlopen(req, timeout=timeout_s) as res:
        status = int(getattr(res, "status", 200))
        headers = {k.lower(): v for k, v in dict(res.headers).items()}
        raw = res.read().decode("utf-8", errors="replace")
        return status, headers, raw


def _should_retry(method: str, status: int | None) -> bool:
    if method != "GET":
        return False
    if status is None:
        return True
    return status >= 500


class HttpClient:
    def __init__(self, options: HttpClientOptions):
        self._base_url = _normalize_base_url(options.base_url)
        self._api_key = options.api_key
        self._timeout_s = options.timeout_s
        self._user_agent = options.user_agent
        self._get_retries = options.get_retries
        self._transport = options.transport or _default_transport

    def request_json(
        self,
        *,
        method: str,
        path: str,
        query: Mapping[str, str | int | float | bool | None] | None = None,
        body: Any | None = None,
    ) -> Any:
        url = _build_url(self._base_url, path, query)

        headers: dict[str, str] = {
            "x-api-key": self._api_key,
            "accept": "application/json",
        }
        if self._user_agent:
            headers["user-agent"] = self._user_agent

        data: bytes | None = None
        if body is not None:
            headers["content-type"] = "application/json"
            data = json.dumps(body).encode("utf-8")

        req = urllib.request.Request(url=url, data=data, method=method, headers=headers)

        attempt = 0
        while True:
            status: int | None = None
            response_headers: Mapping[str, str] = {}
            raw: str = ""
            try:
                status, response_headers, raw = self._transport(req, self._timeout_s)
            except urllib.error.HTTPError as e:
                status = int(getattr(e, "code", 0) or 0) or None
                response_headers = {k.lower(): v for k, v in dict(getattr(e, "headers", {}) or {}).items()}
                raw = (e.read() or b"").decode("utf-8", errors="replace")
            except urllib.error.URLError as e:
                if attempt >= self._get_retries or not _should_retry(method, None):
                    raise e
                backoff_ms = 250 * (2**attempt)
                time.sleep(backoff_ms / 1000.0)
                attempt += 1
                continue

            parsed = _parse_json_if_possible(raw)

            if status is None or not (200 <= status < 300):
                message = None
                if isinstance(parsed, dict) and isinstance(parsed.get("error"), str):
                    message = str(parsed["error"])
                if not message:
                    message = f"HTTP {status}" if status is not None else "HTTP error"
                err = create_http_error(
                    message=message,
                    status=int(status or 0),
                    method=method,
                    url=url,
                    headers=dict(response_headers),
                    body=parsed,
                    raw_body=raw or None,
                )
                if attempt < self._get_retries and _should_retry(method, err.status):
                    backoff_ms = 250 * (2**attempt)
                    time.sleep(backoff_ms / 1000.0)
                    attempt += 1
                    continue
                raise err

            return parsed

