import logging
import os
import shlex
from typing import Any
from urllib.parse import urlparse

from opik import Opik

from .auth import setup_acceldata_auth_hook


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


def _install_http_debug_interceptor(opik_url: str) -> None:
    """Patch httpx.Client.send and httpx.AsyncClient.send to log every Opik
    HTTP call. On responses with status >= 400, dumps the request body to
    `_DEBUG_REQ_DUMP_PATH` and prints a copy-paste curl that replays the
    request. 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 fmt_body(body) -> str:
        if body is None:
            return "<none>"
        if isinstance(body, (bytes, bytearray)):
            try:
                return body.decode("utf-8", errors="replace")[:4000]
            except Exception:
                return f"<{len(body)} bytes>"
        return str(body)[:4000]

    def decode_resp(resp_body) -> str:
        if isinstance(resp_body, (bytes, bytearray)):
            return resp_body.decode("utf-8", errors="replace")
        return str(resp_body)

    def build_replay_curl(method: str, url: str, headers: dict, 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
            parts += ["-H", shlex.quote(f"{k}: {v}")]
        if body_path:
            parts += ["--data-binary", f"@{body_path}"]
        return " \\\n  ".join(parts)

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

    def log_failure(request, response, resp_body, async_marker: str = "") -> None:
        body_path = dump_request_body(request)
        decoded = decode_resp(resp_body)[:8000]
        curl = build_replay_curl(
            request.method, str(request.url), dict(request.headers), body_path
        )
        logger.error(
            f"<<< {async_marker}{response.status_code} {request.method} {request.url}\n"
            f"    error_body={decoded}\n"
            f"    request_body_dumped_to={body_path or '<none>'}\n"
            f"    REPLAY (paste into shell):\n{curl}"
        )

    orig_send = httpx.Client.send

    def patched_send(self, request, *args, **kwargs):
        log_this = is_opik_url(request.url)
        if log_this:
            logger.debug(
                f">>> {request.method} {request.url}\n"
                f"    headers={dict(request.headers)}\n"
                f"    body={fmt_body(request.content)}"
            )
        response = orig_send(self, request, *args, **kwargs)
        if log_this:
            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)
            else:
                logger.debug(
                    f"<<< {response.status_code} {request.method} {request.url}\n"
                    f"    body={fmt_body(resp_body)}"
                )
        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:
            logger.debug(
                f">>> [async] {request.method} {request.url}\n"
                f"    headers={dict(request.headers)}\n"
                f"    body={fmt_body(request.content)}"
            )
        response = await orig_async_send(self, request, *args, **kwargs)
        if log_this:
            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, async_marker="[async] ")
            else:
                logger.debug(
                    f"<<< [async] {response.status_code} {request.method} {request.url}\n"
                    f"    body={fmt_body(resp_body)}"
                )
        return response

    httpx.AsyncClient.send = patched_async_send

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


def AcceldataTracer(
    url: str,
    access_key: str,
    secret_key: str,
    *,
    debug: bool = False,
    check_tls_certificate: bool = True,
    **kwargs: Any,
) -> Opik:
    setup_acceldata_auth_hook(access_key, secret_key)
    os.environ["OPIK_CHECK_TLS_CERTIFICATE"] = "true" if check_tls_certificate else "false"
    if debug:
        _install_http_debug_interceptor(url)
    return Opik(host=url, **kwargs)
