"""HTTP replay patches for requests and httpx."""

from __future__ import annotations

import base64
import json
import re
from typing import Dict

from autodebug_replay.canonicalize import body_hash, canonical_url, json_mismatch_report
from autodebug_replay.transcripts import ReplayDivergence, TranscriptIndex

# Patterns for values that commonly change between runs
_VOLATILE_PATTERNS = [
    (re.compile(r'"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"'), '"<UUID>"'),
    (re.compile(r'"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^"]*"'), '"<TIMESTAMP>"'),
    (re.compile(r'"\d{10,13}"'), '"<EPOCH>"'),
]

# Store originals for uninstall
_orig_requests_request = None
_orig_httpx_request = None
_orig_httpx_async_request = None


class GuardedHeaders(dict):
    def __init__(self, allowed: Dict[str, str]):
        normalized = {str(k).lower(): v for k, v in (allowed or {}).items()}
        super().__init__(normalized)
        self._allowed = set(normalized.keys())

    def __getitem__(self, key):
        lookup = str(key).lower()
        if lookup not in self._allowed:
            raise ReplayDivergence("UNCAPTURED_HTTP_HEADER_READ", {"header": key})
        return super().__getitem__(lookup)

    def get(self, key, default=None):
        lookup = str(key).lower()
        if lookup not in self._allowed:
            raise ReplayDivergence("UNCAPTURED_HTTP_HEADER_READ", {"header": key})
        return super().get(lookup, default)


def _decode_body(payload: str | bytes, encoding: str) -> bytes:
    if encoding == "base64":
        try:
            return base64.b64decode(payload, validate=True)
        except Exception as exc:
            raise ReplayDivergence(
                "INVALID_RECORDED_PAYLOAD",
                {"encoding": "base64", "error": str(exc)},
            ) from exc
    if isinstance(payload, bytes):
        return payload
    return payload.encode("utf-8")


def _normalize_volatile(body: bytes, content_type: str | None) -> bytes:
    """Normalize volatile values (UUIDs, timestamps) in body for fuzzy matching."""
    if not content_type or "json" not in content_type.lower():
        return body
    try:
        text = body.decode("utf-8")
        for pattern, replacement in _VOLATILE_PATTERNS:
            text = pattern.sub(replacement, text)
        return text.encode("utf-8")
    except UnicodeDecodeError:
        return body


def _hash_equivalent(expected_hash: str | None, actual_hash: str) -> bool:
    """Treat short/long hash variants as equivalent when one prefixes the other."""
    if not expected_hash:
        return False
    e = str(expected_hash).strip().lower()
    a = str(actual_hash).strip().lower()
    return e == a or a.startswith(e) or e.startswith(a)


def _expect_http(index: TranscriptIndex, method: str, url: str, body: bytes, content_type: str | None):
    record = index.expect("HTTP")
    if record.get("method") != method:
        raise ReplayDivergence("MISMATCH", {
            "expected_method": record.get("method"),
            "actual_method": method,
            "url": url,
        })
    if record.get("url_canonical") != canonical_url(url):
        raise ReplayDivergence("MISMATCH", {
            "expected_url": record.get("url_canonical"),
            "actual_url": canonical_url(url),
        })
    expected_hash = record.get("body_hash")
    actual_hash = body_hash(body, content_type)
    if not _hash_equivalent(expected_hash, actual_hash):
        # Try fuzzy matching — normalize volatile values before comparing
        norm_actual = _normalize_volatile(body, content_type)
        norm_actual_hash = body_hash(norm_actual, content_type)
        # Check if the recorded body was also stored (for normalization)
        recorded_body_bytes = record.get("request_body_bytes")
        if recorded_body_bytes:
            recorded_body = _decode_body(recorded_body_bytes, record.get("request_body_encoding", "utf-8"))
            norm_expected = _normalize_volatile(recorded_body, content_type)
            norm_expected_hash = body_hash(norm_expected, content_type)
            if _hash_equivalent(norm_expected_hash, norm_actual_hash):
                return record  # Fuzzy match succeeded
            # Semantic matching: ignore volatile keys and compare canonical JSON shape/content.
            semantic_expected_hash = body_hash(recorded_body, content_type, normalize_volatile=True)
            semantic_actual_hash = body_hash(body, content_type, normalize_volatile=True)
            if _hash_equivalent(semantic_expected_hash, semantic_actual_hash):
                return record
            mismatch_report = json_mismatch_report(recorded_body, body, content_type, ignore_volatile=True)
        else:
            mismatch_report = {
                "mismatch_count": 0,
                "field_mismatches": [],
                "volatile_only": False,
                "is_json": False,
            }
        # Provide helpful error with body preview
        body_preview = body[:200].decode("utf-8", errors="replace") if body else ""
        expected_preview = ""
        if recorded_body_bytes:
            expected_preview = recorded_body[:200].decode("utf-8", errors="replace")
        raise ReplayDivergence("PAYLOAD_MISMATCH", {
            "expected_body_hash": expected_hash,
            "actual_body_hash": actual_hash,
            "url": url,
            "method": method,
            "field_mismatches": mismatch_report.get("field_mismatches", [])[:20],
            "mismatch_count": mismatch_report.get("mismatch_count", 0),
            "volatile_only_mismatch": mismatch_report.get("volatile_only", False),
            "expected_body_preview": expected_preview,
            "actual_body_preview": body_preview,
        })
    return record


