import contextvars
import hashlib
import json
import os
import time
import uuid
from typing import Any, Callable, Dict, Optional, Tuple

_INSTALLED_REQUESTS = False
_INSTALLED_HTTPX = False
_INFRA_CALL = contextvars.ContextVar("kyb_infra_call", default=False)


def _env(name: str, default: str = "") -> str:
    value = os.getenv(name)
    return value if value is not None else default


def _truthy(value: str, default: bool = True) -> bool:
    if value is None or str(value).strip() == "":
        return default
    return str(value).strip().lower() in {"1", "true", "yes", "on"}


def _enforce_egress() -> None:
    if not _truthy(_env("KYB_ENFORCE_EGRESS", "0"), default=False):
        return
    if _env("HTTP_PROXY") or _env("HTTPS_PROXY"):
        return
    raise RuntimeError("KYB_ENFORCE_EGRESS=1 requires HTTP_PROXY/HTTPS_PROXY")


def _canonical_json_bytes(value: Any) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")


def _sha256_hex(raw: bytes) -> str:
    return hashlib.sha256(raw).hexdigest()


def _parse_url(url: str) -> Tuple[str, str]:
    try:
        from urllib.parse import urlparse

        parsed = urlparse(url)
        host = parsed.hostname or ""
        path = parsed.path or "/"
        if parsed.query:
            path = path + "?" + parsed.query
        return host, path
    except Exception:
        return "", ""


def _is_side_effect_http(method: str) -> bool:
    m = (method or "").upper()
    return m not in {"GET", "HEAD", "OPTIONS"}


