from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, Generic, Literal, Protocol, TypeVar, runtime_checkable

from app.core.logging import get_logger

logger = get_logger("app.services.runtime_chat_attempts")

RuntimeChatAttemptOutcome = Literal[
    "succeeded",
    "empty",
    "provider_rejected",
    "postprocess_failed",
    "not_sent",
    "outcome_unknown",
]
RuntimeChatObserverStatus = Literal[
    "not_applicable",
    "not_configured",
    "permit_notified",
    "completed_notified",
    "reconciliation_required",
]
RuntimeChatUsageAvailability = Literal["available", "partial", "unavailable"]
RuntimeChatObserverPhase = Literal["permit", "completion", "failure_callback"]


@dataclass(frozen=True, slots=True)
class RuntimeChatOperationContext:
    """由可信业务入口构造；不得从 prompt 或客户端 payload 推导。"""

    tenant_id: str
    operation_id: str
    invocation_seq: int
    attempt_scope: str
    run_id: str | None = None

    def __post_init__(self) -> None:
        tenant_id = str(self.tenant_id or "").strip()
        operation_id = str(self.operation_id or "").strip()
        attempt_scope = str(self.attempt_scope or "").strip()
        run_id = str(self.run_id or "").strip() or None
        if not tenant_id:
            raise ValueError("RuntimeChatOperationContext.tenant_id 不能为空。")
        if not operation_id:
            raise ValueError("RuntimeChatOperationContext.operation_id 不能为空。")
        if (
            isinstance(self.invocation_seq, bool)
            or not isinstance(self.invocation_seq, int)
            or self.invocation_seq < 1
        ):
            raise ValueError("RuntimeChatOperationContext.invocation_seq 必须大于 0。")
        if not attempt_scope:
            raise ValueError("RuntimeChatOperationContext.attempt_scope 不能为空。")
        object.__setattr__(self, "tenant_id", tenant_id)
        object.__setattr__(self, "operation_id", operation_id)
        object.__setattr__(self, "attempt_scope", attempt_scope)
        object.__setattr__(self, "run_id", run_id)

    def build_attempt_identity(
        self,
        *,
        slot: str,
        provider: str,
        model: str,
        attempt: int,
    ) -> RuntimeChatAttemptIdentity:
        return RuntimeChatAttemptIdentity(
            tenant_id=self.tenant_id,
            operation_id=self.operation_id,
            run_id=self.run_id,
            invocation_seq=self.invocation_seq,
            attempt_scope=self.attempt_scope,
            slot=str(slot or "").strip(),
            provider=str(provider or "").strip(),
            model=str(model or "").strip(),
            attempt=int(attempt),
        )


@dataclass(frozen=True, slots=True)
class RuntimeChatAttemptIdentity:
    tenant_id: str
    operation_id: str
    run_id: str | None
    invocation_seq: int
    attempt_scope: str
    slot: str
    provider: str
    model: str
    attempt: int

    @property
    def attempt_key(self) -> str:
        canonical_identity = json.dumps(
            [
                self.tenant_id,
                self.operation_id,
                self.run_id or "-",
                self.invocation_seq,
                self.attempt_scope,
                self.slot,
                self.provider,
                self.model,
                self.attempt,
            ],
            ensure_ascii=False,
            separators=(",", ":"),
        )
        digest = hashlib.sha256(canonical_identity.encode("utf-8")).hexdigest()
        return f"runtime-chat-attempt:v1:{digest}"


@dataclass(frozen=True, slots=True)
class RuntimeChatAuthoritativeUsage:
    source: str = "unavailable"
    availability: RuntimeChatUsageAvailability = "unavailable"
    prompt_tokens: int | None = None
    completion_tokens: int | None = None
    total_tokens: int | None = None
    prompt_eval_count: int | None = None
    eval_count: int | None = None

    def model_dump(self) -> dict[str, Any]:
        return {
            "source": self.source,
            "availability": self.availability,
            "prompt_tokens": self.prompt_tokens,
            "completion_tokens": self.completion_tokens,
            "total_tokens": self.total_tokens,
            "prompt_eval_count": self.prompt_eval_count,
            "eval_count": self.eval_count,
        }


OutputT = TypeVar("OutputT")


@dataclass(slots=True)
class RuntimeChatProviderResponse(Generic[OutputT]):
    output: OutputT
    response_id: str | None = None
    response_model: str | None = None
    usage: RuntimeChatAuthoritativeUsage = field(
        default_factory=RuntimeChatAuthoritativeUsage
    )


