"""``Response`` — typed wrapper around an HTTP response.

See: ``DESIGN.md`` §14 (``response.py`` in the project layout) and
§3.2 (checks read both probe + response).

The wrapper exposes only the fields checks need to read. Streaming
bodies + binary asset handling land in v0.3 alongside the cassette
subsystem; v0.1 assumes the body has been read
into memory.

The wrapper is **frozen** (rule DP-03 — frozen domain types). A check
mutating a Response would produce non-deterministic behavior across
the rest of the check chain.
"""

from __future__ import annotations

import json as _json
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Mapping


@dataclass(frozen=True, slots=True)
class Response:
    """One HTTP response, captured for inspection by checks.

    Construct from real httpx responses via :meth:`from_httpx` (lands
    when transport ships in Phase 1.5 of this implementation), or
    directly from primitives in tests.

    ``headers`` is wrapped in :class:`MappingProxyType` to enforce
    the frozen invariant — mutations raise ``TypeError``. The keys
    are case-INSENSITIVE on lookup via :meth:`header`; raw access via
    ``.headers[...]`` is case-sensitive (matches httpx's typical
    shape — providers control the case they return).
    """

    url: str
    """Final URL after redirects (the URL the body actually came from)."""

    status: int
    """HTTP status code."""

    headers: Mapping[str, str]
    """Response headers. Wrapped in MappingProxyType for immutability."""

    body: bytes
    """Raw response body. Decoded if Content-Encoding required it."""

    elapsed_ms: float
    """Wall-clock duration of the request, milliseconds."""

    request_id: str | None = None
    """Provider's X-Request-Id (or similar), if echoed. Looked up case-insensitively."""

    extras: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
    """Free-form bag for transport-specific signal (TLS version, redirect chain, ...).

    Reserved for v0.2+; checks should not read this field in v0.1.
    """

    def header(self, name: str, default: str | None = None) -> str | None:
        """Case-insensitive header lookup.

        Returns ``default`` (default ``None``) if the header is absent.
        For the multi-value case (``Set-Cookie``) callers should
        iterate ``self.headers.items()`` directly — multi-value
        retention is a transport concern.
        """
        target = name.lower()
        for key, value in self.headers.items():
            if key.lower() == target:
                return value
        return default

    def json(self) -> Any:
        """Decode the body as UTF-8 JSON.

        Raises ``ValueError`` (from ``json.loads``) on malformed JSON
        or non-UTF-8 bytes. Callers that want to express "the body
        SHOULD parse as JSON" emit a Finding instead of catching the
        exception — see ``checks.contract`` (lands later in Phase 1).

        Not cached; call once and reuse the result.
        """
        return _json.loads(self.body.decode("utf-8"))

    @property
    def is_success(self) -> bool:
        """True iff status is in [200, 300)."""
        return 200 <= self.status < 300

    @property
    def is_client_error(self) -> bool:
        """True iff status is in [400, 500)."""
        return 400 <= self.status < 500

    @property
    def is_server_error(self) -> bool:
        """True iff status is in [500, 600)."""
        return 500 <= self.status < 600


__all__ = ["Response"]
