"""History compaction types."""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Literal, Protocol, TypedDict, cast

COMPACTION_SUMMARY_RETRY_FLOOR_TOKENS = 1_000

_ScopeKind = Literal["agent", "team"]
_HistoryMode = Literal["all", "runs", "messages"]
_CompactionMode = Literal["auto", "manual"]
_CompactionDecisionMode = Literal["none", "required"]
CompactionReplyOutcome = Literal["none", "success", "failed", "timeout"]
_CompactionLifecycleStatus = Literal["success", "failed", "timeout"]
CompactionAvailabilityReason = Literal[
    "no_context_window",
    "non_positive_summary_input_budget",
    "summary_input_budget_without_retry_headroom",
]
_ReplayPlanMode = Literal["configured", "limited", "disabled"]


class _HistoryScopeMetadata(TypedDict):
    """JSON-safe persisted history-scope identity."""

    kind: _ScopeKind
    scope_id: str


@dataclass(frozen=True)
class HistoryScope:
    """One logical persisted-history scope inside a stored Agno session."""

    kind: _ScopeKind
    scope_id: str

    def __post_init__(self) -> None:
        """Validate the persisted-history scope identity."""
        if self.kind not in {"agent", "team"}:
            message = f"Invalid history scope kind: {self.kind!r}"
            raise ValueError(message)
        if not self.scope_id:
            message = "History scope requires a non-empty scope_id"
            raise ValueError(message)

    @property
    def key(self) -> str:
        """Return the stable serialized storage key for this scope."""
        return f"{self.kind}:{self.scope_id}"

    def to_metadata(self) -> _HistoryScopeMetadata:
        """Return JSON-safe history-scope metadata."""
        return {
            "kind": self.kind,
            "scope_id": self.scope_id,
        }

    @classmethod
    def from_metadata(cls, raw_metadata: object) -> HistoryScope | None:
        """Return normalized history-scope metadata."""
        if not isinstance(raw_metadata, Mapping):
            return None
        metadata = cast("Mapping[str, object]", raw_metadata)
        kind = metadata.get("kind")
        scope_id = metadata.get("scope_id")
        if kind not in {"agent", "team"} or not isinstance(scope_id, str) or not scope_id:
            return None
        return cls(kind=cast("_ScopeKind", kind), scope_id=scope_id)


@dataclass(frozen=True)
class HistoryPolicy:
    """Authored raw-history selection policy for one run."""

    mode: _HistoryMode
    limit: int | None = None

    @property
    def num_history_runs(self) -> int | None:
        """Return the Agno run-replay limit for this policy."""
        return self.limit if self.mode == "runs" else None

    @property
    def num_history_messages(self) -> int | None:
        """Return the Agno message-replay limit for this policy."""
        return self.limit if self.mode == "messages" else None


@dataclass(frozen=True)
class ResolvedHistorySettings:
    """Resolved history selection policy and tool-call limits for one run."""

    policy: HistoryPolicy
    max_tool_calls_from_history: int | None
    system_message_role: str = "system"


@dataclass(frozen=True)
class HistoryScopeState:
    """Persisted compaction control/audit state stored in session metadata.

    ``compacted_run_ids`` are tombstones for runs already folded into the durable
    summary; they let the state owner prune runs that a stale session write
    reintroduced after compaction progress was persisted.
    """

    last_compacted_at: str | None = None
    last_summary_model: str | None = None
    last_compacted_run_count: int | None = None
    compacted_run_ids: tuple[str, ...] = ()
    force_compact_before_next_run: bool = False


@dataclass(frozen=True)
class ResolvedHistoryExecutionPlan:
    """Single source of truth for history-budget policy in one run scope."""

    authored_compaction_enabled: bool
    destructive_compaction_available: bool
    explicit_compaction_model: bool
    compaction_model_name: str
    compaction_context_window: int | None
    replay_window_tokens: int | None
    trigger_threshold_tokens: int | None
    reserve_tokens: int
    static_prompt_tokens: int | None
    replay_budget_tokens: int | None
    summary_input_budget_tokens: int | None
    compaction_timeout_seconds: float
    unavailable_reason: CompactionAvailabilityReason | None = None
    hard_replay_budget_tokens: int | None = None
    compaction_fallback_model_name: str | None = None
    compaction_fallback_summary_input_budget_tokens: int | None = None


@dataclass(frozen=True)
class ResolvedReplayPlan:
    """Concrete persisted-replay plan for one live model call."""

    mode: _ReplayPlanMode
    estimated_tokens: int
    add_history_to_context: bool
    num_history_runs: int | None = None
    num_history_messages: int | None = None


@dataclass(frozen=True)
class CompactionDecision:
    """Resolved compaction lifecycle decision for one history preparation."""

    mode: _CompactionDecisionMode
    reason: str
    current_history_tokens: int | None = None
    trigger_budget_tokens: int | None = None
    hard_budget_tokens: int | None = None
    fitted_replay_tokens: int | None = None


@dataclass(frozen=True)
class CompactionLifecycleStart:
    """Visible lifecycle notice payload emitted before foreground compaction."""

    mode: _CompactionMode
    session_id: str
    scope: str
    summary_model: str
    before_tokens: int
    history_budget_tokens: int | None
    runs_before: int
    threshold_tokens: int | None = None


