"""Todo list side-panel widget for the TUI.

Renders the agent's current todo plan (the parent session's todo row) with
status-coloured markers and a progress header. Fed by:

  * ``todo_updated`` WS events (live, after each tool turn / planning auto-populate)
  * ``api.get_todos`` (initial seed on attach/switch, before the first WS event)

Sub-agent todos are NOT shown here yet — they live in isolated KV rows and will
be surfaced as nested sub-lists in Phase 3.

Layout: ``TodoPanel`` (a scrollable container, ``height: 1fr``) wraps a
``TodoList`` (a plain ``Static``, ``height: auto``). The info block above
shrinks to its content; the todo block fills the rest of the right column and
scrolls when the plan is longer than the viewport.
"""

from __future__ import annotations

from rich.markup import escape

from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Static

from clients.terminal.tui.themes import get_active_theme

_STATUS_ICON: dict[str, str] = {
    "pending":     "○",
    "in_progress": "◎",
    "done":        "✓",
    "failed":      "✗",
    "skipped":     "—",
}

# Theme attribute name (on the active theme) used to colour each status marker.
_STATUS_COLOR_ATTR: dict[str, str] = {
    "pending":     "text_dim",
    "in_progress": "accent",
    "done":        "success",
    "failed":      "error",
    "skipped":     "text_dim",
}


