"""HTTP debug interceptor for the Acceldata AIO tracer.

When debug mode is enabled (``configure(debug=True)`` /
``AcceldataTracer(debug=True)`` / ``ACCELDATA_AIO_DEBUG``), this patches
``httpx`` so every Opik-bound HTTP call is logged as a concise one-line
summary (method, path, status, elapsed, body metadata) to the
``acceldata_aio_tracer.http_trace`` logger. On 4xx/5xx it dumps the request
body and logs a replay-ready ``curl`` (with credential headers redacted).

Kept separate from ``client.py`` so the client module stays focused on the
client surface; this module owns all debug-only machinery.
"""

import gzip
import json
import logging
import shlex
import time
from typing import Any, Optional
from urllib.parse import urlparse


_DEBUG_REQ_DUMP_PATH = "/tmp/acceldata-aio-tracer-last-failed-request.bin"
_debug_installed = False

# Header names whose values are redacted in failure replays so credentials
# never land in logs.
_SENSITIVE_HEADERS = ("accesskey", "secretkey", "authorization")


def _human_size(num_bytes: int) -> str:
    """Compact human-readable byte size, e.g. 1123 -> '1.1 KB'."""
    size = float(num_bytes)
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024 or unit == "GB":
            return f"{int(size)} {unit}" if unit == "B" else f"{size:.1f} {unit}"
        size /= 1024
    return f"{int(num_bytes)} B"


def _decode_json_body(content: bytes, content_encoding: str) -> Optional[Any]:
    """Best-effort decode of a (possibly gzipped) JSON body. None on any failure."""
    try:
        raw = content
        if "gzip" in content_encoding:
            raw = gzip.decompress(raw)
        return json.loads(raw)
    except Exception:
        return None


def _summarize_request_body(headers: dict, content: Optional[bytes]) -> str:
    """One-line metadata summary of a request body: type, encoding, size, and a
    count of list-valued top-level JSON keys (e.g. '{spans=5}') when decodable.

    Never prints raw (possibly gzipped/binary) body bytes.
    """
    if not content:
        return "no body"
    length = len(content)
    content_type = headers.get("content-type", "").split(";")[0] or "raw"
    short_type = "json" if "json" in content_type else content_type
    content_encoding = headers.get("content-encoding", "")
    desc = short_type + (f"+{content_encoding}" if content_encoding else "")
    desc += f", {_human_size(length)}"
    if "json" in content_type:
        data = _decode_json_body(content, content_encoding)
        if isinstance(data, dict):
            counts = [f"{k}={len(v)}" for k, v in data.items() if isinstance(v, list)]
            if counts:
                desc += "  {" + ", ".join(counts) + "}"
    return desc


def _short_response_body(resp_body: bytes, limit: int = 200) -> str:
    """Short printable response body (decoded text, truncated). Empty -> ''."""
    if not resp_body:
        return ""
    try:
        text = resp_body.decode("utf-8", errors="replace").strip()
    except Exception:
        return f"<{len(resp_body)} bytes>"
    if not text:
        return ""
    return text if len(text) <= limit else text[:limit] + "…"


