from __future__ import annotations

import json
import urllib.error
import urllib.request
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from typing import Any


def request(
    method: str,
    url: str,
    headers: dict[str, Any] | None = None,
    query: dict[str, Any] | None = None,
    body: dict[str, Any] | list[Any] | str | None = None,
    json_body: Any = None,
    *,
    runtime: Any,
) -> dict[str, Any]:
    request_url = _build_url(url, query)
    payload, content_type = _encode_body(body, json_body)
    normalized_headers = {key: str(value) for key, value in (headers or {}).items()}
    if content_type is not None and not _has_header(normalized_headers, "Content-Type"):
        normalized_headers["Content-Type"] = content_type

    req = urllib.request.Request(
        url=request_url,
        method=method.upper(),
        headers=normalized_headers,
        data=payload,
    )

    try:
        with urllib.request.urlopen(req, timeout=runtime.timeout) as response:
            return _build_response(response, method=req.get_method(), default_url=request_url)
    except urllib.error.HTTPError as error:
        return _build_response(error, method=req.get_method(), default_url=request_url)


def _encode_body(body: dict[str, Any] | list[Any] | str | None, json_body: Any) -> tuple[bytes | None, str | None]:
    if body is not None and json_body is not None:
        raise ValueError("net.request accepts either body or json_body, not both")
    if json_body is not None:
        return json.dumps(json_body).encode("utf-8"), "application/json"
    if body is None:
        return None, None
    if isinstance(body, str):
        return body.encode("utf-8"), None
    return json.dumps(body).encode("utf-8"), "application/json"


def _build_url(url: str, query: dict[str, Any] | None) -> str:
    if not query:
        return url

    split = urlsplit(url)
    existing = parse_qsl(split.query, keep_blank_values=True)
    merged: list[tuple[str, Any]] = list(existing)
    for key, value in query.items():
        if isinstance(value, list):
            merged.extend((str(key), item) for item in value)
        else:
            merged.append((str(key), value))
    query_string = urlencode(merged, doseq=True)
    return urlunsplit((split.scheme, split.netloc, split.path, query_string, split.fragment))


def _build_response(response: Any, *, method: str, default_url: str) -> dict[str, Any]:
    body_text = response.read().decode("utf-8", errors="replace")
    headers = dict(response.headers.items())
    content_type = headers.get("Content-Type")
    return {
        "status": response.getcode(),
        "ok": 200 <= response.getcode() < 300,
        "method": method,
        "url": response.geturl() if hasattr(response, "geturl") else default_url,
        "content_type": content_type,
        "headers": headers,
        "body": body_text,
        "json": _maybe_parse_json(body_text, content_type),
    }


def _maybe_parse_json(body_text: str, content_type: str | None) -> Any:
    stripped = body_text.strip()
    if not stripped:
        return None
    is_json = False
    if content_type is not None and "json" in content_type.lower():
        is_json = True
    elif stripped.startswith("{") or stripped.startswith("["):
        is_json = True
    if not is_json:
        return None
    try:
        return json.loads(stripped)
    except json.JSONDecodeError:
        return None


def _has_header(headers: dict[str, str], name: str) -> bool:
    target = name.lower()
    return any(key.lower() == target for key in headers)