class TodoList(Static):
    """Renders the live todo plan as coloured, status-marked lines.

    A plain Static whose content is rebuilt from the latest task payload via
    ``set_tasks`` / ``clear`` / ``refresh_content`` (no compose override, and
    never a method named ``_render`` — that shadows Widget._render and crashes
    Textual's layout pass).
    """

    DEFAULT_CSS = """
    TodoList {
        height: auto;
        color: $tui-text-muted;
        padding: 0;
    }
    """

    def __init__(self) -> None:
        super().__init__("[dim]No plan yet[/dim]")
        self._tasks: list[dict] = []

    def set_tasks(self, tasks: list[dict]) -> None:
        """Replace the rendered todo. ``tasks`` is the REST/WS payload:
        ``[{"index", "text", "status", "validation", "milestone"}, ...]``.
        When any task carries a ``milestone`` label, the panel groups steps
        under ``Milestone <letter>`` headers; otherwise it renders flat
        (status-rank ordering)."""
        self._tasks = list(tasks)
        self.update(self._build_content())

    def clear(self) -> None:
        self._tasks = []
        self.update(self._build_content())

    def refresh_content(self) -> None:
        """Re-render with the current tasks (used after a theme switch so the
        status-marker colours pick up the new palette)."""
        self.update(self._build_content())

    # Display order: in_progress → pending → failed/skipped → done. Lower rank
    # = higher on screen. Unknown statuses fall in with pending so new work
    # stays visible rather than sinking below completed steps.
    _STATUS_RANK: dict[str, int] = {
        "in_progress": 0,
        "pending":     1,
        "failed":      2,
        "skipped":      2,
        "done":        3,
    }

    def _ordered_within(self, tasks: list[dict]) -> list[dict]:
        """Tasks grouped by status for display (in_progress → pending →
        failed/skipped → done), original order preserved within each rank.
        Does not mutate the input."""
        return sorted(
            tasks,
            key=lambda t: self._STATUS_RANK.get(t.get("status", "") or "pending", 1),
        )

    def _ordered_tasks(self) -> list[dict]:
        """All tasks, status-grouped for display (legacy flat path)."""
        return self._ordered_within(self._tasks)

    def _has_milestones(self) -> bool:
        return any((t.get("milestone") or "") for t in self._tasks)

    def _group_rank(self, tasks: list[dict]) -> int:
        """Lowest status-rank among a group's tasks — the group's *most active*
        step. Used to order groups the same way steps are ordered (active
        groups up, fully-done groups down). An empty group sinks like a
        fully-done one (rank 3)."""
        if not tasks:
            return self._STATUS_RANK["done"]
        return min(
            self._STATUS_RANK.get(t.get("status", "") or "pending", 1)
            for t in tasks
        )

    def _milestone_groups(self) -> list[tuple[str, list[dict]]]:
        """Group tasks by ``milestone`` and order groups by their most active
        step's status-rank (in_progress → pending → failed/skipped → fully
        done), so a finished milestone sinks below one with work in progress —
        the same "done to the bottom" rule that applies to individual steps.
        Python's sort is stable, so groups of equal rank keep the plan's
        first-appearance order. The unnamed group (``""``) takes part in the
        same ranking and renders without a header; it no longer forced to the
        end, so an active unnamed block can lead the panel while finished
        named milestones sink."""
        order: list[str] = []
        by_group: dict[str, list[dict]] = {}
        for t in self._tasks:
            ms = t.get("milestone") or ""
            if ms not in by_group:
                order.append(ms)
                by_group[ms] = []
            by_group[ms].append(t)
        groups = [(g, by_group[g]) for g in order]
        groups.sort(key=lambda gv: self._group_rank(gv[1]))
        return groups

    def _render_task_line(self, t: dict, theme, dim: str) -> str:
        status = t.get("status", "") or "pending"
        icon = _STATUS_ICON.get(status, "?")
        icon_color = getattr(theme, _STATUS_COLOR_ATTR.get(status, "text_dim"), theme.text_dim).hex
        # Escape user/LLM-supplied text before interpolating into a rich markup
        # string — a task text like "fix [bug]" would otherwise be parsed as a
        # markup tag and silently break the line's styling (or drop text).
        text = escape(str(t.get("text", "")))
        index_str = escape(str(t.get("index", "")))
        # Line emphasis: the active step is bold + bright; completed/skipped
        # are dimmed; failed pops in the warning colour; pending is muted.
        if status == "in_progress":
            text_style = f"bold {theme.text.hex}"
        elif status in ("done", "skipped"):
            text_style = f"dim {theme.text_muted.hex}"
        elif status == "failed":
            text_style = theme.warning.hex
        else:
            text_style = theme.text_muted.hex
        suffix = f" [{dim}]({status})[/]" if status not in ("pending", "done") else ""
        validation = t.get("validation")
        note = f" [{dim}]✓ {escape(str(validation))}[/]" if validation else ""
        return f"  [{icon_color}]{icon}[/] [{text_style}]{index_str}. {text}[/]{suffix}{note}"

    def _build_content(self) -> str:
        theme = get_active_theme()
        dim = theme.text_dim.hex
        if not self._tasks:
            return f"[{dim}]No plan yet[/]"
        n = len(self._tasks)
        done = sum(1 for t in self._tasks if t.get("status") == "done")
        # Progress header: count in the done colour, total dim.
        lines: list[str] = [
            f"[{theme.success.hex}]{done}[/{theme.success.hex}]/{n} done"
        ]
        if not self._has_milestones():
            # Flat path (legacy): status-rank ordering, no group headers.
            for t in self._ordered_tasks():
                lines.append(self._render_task_line(t, theme, dim))
            return "\n".join(lines)
        # Grouped path: milestone headers (✓ when the whole group is done/
        # skipped), status-rank ordering within each group. Groups are ordered
        # by their most active step (see _milestone_groups), so finished
        # milestones sink below ones with work in progress; the unnamed group
        # renders without a header wherever its rank places it.
        for ms, group_tasks in self._milestone_groups():
            if ms:
                group_done = all(
                    t.get("status") in ("done", "skipped") for t in group_tasks
                )
                check = f" [{theme.success.hex}]✓[/]" if group_done else ""
                lines.append(f"[{dim}]Milestone {escape(str(ms))}{check}[/]")
            for t in self._ordered_within(group_tasks):
                lines.append(self._render_task_line(t, theme, dim))
        return "\n".join(lines)


class TodoPanel(ScrollableContainer):
    """Right-column scrollable frame around the todo plan.

    ``height: 1fr`` so it fills whatever the info block above leaves free; the
    inner ``TodoList`` is ``height: auto`` and this container scrolls when the
    plan is longer than the viewport.
    """

    DEFAULT_CSS = """
    TodoPanel {
        border: solid $tui-border;
        background: $tui-panel;
        color: $tui-text-muted;
        padding: 0 1;
        height: 1fr;
        width: 1fr;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._todo = TodoList()

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

    def on_mount(self) -> None:
        self._todo.styles.height = "auto"
        self.border_title = "TODO"

    def set_tasks(self, tasks: list[dict]) -> None:
        self._todo.set_tasks(tasks)

    def clear(self) -> None:
        self._todo.clear()

    def refresh_content(self) -> None:
        self._todo.refresh_content()