"""HTTP patching for requests and httpx."""

from __future__ import annotations

import base64
import json
import os
from typing import Any, Dict

from autodebug_probe.canonicalize import body_hash, canonical_url, decode_gzip, normalize_headers
from autodebug_probe.exception_patch import format_stack
from autodebug_probe.recorder import get_recorder

MAX_CAPTURED_HTTP_BODY_BYTES = max(
    1024,
    int(os.environ.get("AUTODEBUG_HTTP_CAPTURE_MAX_BODY_BYTES", "1048576")),
)


def _encode_body(body: bytes) -> tuple[str | bytes, str]:
    try:
        decoded_text = body.decode("utf-8")
        return decoded_text, "utf-8"
    except UnicodeDecodeError:
        return base64.b64encode(body).decode("ascii"), "base64"


def _record_response(method: str, url: str, req_body: bytes, content_type: str | None, response) -> None:
    """Record an HTTP response (internal helper for sync clients)."""
    _record_request_response(method, url, req_body, content_type, response, is_async=False)


def _is_streaming_response(response) -> bool:
    """Detect if a response uses streaming (chunked transfer-encoding).

    Checks for indicators of streaming responses:
    - transfer-encoding: chunked header
    - content-length absent with transfer-encoding present
    - Response accessed via iter_content, iter_bytes, or iter_lines

    This is used to annotate recorded responses so the replay engine
    can faithfully reproduce stream vs non-stream behavior.
    """
    headers = {k.lower(): v for k, v in response.headers.items()}
    te = headers.get("transfer-encoding", "")
    cl = headers.get("content-length")
    is_chunked = "chunked" in te.lower()
    is_stream = is_chunked or (te and cl is None)
    return is_stream


def _record_request_response(
    method: str,
    url: str,
    req_body: Any,
    content_type: str | None,
    response,
    is_async: bool = False,
) -> None:
    """Record an HTTP request/response pair.

    This is the main recording function used by both sync and async HTTP clients.
    Handles both regular and streaming responses (chunked transfer-encoding,
    iter_content, iter_bytes, iter_lines).

    Args:
        method: HTTP method (GET, POST, etc.)
        url: Request URL
        req_body: Request body (bytes, str, dict, or None)
        content_type: Content-Type header value
        response: Response object (requests.Response or httpx.Response)
        is_async: Whether this is from an async client
    """
    # Normalize request body to bytes
    if req_body is None:
        req_body_bytes = b""
    elif isinstance(req_body, bytes):
        req_body_bytes = req_body
    elif isinstance(req_body, dict):
        req_body_bytes = json.dumps(req_body).encode("utf-8")
    else:
        req_body_bytes = str(req_body).encode("utf-8")

    response_body = response.content or b""
    was_truncated = False
    original_response_size = len(response_body)
    if original_response_size > MAX_CAPTURED_HTTP_BODY_BYTES:
        response_body = response_body[:MAX_CAPTURED_HTTP_BODY_BYTES]
        was_truncated = True
    decoded_body, was_gzip = decode_gzip(response_body)
    response_body_payload, response_body_encoding = _encode_body(decoded_body)

    headers = list(response.headers.items())
    headers_canon, headers_raw, headers_ignored = normalize_headers(headers)

    payload: Dict[str, Any] = {
        "method": method,
        "url_canonical": canonical_url(url),
        "body_hash": body_hash(req_body_bytes, content_type),
        "status": response.status_code,
        "response_body_bytes": response_body_payload,
        "response_body_encoding": response_body_encoding,
        "response_body_gzip_decoded": was_gzip,
        "response_body_hash": body_hash(decoded_body, headers_raw.get("content-type")),
        "headers_canon": headers_canon,
        "headers_raw": headers_raw,
        "headers_ignored": headers_ignored,
    }

    # Detect and annotate streaming responses (chunked transfer-encoding)
    if _is_streaming_response(response):
        payload["is_streaming"] = True

    # Add async flag if async
    if is_async:
        payload["is_async"] = True
    if was_truncated:
        payload["response_body_truncated"] = True
        payload["response_body_original_size"] = original_response_size
        payload["response_body_captured_size"] = len(response_body)

    get_recorder().record_http(payload)


def install_requests() -> None:
    try:
        import requests
    except ImportError:
        return

    original = 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")

        content_type = None
        headers = kwargs.get("headers") or {}
        if isinstance(headers, dict):
            content_type = headers.get("Content-Type") or headers.get("content-type")
        try:
            response = original(self, method, url, **kwargs)
        except Exception as exc:  # pylint: disable=broad-except
            get_recorder().record_event(
                {
                    "type": "EXCEPTION",
                    "exc_type": exc.__class__.__name__,
                    "message": str(exc),
                    "stack": format_stack(exc.__traceback__),
                    "boundary": "HTTP",
                }
            )
            raise
        _record_response(method, url, req_body, content_type, response)
        return response

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


def install_httpx() -> None:
    try:
        import httpx
    except ImportError:
        return

    original = httpx.Client.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")
        try:
            response = original(self, method, url, **kwargs)
        except Exception as exc:  # pylint: disable=broad-except
            get_recorder().record_event(
                {
                    "type": "EXCEPTION",
                    "exc_type": exc.__class__.__name__,
                    "message": str(exc),
                    "stack": format_stack(exc.__traceback__),
                    "boundary": "HTTP",
                }
            )
            raise
        _record_response(method, str(url), req_body, content_type, response)
        return response

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

    # Also patch AsyncClient if available
    if hasattr(httpx, "AsyncClient"):
        original_async = httpx.AsyncClient.request

        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")
            try:
                response = await original_async(self, method, url, **kwargs)
            except Exception as exc:
                get_recorder().record_event(
                    {
                        "type": "EXCEPTION",
                        "exc_type": exc.__class__.__name__,
                        "message": str(exc),
                        "stack": format_stack(exc.__traceback__),
                        "boundary": "HTTP",
                    }
                )
                raise
            _record_request_response(method, str(url), req_body, content_type, response, is_async=True)
            return response

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