from __future__ import annotations

import json
import socket
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen


class HttpJsonError(RuntimeError):
    pass


class UrllibJsonTransport:
    def __init__(self, connect_timeout: float | None = None) -> None:
        # When set, probe TCP connectivity with this (short) timeout before the
        # request so a down host fails fast instead of stalling the caller for
        # the full read `timeout`. The read still gets the full `timeout`, so a
        # live-but-slow service (e.g. an LLM generating a response) is unaffected.
        self.connect_timeout = connect_timeout

    def request_json(
        self,
        method: str,
        url: str,
        headers: dict[str, str] | None = None,
        payload: dict[str, Any] | None = None,
        timeout: int = 10,
    ) -> dict[str, Any]:
        if self.connect_timeout is not None:
            self._probe_connect(url)
        data = None if payload is None else json.dumps(payload).encode("utf-8")
        request = Request(url, data=data, method=method, headers=headers or {})
        if payload is not None:
            request.add_header("Content-Type", "application/json")
        try:
            with urlopen(request, timeout=timeout) as response:
                raw = response.read().decode("utf-8")
        except HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            raise HttpJsonError(f"HTTP {exc.code}: {detail}") from exc
        except URLError as exc:
            raise HttpJsonError(str(exc.reason)) from exc

        if not raw:
            return {}
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise HttpJsonError(f"invalid JSON response: {exc}") from exc

    def _probe_connect(self, url: str) -> None:
        parts = urlsplit(url)
        host = parts.hostname
        if not host:
            return
        port = parts.port or (443 if parts.scheme == "https" else 80)
        try:
            with socket.create_connection((host, port), timeout=self.connect_timeout):
                pass
        except OSError as exc:
            raise HttpJsonError(f"connection to {host}:{port} failed: {exc}") from exc
