"""Renderers for the ``todo`` tool — the plan tracker.

The default ``ToolStartedRenderer`` dumps the call args as raw JSON, which is
unreadable for a tool as central as todo (and for ``set`` produces a wall of
quoted strings). These renderers turn the call into a compact, op-aware card
and collapse the result into a single status line — the full live plan already
lives in the side panel (``TodoList``), so the chat card only needs to convey
*what changed*, not the whole state.

Pattern mirrors ``subagent.py``: registered before the generic tool renderers.
"""

from __future__ import annotations

import json

from rich.box import ROUNDED
from rich.console import RenderableType
from rich.json import JSON as RichJSON
from rich.panel import Panel
from rich.text import Text

from clients.terminal.tui.themes import get_active_theme

from .base import ContentRenderer

_TODO_TOOL = "todo"

# Same markers as the side panel (todo_list.py) so the chat card and the panel
# read as one vocabulary.
_ICON = {"pending": "○", "in_progress": "◎", "done": "✓", "failed": "✗", "skipped": "—"}
# Validation status text colour by the new status (used in update cards).
_VAL_COLOR = {
    "done": "success",
    "failed": "error",
    "in_progress": "accent",
    "skipped": "text_dim",
    "pending": "text_dim",
}


def _truncate(text: str, limit: int) -> str:
    text = (text or "").strip()
    if len(text) <= limit:
        return text
    return text[:limit].rstrip() + " …"


class TodoStartedRenderer(ContentRenderer):
    """Render a ``todo`` call start as a compact, op-aware card."""

    def accepts(self, msg: dict) -> bool:
        return msg.get("type") == "tool_started" and msg.get("tool") == _TODO_TOOL

    def render(self, msg: dict) -> RenderableType:
        theme = get_active_theme()
        args = msg.get("args") or {}
        op = args.get("op")
        accent = theme.accent.hex

        body = self._body(msg, theme)
        title = self._title(op, args, accent)
        panel = Panel(
            body,
            title=title,
            title_align="left",
            border_style=theme.tool_border.hex,
            box=ROUNDED,
        )
        if bool(msg.get("is_subagent", False)):
            from rich.padding import Padding

            return Padding(panel, (0, 0, 0, 2))
        return panel

    def _title(self, op: str | None, args: dict, accent: str) -> str:
        if op == "set":
            n = len(args.get("tasks") or [])
            return f"→ todo · set plan ({n})"
        if op == "update":
            idx = args.get("index")
            status = args.get("status", "")
            arrow = f" → {status}" if status else ""
            return f"→ todo · #{idx}{arrow}"
        if op == "view":
            return "→ todo · view"
        if op == "clear":
            return "→ todo · clear"
        return f"→ todo · {op or '?'}"

    def _body(self, msg: dict, theme) -> RenderableType:
        args = msg.get("args") or {}
        op = args.get("op")
        dim = theme.text_dim.hex
        muted = theme.text_muted.hex

        if op == "set":
            tasks = args.get("tasks") or []
            if not tasks:
                return Text("(empty plan)", style=dim)
            lines = Text()
            for i, t in enumerate(tasks, 1):
                if i > 1:
                    lines.append("\n")
                lines.append(f"  {_ICON['pending']} ", style=dim)
                lines.append(f"{i}. {_truncate(str(t), 80)}", style=muted)
            return lines

        if op == "update":
            status = args.get("status", "")
            validation = (args.get("validation") or "").strip()
            # The step text comes from ToolStarted.metadata (enriched on the
            # backend by loading the plan before the tool runs — the LLM's
            # update args carry only the index). Absent on history replay.
            step_text = ""
            meta = msg.get("metadata")
            if isinstance(meta, dict):
                step_text = (meta.get("step_text") or "").strip()
            body = Text()
            has_content = False
            if step_text:
                body.append(_truncate(step_text, 100), style=theme.text.hex)
                has_content = True
            if validation:
                color_attr = _VAL_COLOR.get(status, "text_dim")
                color = getattr(theme, color_attr, theme.text_dim).hex
                if has_content:
                    body.append("\n")
                body.append("verified: ", style=dim)
                body.append(_truncate(validation, 160), style=color)
                has_content = True
            if not has_content:
                # No step text (history replay) and no validation — keep the
                # placeholder so the card isn't empty.
                body.append("no validation", style=dim)
            return body

        if op in ("view", "clear"):
            return Text("")

        # Unknown op — fall back to readable JSON so the call is still legible.
        try:
            return RichJSON(json.dumps(args))
        except Exception:
            return Text(str(args), style=dim)


class TodoResultRenderer(ContentRenderer):
    """Render a ``todo`` result as a single compact status line.

    The full plan is in the side panel; the chat card only needs the summary.
    The result text from ``todo._render`` starts with ``Plan — X/N done:``;
    we parse that. Other result strings (``Plan is empty.``, ``Plan cleared.``,
    ``No plan set for this session.``) map to short labels.
    """

    def accepts(self, msg: dict) -> bool:
        return msg.get("type") == "tool_call" and msg.get("tool") == _TODO_TOOL

    def render(self, msg: dict) -> RenderableType:
        theme = get_active_theme()
        success = msg.get("success", True)
        result = msg.get("result")
        text = str(result) if result is not None else ""

        if bool(msg.get("is_subagent", False)):
            from rich.padding import Padding

            return Padding(self._card(success, text, theme), (0, 0, 0, 2))
        return self._card(success, text, theme)

    def _card(self, success: bool, text: str, theme) -> Panel:
        if not success:
            # e.g. validation_required — surface the first line compactly.
            first = text.splitlines()[0].strip() if text else "error"
            color = theme.tool_error
            return Panel(
                Text(_truncate(first, 120), style=color.hex),
                title="← todo ✗",
                title_align="left",
                border_style=color.hex,
                box=ROUNDED,
            )

        # The step text lives in the *call* card (started, via metadata); the
        # result card only carries the new overall state — one compact line.
        label = self._status_label(text)
        return Panel(
            self._status_line(label, theme),
            title="← todo ✓",
            title_align="left",
            border_style=theme.tool_success.hex,
            box=ROUNDED,
        )

    def _status_label(self, text: str) -> str:
        first = text.splitlines()[0].strip() if text else ""
        # "Plan — 2/5 done:"
        if first.startswith("Plan —") and "done" in first:
            tail = first[len("Plan —"):].rstrip(":").strip()  # "2/5 done"
            return f"plan · {tail}"
        if first == "Plan is empty.":
            return "plan · empty"
        if first == "Plan cleared.":
            return "plan · cleared"
        if first == "No plan set for this session.":
            return "plan · no plan"
        return _truncate(first, 80) or "plan · ok"

    def _status_line(self, label: str, theme) -> Text:
        dim = theme.text_dim.hex
        if label.startswith("plan · ") and "/" in label:
            # "plan · 2/5 done" → prefix dim, "2" success, "/5 done" dim.
            body = Text("plan · ", style=dim)
            rest = label[len("plan · "):]  # "2/5 done"
            slash = rest.find("/")
            done_n = rest[:slash]
            after = rest[slash:]  # "/5 done"
            body.append(done_n, style=theme.success.hex)
            body.append(after, style=dim)
            return body
        return Text(label, style=dim)