"""Chat panel widget for the TUI."""

from __future__ import annotations

import json
from typing import Any

from rich.console import Group, RenderableType
from rich.text import Text
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Static

from clients.terminal.tui.chat_model import ChatItem, ChatModel
from clients.terminal.tui.renderers import default_registry
from clients.terminal.tui.settings import get_tui_settings
from clients.terminal.tui.themes import get_active_theme


def _item_msg(item: ChatItem) -> dict[str, Any]:
    """Map a ChatItem to the msg dict its renderer expects."""
    kind = item.kind
    if kind == "user_message":
        return {"type": "user_message", "content": item.content}
    if kind == "assistant_message":
        return {"type": "assistant_message", "content": item.content}
    if kind == "thinking_block":
        return {
            "type": "thinking_block",
            "content": item.content,
            "is_subagent": item.meta.get("is_subagent", False),
        }
    if kind == "tool_started":
        return {"type": "tool_started", **item.meta}
    if kind == "tool_call":
        return {"type": "tool_call", **item.meta}
    if kind == "error":
        return {"type": "error", "message": item.content}
    if kind == "status":
        return {"type": "status", "content": item.content}
    if kind == "planning_status":
        return {
            "type": "planning_status",
            "label": item.content,
            "phase": item.meta.get("phase"),
            "is_subagent": item.meta.get("is_subagent", False),
        }
    if kind == "plan_ready":
        return {
            "type": "plan_ready",
            "plan": item.content,
            "is_subagent": item.meta.get("is_subagent", False),
        }
    if kind == "turn_meta":
        return {"type": "turn_meta", "elapsed_seconds": item.meta.get("elapsed_seconds")}
    return {"type": "plain", "content": item.content}


def _signature(msg: dict[str, Any]) -> str:
    """Stable serialized form of a renderer msg, for cache invalidation.

    Only an item whose rendered inputs changed should miss the cache; this
    captures content + meta so in-place mutations (e.g. a streaming assistant
    bubble growing token by token, or a rolling planning_status line) invalidate
    exactly their own entry.
    """
    return json.dumps(msg, sort_keys=True, default=str)


class ChatPanel(ScrollableContainer):
    """Scrollable conversation panel."""

    DEFAULT_CSS = """
    ChatPanel {
        border: solid $tui-border;
        background: $tui-surface;
        color: $tui-text;
        padding: 0 1;
        height: 1fr;
        width: 2fr;
    }
    ChatPanel Static {
        color: $tui-text;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._model = ChatModel()
        self._registry = default_registry()
        self._items_container = Static("")
        # Renderable cache keyed by id(item) -> (signature, renderable). On a
        # per-token stream_delta only the streaming item's signature changes,
        # so every other visible item is reused from here instead of being
        # rebuilt (and re-parsed as markdown) on every token.
        self._chat_render_cache: dict[int, tuple[str, RenderableType]] = {}

    def compose(self) -> ComposeResult:
        yield self._items_container

    def on_mount(self) -> None:
        self._items_container.styles.height = "auto"

    def add_user_message(self, text: str) -> None:
        self._model.add_user_message(text)
        self._refresh()

    def load_history(self, messages: list[dict]) -> None:
        """Populate the chat panel with a session's stored messages on resume."""
        self._model.load_history(messages)
        self._chat_render_cache.clear()
        self._refresh()

    def handle_ws_event(self, msg: dict) -> None:
        self._model.handle_ws_event(msg)
        self._refresh()

    def clear(self) -> None:
        """Reset the chat model and redraw an empty conversation."""
        self._model.items.clear()
        self._model._current_assistant = None
        self._model._current_thinking = None
        self._chat_render_cache.clear()
        self._refresh()

    def _max_visible_items(self) -> int:
        return get_tui_settings().max_visible_items

    def _truncation_hint(self, hidden: int) -> RenderableType:
        theme = get_active_theme()
        return Text(
            f"… {hidden} earlier messages not shown",
            style=theme.text.hex,
        )

    def _refresh(self) -> None:
        items = self._model.items
        limit = self._max_visible_items()
        hidden = max(0, len(items) - limit)
        # Slice the last `limit` items when over the cap; otherwise keep all.
        visible = items[-limit:] if hidden else items

        renderables: list[RenderableType] = []
        if hidden:
            renderables.append(self._truncation_hint(hidden))

        seen_ids: set[int] = set()
        for item in visible:
            seen_ids.add(id(item))
            msg = _item_msg(item)
            sig = _signature(msg)
            cached = self._chat_render_cache.get(id(item))
            if cached is not None and cached[0] == sig:
                renderables.append(cached[1])
            else:
                rendered = self._registry.render(msg)
                self._chat_render_cache[id(item)] = (sig, rendered)
                renderables.append(rendered)

        # Drop cache entries for items no longer in the model so memory stays
        # bounded by the live history, not by everything ever shown.
        stale = [key for key in self._chat_render_cache if key not in seen_ids]
        for key in stale:
            self._chat_render_cache.pop(key, None)

        self._items_container.update(Group(*renderables))
        self.scroll_end(animate=False)