@dataclass(frozen=True)
class CompactionLifecycleProgress:
    """Visible lifecycle progress payload emitted after persisted compaction chunks."""

    notice_event_id: str | None
    mode: _CompactionMode
    session_id: str
    scope: str
    summary_model: str
    before_tokens: int
    after_tokens: int
    history_budget_tokens: int | None
    runs_before: int
    compacted_run_count: int
    runs_remaining: int
    threshold_tokens: int | None = None
    duration_ms: int | None = None

    def to_notice_metadata(self) -> dict[str, object]:
        """Return serialized progress metadata for Matrix compaction messages."""
        meta: dict[str, object] = {
            "version": 3,
            "status": "running",
            "mode": self.mode,
            "session_id": self.session_id,
            "scope": self.scope,
            "summary_model": self.summary_model,
            "before_tokens": self.before_tokens,
            "after_tokens": self.after_tokens,
            "runs_before": self.runs_before,
            "compacted_run_count": self.compacted_run_count,
            "runs_remaining": self.runs_remaining,
        }
        if self.history_budget_tokens is not None:
            meta["history_budget_tokens"] = self.history_budget_tokens
        if self.threshold_tokens is not None:
            meta["threshold_tokens"] = self.threshold_tokens
        if self.duration_ms is not None:
            meta["duration_ms"] = self.duration_ms
        return meta

    def format_notice(self) -> str:
        """Format a human-readable in-progress compaction notice."""
        body = (
            f"Compacting history... saved {self.compacted_run_count}/{self.runs_before} runs: "
            f"{_format_exact_tokens(self.before_tokens)} -> {_format_exact_tokens(self.after_tokens)}"
        )
        if self.history_budget_tokens is not None:
            body += f" / {_format_exact_tokens(self.history_budget_tokens)} history budget"
        if self.runs_remaining:
            body += f"\n   {self.runs_remaining} runs remaining"
        return body


@dataclass(frozen=True)
class CompactionLifecycleFailure:
    """Visible lifecycle notice payload emitted after failed foreground compaction."""

    notice_event_id: str | None
    mode: _CompactionMode
    session_id: str
    scope: str
    summary_model: str
    status: _CompactionLifecycleStatus
    duration_ms: int
    failure_reason: str
    history_budget_tokens: int | None


class CompactionLifecycle(Protocol):
    """Interface for ordered foreground compaction Matrix notice delivery."""

    async def start(self, event: CompactionLifecycleStart) -> str | None:
        """Send the initial compaction notice and return its Matrix event id."""

    async def complete_success(self, outcome: CompactionOutcome) -> None:
        """Edit the lifecycle notice after successful compaction."""

    async def progress(self, event: CompactionLifecycleProgress) -> None:
        """Edit the lifecycle notice after persisted compaction progress."""

    async def complete_failure(self, event: CompactionLifecycleFailure) -> None:
        """Edit the lifecycle notice after failed compaction."""


def _format_exact_tokens(tokens: int) -> str:
    """Format token counts exactly with thousands separators."""
    return f"{tokens:,}"


@dataclass(frozen=True)
class CompactionOutcome:
    """Completed compaction result used for lifecycle notices and tests."""

    mode: _CompactionMode
    session_id: str
    scope: str
    summary: str
    summary_model: str
    before_tokens: int
    after_tokens: int
    window_tokens: int
    threshold_tokens: int
    runs_before: int
    runs_after: int
    compacted_run_count: int
    compacted_at: str
    history_budget_tokens: int | None = None
    lifecycle_notice_event_id: str | None = None
    duration_ms: int | None = None
    status: _CompactionLifecycleStatus = "success"

    def to_notice_metadata(self) -> dict[str, object]:
        """Return serialized notice metadata for Matrix compaction messages."""
        meta: dict[str, object] = {
            "version": 3,
            "status": self.status,
            "mode": self.mode,
            "session_id": self.session_id,
            "scope": self.scope,
            "summary_model": self.summary_model,
            "before_tokens": self.before_tokens,
            "after_tokens": self.after_tokens,
            "window_tokens": self.window_tokens,
            "runs_before": self.runs_before,
            "runs_after": self.runs_after,
            "compacted_run_count": self.compacted_run_count,
            "compacted_at": self.compacted_at,
        }
        if self.history_budget_tokens is not None:
            meta["history_budget_tokens"] = self.history_budget_tokens
        if self.threshold_tokens:
            meta["threshold_tokens"] = self.threshold_tokens
        if self.lifecycle_notice_event_id is not None:
            meta["lifecycle_notice_event_id"] = self.lifecycle_notice_event_id
        if self.duration_ms is not None:
            meta["duration_ms"] = self.duration_ms
        return meta

    def format_notice(self) -> str:
        """Format a human-readable compaction notice."""
        line1 = (
            f"\U0001f4e6 Compacted {self.compacted_run_count} runs: "
            f"{_format_exact_tokens(self.before_tokens)} \u2192 {_format_exact_tokens(self.after_tokens)}"
        )
        if self.history_budget_tokens is not None:
            line1 += f" / {_format_exact_tokens(self.history_budget_tokens)} history budget"
        return line1


@dataclass(frozen=True)
class PreparedHistoryState:
    """Prepared persisted-history state for one run."""

    compaction_outcomes: list[CompactionOutcome] = field(default_factory=list)
    replay_plan: ResolvedReplayPlan | None = None
    replays_persisted_history: bool = False
    compaction_decision: CompactionDecision = field(
        default_factory=lambda: CompactionDecision(mode="none", reason="unclassified"),
    )
    compaction_reply_outcome: CompactionReplyOutcome = "none"
    prepared_context_tokens: int | None = None
