"""Standard error envelope model with exception-based construction."""

from __future__ import annotations

import re
from collections import deque
from typing import Any

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    JsonValue,
    field_validator,
    model_validator,
)

from dr_langfuse.errors.codes import ErrorCode, StatusCodeMapping
from dr_langfuse.validation import ensure_finite_json


class ErrorEnvelope(BaseModel):
    """Standard error payload."""

    model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)

    code: ErrorCode | None = None
    message: str | None = Field(default=None, min_length=1)
    retriable: bool = False
    details: dict[str, JsonValue] = Field(default_factory=dict)

    # Construction inputs — excluded from serialization
    exception: Exception | None = Field(default=None, exclude=True, repr=False)
    operation: str | None = Field(default=None, exclude=True, repr=False)
    status_codes: StatusCodeMapping = Field(
        default_factory=StatusCodeMapping, exclude=True, repr=False
    )

    @model_validator(mode="before")
    @classmethod
    def _resolve_from_exception(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        exc = data.get("exception")
        if exc is None:
            return data

        operation = data.get("operation", "unknown")
        status_codes = data.get("status_codes") or StatusCodeMapping()
        if isinstance(status_codes, dict):
            status_codes = StatusCodeMapping(**status_codes)

        details: dict[str, Any] = {
            "operation": operation,
            "exception_type": type(exc).__name__,
        }

        status_code = _extract_status_code(exc)
        if status_code in status_codes.auth:
            data.setdefault("code", ErrorCode.AUTH)
            data.setdefault("message", "Langfuse authentication failed")
            data.setdefault("retriable", False)
            details["status_code"] = status_code
        elif status_code in status_codes.timeout:
            data.setdefault("code", ErrorCode.TIMEOUT)
            data.setdefault("message", "Langfuse request timed out")
            data.setdefault("retriable", True)
            details["status_code"] = status_code
        elif status_code in status_codes.malformed_request:
            data.setdefault("code", ErrorCode.MALFORMED_REQUEST)
            data.setdefault("message", "Langfuse request was rejected")
            data.setdefault("retriable", False)
            details["status_code"] = status_code
        elif status_code in status_codes.unavailable:
            data.setdefault("code", ErrorCode.UNAVAILABLE)
            data.setdefault("message", "Langfuse is unavailable")
            data.setdefault("retriable", True)
            details["status_code"] = status_code
        elif _is_transport_exception(exc):
            data.setdefault("code", ErrorCode.UNAVAILABLE)
            data.setdefault("message", "Langfuse is unavailable")
            data.setdefault("retriable", True)
        else:
            data.setdefault("code", ErrorCode.INTERNAL_ERROR)
            data.setdefault("message", "Langfuse integration failed")
            data.setdefault("retriable", False)

        existing_details = data.get("details", {})
        data["details"] = {**details, **existing_details}

        return data

    @model_validator(mode="after")
    def _require_code_and_message(self) -> ErrorEnvelope:
        if self.code is None:
            raise ValueError(
                "ErrorEnvelope.code is required when exception-based inference is unavailable"
            )
        if self.message is None:
            raise ValueError(
                "ErrorEnvelope.message is required when exception-based inference is unavailable"
            )
        return self

    @field_validator("details")
    @classmethod
    def _ensure_json_safe_details(
        cls, details: dict[str, JsonValue]
    ) -> dict[str, JsonValue]:
        for key, value in details.items():
            ensure_finite_json(value, path=f"details.{key}")
        return details


# Match transport-related tokens; exclude exact word-boundary prefixes
# used in "disconnect"/"reconnect".
_TRANSPORT_NAME_PATTERN = re.compile(
    r"(?<!\bdis)(?<!\bre)(connect|connection|timeout|network)"
)


def _looks_like_transport_exception(name: str) -> bool:
    """True if the exception name suggests a transport error (connect/timeout/network)."""
    return _TRANSPORT_NAME_PATTERN.search(name) is not None


def _extract_status_code(exc: Exception) -> int | None:
    status_code = getattr(exc, "status_code", None)
    if isinstance(status_code, int):
        return status_code
    response = getattr(exc, "response", None)
    response_code = getattr(response, "status_code", None)
    if isinstance(response_code, int):
        return response_code
    return None


def _is_transport_exception(exc: Exception) -> bool:
    queue: deque[BaseException] = deque([exc])
    seen: set[int] = set()
    while queue:
        current = queue.popleft()
        if id(current) in seen:
            continue
        seen.add(id(current))

        if isinstance(current, (ConnectionError, TimeoutError)):
            return True
        request = getattr(current, "request", None)
        name = type(current).__name__.lower()
        if request is not None and _looks_like_transport_exception(name):
            return True

        cause = current.__cause__
        if cause is not None:
            queue.append(cause)
        context = current.__context__
        if context is not None:
            queue.append(context)
    return False