class RuntimeChatProviderAttemptError(RuntimeError):
    def __init__(
        self,
        message: str,
        *,
        outcome: RuntimeChatAttemptOutcome,
        response_id: str | None = None,
        response_model: str | None = None,
        usage: RuntimeChatAuthoritativeUsage | None = None,
        status_code: int | None = None,
    ) -> None:
        super().__init__(message)
        self.outcome = outcome
        self.response_id = response_id
        self.response_model = response_model
        self.usage = usage or RuntimeChatAuthoritativeUsage()
        self.status_code = status_code


@dataclass(frozen=True, slots=True)
class RuntimeChatAttemptPermit:
    allowed: bool = True
    reason: str = ""


@dataclass(frozen=True, slots=True)
class RuntimeChatAttemptPermitEvent:
    identity: RuntimeChatAttemptIdentity
    started_at: datetime
    phase: Literal["permit"] = "permit"


@dataclass(frozen=True, slots=True)
class RuntimeChatAttemptCompletedEvent:
    identity: RuntimeChatAttemptIdentity
    started_at: datetime
    completed_at: datetime
    outcome: RuntimeChatAttemptOutcome
    response_id: str | None
    response_model: str | None
    provider_status_code: int | None
    usage: RuntimeChatAuthoritativeUsage
    error_type: str | None = None
    error_message: str | None = None

    @property
    def request_may_have_been_sent(self) -> bool:
        return self.outcome != "not_sent"

    @property
    def requires_reconciliation(self) -> bool:
        """发送结果未知时，后续只能补账核对，不能按零用量释放。"""

        return self.outcome == "outcome_unknown"


@dataclass(frozen=True, slots=True)
class RuntimeChatObserverFailure:
    phase: RuntimeChatObserverPhase
    identity: RuntimeChatAttemptIdentity
    occurred_at: datetime
    error_type: str
    error_message: str

    def model_dump(self) -> dict[str, str]:
        return {
            "phase": self.phase,
            "attempt_key": self.identity.attempt_key,
            "occurred_at": self.occurred_at.isoformat(),
            "error_type": self.error_type,
            "error_message": self.error_message,
        }


@runtime_checkable
class RuntimeChatAttemptObserver(Protocol):
    def on_permit(
        self,
        event: RuntimeChatAttemptPermitEvent,
    ) -> RuntimeChatAttemptPermit | None: ...

    def on_completed(self, event: RuntimeChatAttemptCompletedEvent) -> None: ...

    def on_observer_failure(self, failure: RuntimeChatObserverFailure) -> None: ...


@dataclass(frozen=True, slots=True)
class RuntimeChatAttemptNotification:
    started_at: datetime
    completed_at: datetime
    observer_status: RuntimeChatObserverStatus
    observer_failures: tuple[RuntimeChatObserverFailure, ...]


