"""Credential-safe diagnostics shared across product boundaries."""

import json
import re
import traceback
from dataclasses import dataclass
from typing import Any

from .failures import ExecutionFailure

ERROR_DETAIL_DISPLAY_CAP_BYTES = 16_384
_MAX_CAUSE_CHAIN_DEPTH = 4
_UPSTREAM_BODY_ATTR = "_fcc_upstream_error_body"
_UPSTREAM_BODY_TRUNCATED_ATTR = "_fcc_upstream_error_body_truncated"

_SECRET_TEXT_REPLACEMENTS = (
    (
        re.compile(
            r"(?i)(?P<prefix>[\"']?authorization[\"']?\s*[:=]\s*)"
            r"(?P<quote>[\"']?)(?:(?:bearer|basic)\s+)?"
            r"[^\"'\s,;&}\]]+(?P=quote)"
        ),
        r"\g<prefix>\g<quote><redacted>\g<quote>",
    ),
    (
        re.compile(
            r"(?i)(?P<prefix>[\"']?(?:api[_-]?key|access[_-]?token|"
            r"refresh[_-]?token|token|client[_-]?secret|secret|password)"
            r"[\"']?\s*[:=]\s*)(?P<quote>[\"']?)"
            r"[^\"'\s,;&}\]]+(?P=quote)"
        ),
        r"\g<prefix>\g<quote><redacted>\g<quote>",
    ),
    (re.compile(r"(?i)(bearer\s+)[^\s,;]+"), r"\1<redacted>"),
    (
        re.compile(
            r"(?i)(?<![a-z0-9])(?:sk-[a-z0-9._-]{8,}|"
            r"nvapi-[a-z0-9._-]{8,}|hf_[a-z0-9_-]{8,}|"
            r"gsk_[a-z0-9_-]{8,}|github_pat_[a-z0-9_]{8,}|"
            r"gh[pousr]_[a-z0-9]{8,}|AIza[a-z0-9_-]{20,})"
            r"(?![a-z0-9])"
        ),
        "<redacted>",
    ),
)


@dataclass(frozen=True, slots=True)
class UpstreamErrorDetail:
    """Sanitized diagnostic detail extracted from an upstream exception."""

    status_code: int | None = None
    body_text: str | None = None
    exception_text: str | None = None
    cause_chain_text: str | None = None
    category_hint: str | None = None
    body_truncated: bool = False


def redact_sensitive_error_text(text: str) -> str:
    """Redact recognizable credentials while preserving diagnostic context."""
    sanitized = text
    for pattern, replacement in _SECRET_TEXT_REPLACEMENTS:
        sanitized = pattern.sub(replacement, sanitized)
    return sanitized


def safe_exception_message(
    exc: BaseException,
    *,
    fallback: str = "Provider request failed unexpectedly.",
) -> str:
    """Return a redacted, non-empty exception message."""
    message = redact_sensitive_error_text(str(exc).strip())
    return message or fallback


def format_user_error_preview(exc: BaseException, *, max_len: int = 200) -> str:
    """Return a short redacted exception preview for chat surfaces."""
    return safe_exception_message(exc)[:max_len]


def attach_upstream_error_body(
    exc: Exception,
    body: bytes | str,
    *,
    truncated: bool = False,
) -> None:
    """Attach a bounded streamed response body for later safe formatting."""
    setattr(exc, _UPSTREAM_BODY_ATTR, body)
    setattr(exc, _UPSTREAM_BODY_TRUNCATED_ATTR, truncated)


def exception_cause_types(exc: BaseException) -> tuple[str, ...]:
    """Return exception cause type names without logging their contents."""
    return tuple(type(cause).__name__ for cause in _exception_causes(exc))


def redacted_exception_traceback(exc: BaseException) -> str:
    """Format a traceback while redacting recognizable credentials."""
    return redact_sensitive_error_text("".join(traceback.format_exception(exc)))


def extract_upstream_error_detail(exc: Exception) -> UpstreamErrorDetail:
    """Extract bounded, redacted body, exception, and cause-chain details."""
    raw_body = getattr(exc, _UPSTREAM_BODY_ATTR, None)
    body_truncated = bool(getattr(exc, _UPSTREAM_BODY_TRUNCATED_ATTR, False))
    if raw_body is None:
        raw_body = getattr(exc, "body", None)
    if raw_body is None:
        raw_body = _body_from_response(exc)

    body_text = _normalize_body_text(raw_body)
    if body_text is not None:
        body_text = redact_sensitive_error_text(body_text)
        body_text, capped = _cap_text_bytes(body_text)
        body_truncated = body_truncated or capped

    exception_text = str(exc).strip() or None
    if exception_text is not None:
        exception_text = redact_sensitive_error_text(exception_text)
        exception_text, _ = _cap_text_bytes(exception_text)

    return UpstreamErrorDetail(
        status_code=_status_code_from_exception(exc),
        body_text=body_text,
        exception_text=exception_text,
        cause_chain_text=_exception_cause_chain_text(exc),
        category_hint=_category_hint_from_body(raw_body, body_text),
        body_truncated=body_truncated,
    )