def _derive_http_signature(method: str, url: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
    host, path = _parse_url(url)
    body = kwargs.get("data")
    if isinstance(body, (bytes, bytearray)):
        body_value: Optional[str] = body.decode("utf-8", "ignore")
    elif isinstance(body, str):
        body_value = body
    else:
        body_value = None
    return {
        "method": (method or "").upper(),
        "host": host,
        "path": path,
        "json": kwargs.get("json"),
        "data": body_value,
    }


def _derive_http_idempotency_key(method: str, url: str, kwargs: Dict[str, Any]) -> Tuple[str, str]:
    sig = _derive_http_signature(method, url, kwargs)
    digest = _sha256_hex(_canonical_json_bytes(sig))
    return "http:" + digest, "sha256:" + digest


def _parse_cost_header(resp: Any) -> int:
    try:
        headers = getattr(resp, "headers", None) or {}
        value = headers.get("X-Kyb-Cost-Micros") or headers.get("x-kyb-cost-micros") or headers.get("X-Cost-Micros")
        if value is None:
            return 0
        return max(0, int(str(value).strip()))
    except Exception:
        return 0


def _retry_sleep(base_ms: int, max_ms: int, attempt: int, jitter: bool) -> None:
    delay = base_ms * (2 ** max(0, attempt - 1))
    if max_ms > 0:
        delay = min(delay, max_ms)
    if delay <= 0:
        return
    if jitter:
        import random

        delay = int(delay * (0.5 + random.random()))
    time.sleep(delay / 1000.0)


def _safe_call(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
    try:
        fn(*args, **kwargs)
    except Exception:
        return


def _is_infra_target(url: str, infra_base: str) -> bool:
    if not infra_base:
        return False
    request_host, _ = _parse_url(url)
    base_host, _ = _parse_url(infra_base)
    if not request_host or not base_host:
        return False
    return request_host.lower() == base_host.lower()


def _apply_proxy_headers(kwargs: Dict[str, Any], proxy_headers: Dict[str, str]) -> None:
    if not proxy_headers:
        return
    headers = kwargs.get("headers") or {}
    merged = dict(headers)
    for key, value in proxy_headers.items():
        if key and value is not None:
            merged[key] = str(value)
    kwargs["headers"] = merged


class InfraClient:
    def __init__(self) -> None:
        self.base = _env("KYB_INFRA_URL", "").rstrip("/")
        self.api_key = _env("KYB_API_KEY", "")
        self.tenant_id = _env("KYB_TENANT_ID", "")
        self.run_id = _env("KYB_RUN_ID", "")
        self.project_id = _env("KYB_PROJECT_ID", "")
        self.environment = _env("KYB_ENVIRONMENT", "default")

    def enabled(self) -> bool:
        return bool(self.base) and bool(self.api_key)

    def _headers(self) -> Dict[str, str]:
        out = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
        if self.tenant_id:
            out["X-Kyb-Tenant-Id"] = self.tenant_id
        out["X-Kyb-Request-Id"] = str(uuid.uuid4())
        return out

    def _post(self, path: str, payload: Any, timeout_s: float = 5.0) -> Dict[str, Any]:
        import requests

        url = f"{self.base}{path}"
        token = _INFRA_CALL.set(True)
        try:
            resp = requests.post(url, headers=self._headers(), data=_canonical_json_bytes(payload), timeout=timeout_s)
        finally:
            _INFRA_CALL.reset(token)
        data = resp.json()
        if not data.get("ok", False):
            raise RuntimeError(f"infra_error:{path}:{data.get('error')}")
        return data.get("data") or {}

    def _get(self, path: str, params: Dict[str, str], timeout_s: float = 5.0) -> Dict[str, Any]:
        import requests

        url = f"{self.base}{path}"
        token = _INFRA_CALL.set(True)
        try:
            resp = requests.get(url, headers=self._headers(), params=params, timeout=timeout_s)
        finally:
            _INFRA_CALL.reset(token)
        data = resp.json()
        if not data.get("ok", False):
            raise RuntimeError(f"infra_error:{path}:{data.get('error')}")
        return data.get("data") or {}

    def decide(
        self,
        span_id: str,
        name: str,
        method: str,
        url: str,
        attrs: Dict[str, Any],
        idempotency_key: str,
        input_hash: str,
        attempt: int,
        prior_http_attempts: int,
    ) -> Dict[str, Any]:
        payload = {
            "ksp_version": "1",
            "tenant_id": self.tenant_id,
            "project_id": self.project_id,
            "environment": self.environment,
            "run": {"run_id": self.run_id, "trace_id": "", "initiator": {}},
            "span": {
                "span_id": span_id,
                "parent_span_id": "",
                "kind": "http",
                "name": name,
                "start_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                "idempotency_key": idempotency_key,
                "dedupe_scope": "run",
                "input_fingerprint": input_hash,
                "attributes": attrs,
            },
            "toggles": {
                "kyb_on": True,
                "idempotency": True,
                "retry": True,
                "budget": True,
                "gate": False,
            },
            "observations": {
                "attempt": attempt,
                "prior_http_attempts": prior_http_attempts,
                "prior_spans": 0,
                "run_cost_micros_so_far": 0,
                "run_calls_so_far": 0,
            },
            "meta": {},
            "run_id": self.run_id,
            "span_id": span_id,
            "span_type": "http",
            "name": name,
            "attempt": attempt,
            "idempotency_key": idempotency_key,
            "input_hash": input_hash,
            "attributes": attrs,
        }
        return self._post("/infra/decide", payload)

    def gate_status(self, gate_id: str) -> Dict[str, Any]:
        return self._get("/infra/gate/status", {"gate_id": gate_id})

    def emit_event(self, run_id: str, parent_id: str, event_type: str, payload: Dict[str, Any]) -> None:
        body = {"run_id": run_id, "parent_id": parent_id, "type": event_type, "payload": payload}
        self._post("/infra/graph/event", body, timeout_s=5.0)

    def emit_events_batch(self, events: list[Dict[str, Any]]) -> None:
        self._post("/infra/graph/events/batch", {"events": events}, timeout_s=5.0)

    def task_run_start(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._post("/infra/task_runs/start", payload, timeout_s=5.0)

    def task_run_finish(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._post("/infra/task_runs/finish", payload, timeout_s=5.0)

    def task_run_artifact(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._post("/infra/task_runs/artifacts", payload, timeout_s=5.0)

    def cost_record(self, tenant_id: str, run_id: str, micros: int, currency: str, source: str) -> None:
        body = {"tenant_id": tenant_id, "run_id": run_id, "cost_micros": micros, "currency": currency, "source": source}
        self._post("/infra/cost/record", body, timeout_s=5.0)


def _span_start_payload(
    tenant_id: str,
    run_id: str,
    span_id: str,
    name: str,
    idempotency_key: str,
    input_hash: str,
    attrs: Dict[str, Any],
) -> Dict[str, Any]:
    return {
        "ksp_version": "ksp.v1",
        "run_id": run_id,
        "tenant_id": tenant_id,
        "event_type": "SPAN_START",
        "span_id": span_id,
        "span_type": "http",
        "name": name,
        "idempotency_key": idempotency_key,
        "input_hash": input_hash,
        "attributes": attrs,
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }


def _span_end_payload(
    tenant_id: str,
    run_id: str,
    span_id: str,
    status: str,
    latency_ms: int,
    status_code: Optional[int],
    error: str = "",
) -> Dict[str, Any]:
    payload = {
        "ksp_version": "ksp.v1",
        "run_id": run_id,
        "tenant_id": tenant_id,
        "event_type": "SPAN_END",
        "span_id": span_id,
        "status": status,
        "latency_ms": max(0, int(latency_ms)),
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }
    if status_code is not None:
        payload["http_status"] = int(status_code)
    if error:
        payload["error"] = error
    return payload


def _intercept_sync_request(
    method: str,
    url: str,
    kwargs: Dict[str, Any],
    infra: InfraClient,
    sender: Callable[[Dict[str, Any]], Any],
) -> Any:
    _enforce_egress()
    local_kwargs = dict(kwargs)
    if (
        not infra.enabled()
        or not infra.run_id
        or not infra.tenant_id
        or _INFRA_CALL.get()
        or _is_infra_target(url, infra.base)
    ):
        return sender(local_kwargs)

    host, path = _parse_url(url)
    side_effect = _is_side_effect_http(method)
    span_id = str(uuid.uuid4())
    idempotency_key, input_hash = _derive_http_idempotency_key(method, url, local_kwargs)
    name = host or "http"

    attrs = {
        "http": {
            "method": (method or "").upper(),
            "url": str(url),
            "host": host,
            "path": path,
            "is_side_effect": side_effect,
        },
        "side_effect": side_effect,
    }

    task_start = infra.task_run_start(
        {
            "tenant_id": infra.tenant_id,
            "run_id": infra.run_id,
            "span_id": span_id,
            "task_run_id": span_id,
            "kind": "http",
            "name": name,
            "idempotency_key": idempotency_key,
            "attributes": attrs,
            "attempt": 1,
        }
    )
    task_run_id = str(task_start.get("task_run_id") or span_id)

    _safe_call(
        infra.task_run_artifact,
        {
            "tenant_id": infra.tenant_id,
            "run_id": infra.run_id,
            "task_run_id": task_run_id,
            "kind": "request",
            "payload": _derive_http_signature(method, url, local_kwargs),
        },
    )

    _safe_call(
        infra.emit_event,
        infra.run_id,
        "",
        "SPAN_START",
        _span_start_payload(infra.tenant_id, infra.run_id, span_id, name, idempotency_key, input_hash, attrs),
    )

    prior_http_attempts = 0
    retry_conf: Optional[Dict[str, Any]] = None
    max_attempts = 1
    last_exc: Optional[BaseException] = None
    span_ended = False

    try:
        decision = infra.decide(
            span_id=span_id,
            name=name,
            method=method,
            url=url,
            attrs=attrs,
            idempotency_key=idempotency_key,
            input_hash=input_hash,
            attempt=1,
            prior_http_attempts=0,
        )
        _apply_proxy_headers(local_kwargs, decision.get("proxy_headers") or {})

        verdict = str(decision.get("verdict") or "allow").lower()
        if verdict == "deny":
            _safe_call(
                infra.task_run_finish,
                {
                    "tenant_id": infra.tenant_id,
                    "run_id": infra.run_id,
                    "task_run_id": task_run_id,
                    "status": "DENIED",
                    "error": decision.get("reason", "policy_denied"),
                    "ended_at": time.time(),
                },
            )
            _safe_call(
                infra.emit_event,
                infra.run_id,
                "",
                "SPAN_END",
                _span_end_payload(
                    infra.tenant_id,
                    infra.run_id,
                    span_id,
                    status="error",
                    latency_ms=0,
                    status_code=None,
                    error=str(decision.get("reason", "policy_denied")),
                ),
            )
            span_ended = True
            raise RuntimeError(f"kyb_deny:{decision.get('reason')}")

        if verdict == "require_approval":
            gate_id = str(decision.get("gate_id") or "")
            deadline_s = float(_env("KYB_GATE_TIMEOUT_S", "300") or "300")
            poll_s = float(_env("KYB_GATE_POLL_S", "1") or "1")
            started = time.time()
            while True:
                state = infra.gate_status(gate_id)
                gate_status = str(state.get("status") or state.get("gate_status") or "").upper()
                if gate_status in {"APPROVED", "DENIED"}:
                    if gate_status == "DENIED":
                        _safe_call(
                            infra.task_run_finish,
                            {
                                "tenant_id": infra.tenant_id,
                                "run_id": infra.run_id,
                                "task_run_id": task_run_id,
                                "status": "DENIED",
                                "error": "gate_denied",
                                "ended_at": time.time(),
                            },
                        )
                        _safe_call(
                            infra.emit_event,
                            infra.run_id,
                            "",
                            "SPAN_END",
                            _span_end_payload(
                                infra.tenant_id,
                                infra.run_id,
                                span_id,
                                status="error",
                                latency_ms=0,
                                status_code=None,
                                error="gate_denied",
                            ),
                        )
                        span_ended = True
                        raise RuntimeError("kyb_gate_denied")
                    break
                if time.time() - started > deadline_s:
                    _safe_call(
                        infra.task_run_finish,
                        {
                            "tenant_id": infra.tenant_id,
                            "run_id": infra.run_id,
                            "task_run_id": task_run_id,
                            "status": "BLOCKED",
                            "error": "gate_timeout",
                            "ended_at": time.time(),
                        },
                    )
                    _safe_call(
                        infra.emit_event,
                        infra.run_id,
                        "",
                        "SPAN_END",
                        _span_end_payload(
                            infra.tenant_id,
                            infra.run_id,
                            span_id,
                            status="error",
                            latency_ms=0,
                            status_code=None,
                            error="gate_timeout",
                        ),
                    )
                    span_ended = True
                    raise RuntimeError("kyb_gate_timeout")
                time.sleep(poll_s)

        retry_conf = decision.get("retry")
        if retry_conf and int(retry_conf.get("max_attempts") or 0) > 1:
            max_attempts = int(retry_conf.get("max_attempts"))

        for attempt in range(1, max_attempts + 1):
            request_started = time.time()
            try:
                resp = sender(local_kwargs)
                latency_ms = int((time.time() - request_started) * 1000)

                _safe_call(
                    infra.emit_event,
                    infra.run_id,
                    "",
                    "SPAN_END",
                    _span_end_payload(
                        infra.tenant_id,
                        infra.run_id,
                        span_id,
                        status="ok",
                        latency_ms=latency_ms,
                        status_code=getattr(resp, "status_code", None),
                    ),
                )
                span_ended = True

                _safe_call(
                    infra.task_run_artifact,
                    {
                        "tenant_id": infra.tenant_id,
                        "run_id": infra.run_id,
                        "task_run_id": task_run_id,
                        "kind": "response",
                        "payload": {"status_code": getattr(resp, "status_code", None), "latency_ms": latency_ms},
                    },
                )

                _safe_call(
                    infra.task_run_finish,
                    {
                        "tenant_id": infra.tenant_id,
                        "run_id": infra.run_id,
                        "task_run_id": task_run_id,
                        "status": "OK",
                        "result": {"status_code": getattr(resp, "status_code", None), "latency_ms": latency_ms},
                        "ended_at": time.time(),
                    },
                )

                cost_micros = _parse_cost_header(resp)
                if cost_micros > 0:
                    _safe_call(infra.cost_record, infra.tenant_id, infra.run_id, cost_micros, "USD", "http")
                return resp
            except Exception as exc:  # noqa: PERF203
                last_exc = exc
                prior_http_attempts += 1
                latency_ms = int((time.time() - request_started) * 1000)

                _safe_call(
                    infra.emit_event,
                    infra.run_id,
                    "",
                    "HTTP_ERROR",
                    {
                        "run_id": infra.run_id,
                        "span_id": span_id,
                        "error": str(exc),
                        "attempt": attempt,
                        "latency_ms": latency_ms,
                    },
                )

                _safe_call(
                    infra.task_run_artifact,
                    {
                        "tenant_id": infra.tenant_id,
                        "run_id": infra.run_id,
                        "task_run_id": task_run_id,
                        "kind": "error",
                        "payload": {"attempt": attempt, "error": str(exc)},
                    },
                )

                decision = infra.decide(
                    span_id=span_id,
                    name=name,
                    method=method,
                    url=url,
                    attrs=attrs,
                    idempotency_key=idempotency_key,
                    input_hash=input_hash,
                    attempt=attempt + 1,
                    prior_http_attempts=prior_http_attempts,
                )
                verdict = str(decision.get("verdict") or "allow").lower()
                if verdict == "deny":
                    break

                if retry_conf:
                    _retry_sleep(
                        int(retry_conf.get("base_delay_ms") or 0),
                        int(retry_conf.get("max_delay_ms") or 0),
                        attempt,
                        bool(retry_conf.get("jitter") or False),
                    )
                else:
                    break

        _safe_call(
            infra.task_run_finish,
            {
                "tenant_id": infra.tenant_id,
                "run_id": infra.run_id,
                "task_run_id": task_run_id,
                "status": "ERROR",
                "error": str(last_exc) if last_exc else "unknown_error",
                "ended_at": time.time(),
            },
        )
        if not span_ended:
            _safe_call(
                infra.emit_event,
                infra.run_id,
                "",
                "SPAN_END",
                _span_end_payload(
                    infra.tenant_id,
                    infra.run_id,
                    span_id,
                    status="error",
                    latency_ms=0,
                    status_code=None,
                    error=str(last_exc) if last_exc else "unknown_error",
                ),
            )
            span_ended = True
        if last_exc is not None:
            raise last_exc
        raise RuntimeError("http_request_failed")
    except Exception:
        if not span_ended:
            _safe_call(
                infra.emit_event,
                infra.run_id,
                "",
                "SPAN_END",
                _span_end_payload(
                    infra.tenant_id,
                    infra.run_id,
                    span_id,
                    status="error",
                    latency_ms=0,
                    status_code=None,
                    error="local_error",
                ),
            )
        raise


def install_requests_patch() -> None:
    global _INSTALLED_REQUESTS
    if _INSTALLED_REQUESTS:
        return
    _INSTALLED_REQUESTS = True

    try:
        import requests  # type: ignore
    except Exception:
        return

    original = requests.sessions.Session.request
    infra = InfraClient()

    def patched(self, method, url, **kwargs):  # type: ignore[no-untyped-def]
        return _intercept_sync_request(
            method=str(method),
            url=str(url),
            kwargs=kwargs,
            infra=infra,
            sender=lambda request_kwargs: original(self, method, url, **request_kwargs),
        )

    requests.sessions.Session.request = patched  # type: ignore[attr-defined]


def install_httpx_patch() -> None:
    global _INSTALLED_HTTPX
    if _INSTALLED_HTTPX:
        return
    _INSTALLED_HTTPX = True

    try:
        import httpx  # type: ignore
    except Exception:
        return

    original = httpx.Client.request
    infra = InfraClient()

    def patched(self, method, url, **kwargs):  # type: ignore[no-untyped-def]
        return _intercept_sync_request(
            method=str(method),
            url=str(url),
            kwargs=kwargs,
            infra=infra,
            sender=lambda request_kwargs: original(self, method, url, **request_kwargs),
        )

    httpx.Client.request = patched  # type: ignore[attr-defined]