class RuntimeChatAttemptTracker:
    def __init__(
        self,
        *,
        observer: RuntimeChatAttemptObserver | None,
        operation_context: RuntimeChatOperationContext | None,
        slot: str,
        provider: str,
        model: str,
        attempt: int,
    ) -> None:
        self.observer = observer
        self.started_at = datetime.now(UTC)
        self.identity = (
            operation_context.build_attempt_identity(
                slot=slot,
                provider=provider,
                model=model,
                attempt=attempt,
            )
            if operation_context is not None
            else None
        )
        self.observer_failures: list[RuntimeChatObserverFailure] = []
        if self.identity is None:
            self.observer_status: RuntimeChatObserverStatus = "not_applicable"
        elif observer is None:
            self.observer_status = "not_configured"
        else:
            self.observer_status = "permit_notified"

    def notify_permit(self) -> RuntimeChatAttemptPermit:
        if self.identity is None or self.observer is None:
            return RuntimeChatAttemptPermit()
        try:
            decision = self.observer.on_permit(
                RuntimeChatAttemptPermitEvent(
                    identity=self.identity,
                    started_at=self.started_at,
                )
            )
            if decision is None:
                return RuntimeChatAttemptPermit()
            if not isinstance(decision, RuntimeChatAttemptPermit):
                raise TypeError(
                    "attempt observer on_permit 必须返回 "
                    "RuntimeChatAttemptPermit 或 None。"
                )
            return decision
        except Exception as exc:
            self._capture_observer_failure("permit", exc)
            # permit 可能承担额度预留或策略授权。状态未知时不得继续发起
            # provider 请求；completion(not_sent) 会给观察器释放或补偿机会。
            return RuntimeChatAttemptPermit(
                allowed=False,
                reason="Runtime chat attempt permit observer failed.",
            )

    def notify_completed(
        self,
        *,
        outcome: RuntimeChatAttemptOutcome,
        response_id: str | None,
        response_model: str | None,
        provider_status_code: int | None,
        usage: RuntimeChatAuthoritativeUsage,
        error: Exception | None = None,
    ) -> RuntimeChatAttemptNotification:
        completed_at = datetime.now(UTC)
        if self.identity is not None and self.observer is not None:
            try:
                self.observer.on_completed(
                    RuntimeChatAttemptCompletedEvent(
                        identity=self.identity,
                        started_at=self.started_at,
                        completed_at=completed_at,
                        outcome=outcome,
                        response_id=response_id,
                        response_model=response_model,
                        provider_status_code=provider_status_code,
                        usage=usage,
                        error_type=type(error).__name__ if error is not None else None,
                        error_message=str(error) if error is not None else None,
                    )
                )
                if not self.observer_failures:
                    self.observer_status = "completed_notified"
            except Exception as exc:
                self._capture_observer_failure("completion", exc)
        return RuntimeChatAttemptNotification(
            started_at=self.started_at,
            completed_at=completed_at,
            observer_status=self.observer_status,
            observer_failures=tuple(self.observer_failures),
        )

    def _capture_observer_failure(
        self,
        phase: Literal["permit", "completion"],
        exc: Exception,
    ) -> None:
        if self.identity is None:
            return
        failure = RuntimeChatObserverFailure(
            phase=phase,
            identity=self.identity,
            occurred_at=datetime.now(UTC),
            error_type=type(exc).__name__,
            error_message=str(exc),
        )
        self.observer_failures.append(failure)
        self.observer_status = "reconciliation_required"
        logger.warning(
            "Runtime chat attempt observer failed phase=%s attempt_key=%s error=%s",
            phase,
            self.identity.attempt_key,
            exc,
        )
        callback = getattr(self.observer, "on_observer_failure", None)
        if not callable(callback):
            return
        try:
            callback(failure)
        except Exception as callback_exc:
            callback_failure = RuntimeChatObserverFailure(
                phase="failure_callback",
                identity=self.identity,
                occurred_at=datetime.now(UTC),
                error_type=type(callback_exc).__name__,
                error_message=str(callback_exc),
            )
            self.observer_failures.append(callback_failure)
            logger.warning(
                "Runtime chat observer failure callback failed attempt_key=%s error=%s",
                self.identity.attempt_key,
                callback_exc,
            )


@dataclass(slots=True)
class RuntimeChatCallTrace:
    slot: str
    provider: str
    model: str
    attempt: int
    status: str
    duration_ms: int = 0
    error_message: str | None = None
    skipped_reason: str | None = None
    outcome: RuntimeChatAttemptOutcome | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    response_id: str | None = None
    response_model: str | None = None
    provider_status_code: int | None = None
    usage: RuntimeChatAuthoritativeUsage = field(
        default_factory=RuntimeChatAuthoritativeUsage
    )
    observer_status: RuntimeChatObserverStatus = "not_applicable"
    observer_failures: list[RuntimeChatObserverFailure] = field(default_factory=list)

    @property
    def requires_reconciliation(self) -> bool:
        return (
            self.outcome == "outcome_unknown"
            or self.observer_status
            in {"not_configured", "reconciliation_required"}
        )

    def model_dump(self) -> dict[str, Any]:
        return {
            "slot": self.slot,
            "provider": self.provider,
            "model": self.model,
            "attempt": self.attempt,
            "status": self.status,
            "duration_ms": self.duration_ms,
            "error_message": self.error_message,
            "skipped_reason": self.skipped_reason,
            "outcome": self.outcome,
            "started_at": self.started_at.isoformat() if self.started_at else None,
            "completed_at": self.completed_at.isoformat() if self.completed_at else None,
            "response_id": self.response_id,
            "response_model": self.response_model,
            "provider_status_code": self.provider_status_code,
            "usage": self.usage.model_dump(),
            "observer_status": self.observer_status,
            "requires_reconciliation": self.requires_reconciliation,
            "observer_failures": [item.model_dump() for item in self.observer_failures],
        }


@dataclass(slots=True)
class RuntimeChatResult:
    text: str | None
    calls: list[RuntimeChatCallTrace]

    def calls_as_dicts(self) -> list[dict[str, Any]]:
        return [item.model_dump() for item in self.calls]


@dataclass(slots=True)
class RuntimeChatToolCall:
    name: str
    arguments: dict[str, Any]
    call_id: str | None = None
    raw_arguments: str = ""


@dataclass(slots=True)
class RuntimeToolCallResult:
    tool_call: RuntimeChatToolCall | None
    calls: list[RuntimeChatCallTrace]

    def calls_as_dicts(self) -> list[dict[str, Any]]:
        return [item.model_dump() for item in self.calls]