def format_execution_failure_message(
    failure: ExecutionFailure,
    detail: UpstreamErrorDetail,
    *,
    upstream_name: str,
    request_id: str | None = None,
) -> str:
    """Build a copyable, redacted diagnostic for a finalized execution failure."""
    stable_message = failure.message
    has_upstream_detail = detail.status_code is not None or detail.body_text is not None
    if not has_upstream_detail:
        lines = [stable_message]
        if detail.exception_text and detail.exception_text != stable_message:
            lines.extend(("", "Provider exception:", detail.exception_text))
        if detail.cause_chain_text:
            lines.extend(("", "Caused by:", detail.cause_chain_text))
        _append_request_id_lines(lines, request_id)
        return "\n".join(lines)

    if detail.status_code == 405:
        lines = [
            f"Upstream provider {upstream_name} rejected the request method "
            "or endpoint (HTTP 405)."
        ]
    elif detail.status_code is not None:
        lines = [
            f"Upstream provider {upstream_name} returned HTTP {detail.status_code}."
        ]
    else:
        lines = [f"Upstream provider {upstream_name} returned an error."]

    lines.append(f"Category: {detail.category_hint or failure.kind.value}")
    if stable_message and stable_message != lines[0]:
        lines.append(f"Mapped message: {stable_message}")
    lines.extend(("", "Upstream error:"))
    lines.append(detail.body_text or "(empty upstream error body)")
    if _body_truncation_line_needed(detail):
        lines.append(f"... [truncated after {ERROR_DETAIL_DISPLAY_CAP_BYTES} bytes]")
    _append_request_id_lines(lines, request_id)
    return "\n".join(lines)


def _body_truncation_line_needed(detail: UpstreamErrorDetail) -> bool:
    """Return whether a separate truncation marker is needed."""
    return detail.body_truncated and (
        detail.body_text is None
        or f"truncated after {ERROR_DETAIL_DISPLAY_CAP_BYTES} bytes"
        not in detail.body_text
    )


def _status_code_from_exception(exc: Exception) -> int | None:
    status = getattr(exc, "status_code", None)
    if isinstance(status, int):
        return status
    response = getattr(exc, "response", None)
    response_status = getattr(response, "status_code", None)
    return response_status if isinstance(response_status, int) else None


def _body_from_response(exc: Exception) -> Any:
    response = getattr(exc, "response", None)
    if response is None:
        return None
    try:
        return response.json()
    except Exception:
        pass
    try:
        return response.text
    except Exception:
        return None


def _normalize_body_text(body: Any) -> str | None:
    if body is None:
        return None
    if isinstance(body, bytes):
        text = body.decode("utf-8", errors="replace")
    elif isinstance(body, str):
        text = body
    else:
        try:
            return json.dumps(body, ensure_ascii=False, separators=(",", ":"))
        except TypeError:
            text = str(body)
    stripped = text.strip()
    if not stripped:
        return None
    try:
        parsed = json.loads(stripped)
    except ValueError:
        return stripped
    return json.dumps(parsed, ensure_ascii=False, separators=(",", ":"))


def _cap_text_bytes(text: str) -> tuple[str, bool]:
    encoded = text.encode("utf-8", errors="replace")
    if len(encoded) <= ERROR_DETAIL_DISPLAY_CAP_BYTES:
        return text, False
    capped = encoded[:ERROR_DETAIL_DISPLAY_CAP_BYTES].decode("utf-8", errors="replace")
    return (
        f"{capped}\n... [truncated after {ERROR_DETAIL_DISPLAY_CAP_BYTES} bytes]",
        True,
    )


def _exception_causes(exc: BaseException) -> tuple[BaseException, ...]:
    causes: list[BaseException] = []
    seen = {id(exc)}
    current: BaseException | None = exc
    while current is not None and len(causes) < _MAX_CAUSE_CHAIN_DEPTH:
        next_exc = current.__cause__ or current.__context__
        if next_exc is None or id(next_exc) in seen:
            break
        seen.add(id(next_exc))
        causes.append(next_exc)
        current = next_exc
    return tuple(causes)


def _exception_cause_chain_text(exc: BaseException) -> str | None:
    lines: list[str] = []
    for cause in _exception_causes(exc):
        raw_text = str(cause).strip()
        lines.append(
            f"{type(cause).__name__}: {redact_sensitive_error_text(raw_text)}"
            if raw_text
            else type(cause).__name__
        )
    if not lines:
        return None
    text, _ = _cap_text_bytes("\n".join(lines))
    return text


def _category_hint_from_body(body: Any, body_text: str | None) -> str | None:
    parsed = body
    if isinstance(parsed, bytes):
        parsed = parsed.decode("utf-8", errors="replace")
    if isinstance(parsed, str):
        try:
            parsed = json.loads(parsed)
        except ValueError:
            parsed = None
    if isinstance(parsed, dict):
        error = parsed.get("error")
        if isinstance(error, dict):
            for key in ("type", "code"):
                value = error.get(key)
                if isinstance(value, str) and value.strip():
                    return value.strip()
        for key in ("type", "code"):
            value = parsed.get(key)
            if isinstance(value, str) and value.strip():
                return value.strip()
    if (
        body_text
        and "model" in body_text.lower()
        and "unsupported" in body_text.lower()
    ):
        return "upstream_model_error"
    return None


def _append_request_id_lines(lines: list[str], request_id: str | None) -> None:
    if request_id:
        lines.extend(("", f"Request ID: {request_id}"))