def install_requests(index: TranscriptIndex) -> None:
    global _orig_requests_request
    try:
        import requests
    except ImportError:
        return

    _orig_requests_request = requests.Session.request

    def wrapped(self, method, url, **kwargs):
        body = kwargs.get("data")
        if body is None and "json" in kwargs:
            body = json.dumps(kwargs["json"]).encode("utf-8")
        if body is None:
            req_body = b""
        elif isinstance(body, bytes):
            req_body = body
        else:
            req_body = str(body).encode("utf-8")

        headers = kwargs.get("headers") or {}
        content_type = None
        if isinstance(headers, dict):
            content_type = headers.get("Content-Type") or headers.get("content-type")

        record = _expect_http(index, method, url, req_body, content_type)
        resp = requests.Response()
        resp.status_code = record.get("status")
        resp._content = _decode_body(record.get("response_body_bytes"), record.get("response_body_encoding"))
        resp.headers = GuardedHeaders(record.get("headers_canon", {}))
        resp.url = url
        return resp

    requests.Session.request = wrapped  # type: ignore[assignment]


def install_httpx(index: TranscriptIndex) -> None:
    global _orig_httpx_request, _orig_httpx_async_request
    try:
        import httpx
    except ImportError:
        return

    _orig_httpx_request = httpx.Client.request
    _orig_httpx_async_request = httpx.AsyncClient.request

    def wrapped(self, method, url, **kwargs):
        content = kwargs.get("content")
        if content is None and "json" in kwargs:
            content = json.dumps(kwargs["json"]).encode("utf-8")
        if content is None:
            req_body = b""
        elif isinstance(content, bytes):
            req_body = content
        else:
            req_body = str(content).encode("utf-8")

        headers = kwargs.get("headers") or {}
        content_type = None
        if isinstance(headers, dict):
            content_type = headers.get("Content-Type") or headers.get("content-type")

        record = _expect_http(index, method, str(url), req_body, content_type)
        content_bytes = _decode_body(record.get("response_body_bytes"), record.get("response_body_encoding"))
        return httpx.Response(
            status_code=record.get("status"),
            headers=GuardedHeaders(record.get("headers_canon", {})),
            content=content_bytes,
            request=httpx.Request(method, url),
        )

    httpx.Client.request = wrapped  # type: ignore[assignment]

    async def wrapped_async(self, method, url, **kwargs):
        content = kwargs.get("content")
        if content is None and "json" in kwargs:
            content = json.dumps(kwargs["json"]).encode("utf-8")
        if content is None:
            req_body = b""
        elif isinstance(content, bytes):
            req_body = content
        else:
            req_body = str(content).encode("utf-8")

        headers = kwargs.get("headers") or {}
        content_type = None
        if isinstance(headers, dict):
            content_type = headers.get("Content-Type") or headers.get("content-type")

        record = _expect_http(index, method, str(url), req_body, content_type)
        content_bytes = _decode_body(record.get("response_body_bytes"), record.get("response_body_encoding"))
        return httpx.Response(
            status_code=record.get("status"),
            headers=GuardedHeaders(record.get("headers_canon", {})),
            content=content_bytes,
            request=httpx.Request(method, url),
        )

    httpx.AsyncClient.request = wrapped_async  # type: ignore[assignment]


def uninstall() -> None:
    """Restore original HTTP functions."""
    global _orig_requests_request, _orig_httpx_request, _orig_httpx_async_request

    if _orig_requests_request is not None:
        try:
            import requests
            requests.Session.request = _orig_requests_request
        except ImportError:
            pass
        _orig_requests_request = None

    if _orig_httpx_request is not None:
        try:
            import httpx
            httpx.Client.request = _orig_httpx_request
        except ImportError:
            pass
        _orig_httpx_request = None
    if _orig_httpx_async_request is not None:
        try:
            import httpx
            httpx.AsyncClient.request = _orig_httpx_async_request
        except ImportError:
            pass
        _orig_httpx_async_request = None
