"""Element snapshot data models for the output rendering framework.

Each model represents the accumulated state of an element at a point in
time.  Handles mutate these models incrementally; strategies consume them
for rendering.

Based on the Output Rendering Framework specification (v3_spec.md §Output
Rendering Framework / Element Handles).
"""

from __future__ import annotations

import time
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, field_validator

# ---------------------------------------------------------------------------
# Resource limits (configurable soft caps — M7, M8)
# ---------------------------------------------------------------------------

MAX_TREE_DEPTH: int = 64
"""Maximum nesting depth for tree elements (DoS guard)."""

MAX_ELEMENTS_PER_SESSION: int = 10_000
"""Soft limit on the number of element handles per session."""

MAX_TABLE_ROWS: int = 100_000
"""Soft limit on the number of rows in a single table."""


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------


class ElementClosedError(RuntimeError):
    """Raised when a write method is called on a closed handle."""


# ---------------------------------------------------------------------------
# Element snapshot data classes
# ---------------------------------------------------------------------------


class PanelEntry(BaseModel):
    """A single key-value pair within a Panel."""

    key: str
    value: str
    style_hint: str | None = None
    icon: str | None = None


class Panel(BaseModel):
    """A titled group of key-value pairs."""

    title: str
    entries: list[PanelEntry] = Field(default_factory=list)
    border_style: str = "rounded"
    priority: str = "normal"
    collapse_hint: str = "auto"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "panel"


class ColumnDef(BaseModel):
    """Schema for a single table column."""

    name: str
    col_type: str = "string"
    alignment: str = "left"
    width_hint: int | None = None
    sortable: bool = False
    style_hint: str | None = None


class Table(BaseModel):
    """A tabular data set with typed columns."""

    title: str | None
    columns: list[ColumnDef] = Field(default_factory=list)
    rows: list[dict[str, Any]] = Field(default_factory=list)
    summary: dict[str, Any] | None = None
    max_rows_hint: int | None = None
    sort_key: str | None = None
    sort_descending: bool = False
    priority: str = "normal"
    collapse_hint: str = "auto"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "table"


class StatusMessage(BaseModel):
    """A status line (success, warning, error, info)."""

    message: str
    level: str = "info"
    detail: str | None = None
    priority: str = "normal"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "status"


class ProgressStep(BaseModel):
    """A named step within a progress indicator."""

    label: str
    status: str = "pending"


class ProgressIndicator(BaseModel):
    """A progress bar or spinner for long-running operations."""

    label: str
    current: int | None = None
    total: int | None = None
    indeterminate: bool = False
    steps: list[ProgressStep] | None = None
    priority: str = "normal"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "progress"


class TreeNode(BaseModel):
    """A single node in a hierarchical tree structure."""

    label: str
    style_hint: str | None = None
    children: list[TreeNode] = Field(default_factory=list)
    collapsed: bool = False
    metadata: dict[str, Any] = Field(default_factory=dict)


class Tree(BaseModel):
    """A hierarchical tree display element."""

    root: TreeNode
    max_depth_hint: int | None = None
    show_guides: bool = True
    priority: str = "normal"
    collapse_hint: str = "auto"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "tree"


class TextBlock(BaseModel):
    """A free-form text block element."""

    content: str = ""
    wrap: bool = True
    indent: int = 0
    priority: str = "normal"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "text"

    @field_validator("indent")
    @classmethod
    def _validate_indent(cls, v: int) -> int:
        if v < 0:
            raise ValueError(f"indent must be non-negative, got {v}")
        return v


class CodeBlock(BaseModel):
    """A source-code block with optional syntax highlighting hints."""

    content: str = ""
    language: str | None = None
    line_numbers: bool = False
    highlight_lines: list[int] = Field(default_factory=list)
    priority: str = "normal"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "code"


class DiffLine(BaseModel):
    """A single line within a unified diff hunk.

    P2-1 fix (Luis review): renamed ``type`` → ``line_type`` to avoid
    shadowing the Python builtin ``type()``, consistent with the SD-21
    rationale for ``ColumnDef.col_type``.  The ``alias="type"`` preserves
    backward-compatible construction via ``DiffLine(type="add", ...)``.
    """

    model_config = ConfigDict(populate_by_name=True)

    line_type: str = Field(alias="type")  # "context", "add", "remove"
    content: str
    line_number_old: int | None = None
    line_number_new: int | None = None

    @field_validator("line_type")
    @classmethod
    def _validate_line_type(cls, v: str) -> str:
        valid = {"context", "add", "remove"}
        if v not in valid:
            raise ValueError(f"DiffLine.line_type must be one of {valid!r}, got {v!r}")
        return v


class DiffHunk(BaseModel):
    """A hunk within a unified diff."""

    header: str
    lines: list[DiffLine] = Field(default_factory=list)


class DiffBlock(BaseModel):
    """A unified-diff display element."""

    hunks: list[DiffHunk] = Field(default_factory=list)
    file_a: str | None = None
    file_b: str | None = None
    stats: dict[str, Any] | None = None
    priority: str = "normal"
    metadata: dict[str, Any] = Field(default_factory=dict)
    element_type: str = "diff"


class Separator(BaseModel):
    """A visual separator between elements."""

    style: str = "line"  # "line", "blank", "double"
    element_type: str = "separator"

    @field_validator("style")
    @classmethod
    def _validate_style(cls, v: str) -> str:
        valid = {"line", "blank", "double"}
        if v not in valid:
            raise ValueError(f"Separator.style must be one of {valid!r}, got {v!r}")
        return v


class ActionHint(BaseModel):
    """Suggested next-step CLI commands."""

    commands: list[str] = Field(default_factory=list)
    description: str = ""
    element_type: str = "action_hint"


# Union of all element snapshot types used by handles.
ElementSnapshot = (
    Panel
    | Table
    | StatusMessage
    | ProgressIndicator
    | Tree
    | TextBlock
    | CodeBlock
    | DiffBlock
    | Separator
    | ActionHint
)


# ---------------------------------------------------------------------------
# Element events
# ---------------------------------------------------------------------------


class ElementEvent(BaseModel):
    """Base class for all events emitted by element handles."""

    event_type: str
    handle_id: str
    element_kind: str
    timestamp: float = Field(default_factory=time.monotonic)


class ElementCreated(ElementEvent):
    """Emitted when a new element handle is created."""

    declaration_index: int = 0
    initial_state: ElementSnapshot | None = None


class ElementUpdated(ElementEvent):
    """Emitted when data is written to an element handle."""

    update_type: str = ""
    delta: dict[str, Any] = Field(default_factory=dict)
    element_snapshot: ElementSnapshot | None = None


class ElementClosed(ElementEvent):
    """Emitted when an element handle is closed."""

    final_state: ElementSnapshot | None = None


class SessionEnd(ElementEvent):
    """Emitted when the session itself is closed."""

    exit_code: int = 0