def _install_http_debug_interceptor(opik_url: str) -> None:
    """Patch httpx.Client.send and httpx.AsyncClient.send to log a concise
    one-line summary of every Opik HTTP call: method, path, status, elapsed
    time, and request-body metadata (type/encoding/size + decoded item counts).

    On responses with status >= 400, dumps the request body to
    `_DEBUG_REQ_DUMP_PATH` and logs a copy-paste curl that replays the request
    (credential headers redacted). Idempotent — safe to call multiple times.
    """
    global _debug_installed
    if _debug_installed:
        return

    import httpx

    logger = logging.getLogger("acceldata_aio_tracer.http_trace")
    logger.setLevel(logging.DEBUG)
    if not logger.handlers:
        h = logging.StreamHandler()
        h.setFormatter(logging.Formatter(
            "%(asctime)s %(levelname)-5s acceldata_aio_tracer.http_trace: %(message)s"
        ))
        logger.addHandler(h)
        logger.propagate = False

    target_host = urlparse(opik_url).netloc

    def is_opik_url(url) -> bool:
        try:
            return urlparse(str(url)).netloc == target_host
        except Exception:
            return False

    def path_of(url) -> str:
        parsed = urlparse(str(url))
        return parsed.path + (f"?{parsed.query}" if parsed.query else "")

    def safe_content(request) -> Optional[bytes]:
        try:
            return request.content
        except Exception:
            return None

    def dump_request_body(request) -> str:
        content = safe_content(request)
        if not content:
            return ""
        try:
            with open(_DEBUG_REQ_DUMP_PATH, "wb") as f:
                f.write(content)
            return _DEBUG_REQ_DUMP_PATH
        except Exception as write_err:
            logger.warning(f"failed to dump request body: {write_err}")
            return ""

    def build_replay_curl(method, url, headers, body_path) -> str:
        parts = ["curl -v --insecure -X", method, shlex.quote(str(url))]
        for k, v in headers.items():
            if k.lower() in ("host", "content-length"):
                continue
            shown = "<redacted>" if k.lower() in _SENSITIVE_HEADERS else v
            parts += ["-H", shlex.quote(f"{k}: {shown}")]
        if body_path:
            parts += ["--data-binary", f"@{body_path}"]
        return " \\\n  ".join(parts)

    def log_failure(request, response, resp_body, elapsed, marker="") -> None:
        body_path = dump_request_body(request)
        decoded = _short_response_body(resp_body, limit=8000)
        curl = build_replay_curl(
            request.method, str(request.url), dict(request.headers), body_path
        )
        logger.error(
            f"<<< {marker}{request.method} {path_of(request.url)}  "
            f"[{response.status_code}, {elapsed:.2f}s]\n"
            f"    error={decoded}\n"
            f"    request_body_dumped_to={body_path or '<none>'}\n"
            f"    replay (paste into shell):\n{curl}"
        )

    def log_success(request, response, resp_body, elapsed, marker="") -> None:
        line = (
            f"<<< {marker}{request.method} {path_of(request.url)}  "
            f"[{response.status_code}, {elapsed:.2f}s]"
        )
        short = _short_response_body(resp_body)
        if short:
            line += f"  {short}"
        logger.debug(line)

    def log_request(request, marker="") -> None:
        summary = _summarize_request_body(dict(request.headers), safe_content(request))
        logger.debug(f">>> {marker}{request.method} {path_of(request.url)}  [{summary}]")

    orig_send = httpx.Client.send

    def patched_send(self, request, *args, **kwargs):
        log_this = is_opik_url(request.url)
        if log_this:
            log_request(request)
        start = time.perf_counter()
        response = orig_send(self, request, *args, **kwargs)
        if log_this:
            elapsed = time.perf_counter() - start
            try:
                resp_body = response.read()
            except Exception as read_err:
                resp_body = f"<read failed: {read_err}>".encode()
            if response.status_code >= 400:
                log_failure(request, response, resp_body, elapsed)
            else:
                log_success(request, response, resp_body, elapsed)
        return response

    httpx.Client.send = patched_send

    orig_async_send = httpx.AsyncClient.send

    async def patched_async_send(self, request, *args, **kwargs):
        log_this = is_opik_url(request.url)
        if log_this:
            log_request(request, marker="[async] ")
        start = time.perf_counter()
        response = await orig_async_send(self, request, *args, **kwargs)
        if log_this:
            elapsed = time.perf_counter() - start
            try:
                resp_body = await response.aread()
            except Exception as read_err:
                resp_body = f"<read failed: {read_err}>".encode()
            if response.status_code >= 400:
                log_failure(request, response, resp_body, elapsed, marker="[async] ")
            else:
                log_success(request, response, resp_body, elapsed, marker="[async] ")
        return response

    httpx.AsyncClient.send = patched_async_send

    _debug_installed = True
    logger.info(f"http debug interceptor installed for host={target_host}")
