"""Single-task row widget for the Task Center.

Three lines per row. The head line uses the same ``pill()`` treatment
as the model cards so the screen matches the rest of the app; the
left rail carries the state color and pulses at 1 Hz for the active
row. The widget is pure-presentation: ``update(task, tick)`` writes
the three labels from a ``Task`` snapshot. ``TaskCenter._poll`` calls
it at 10 Hz.
"""

from __future__ import annotations

from time import monotonic

from textual.app import ComposeResult
from textual.content import Content
from textual.widget import Widget
from textual.widgets import Label, Static

from lilbee.cli.tui.pill import pill
from lilbee.cli.tui.task_queue import Task, TaskStatus, TaskType
from lilbee.cli.tui.widgets.progress_cell import (
    frozen_indeterminate_cell,
    indeterminate_cell,
    progress_cell,
)

# ~1.7 Hz rail pulse at a 10 Hz poll cadence = 3 ticks on, 3 off.
# Faster cadence than the original 1 Hz makes 'something is happening'
# visibly obvious at a glance (bb-18y3).
_PULSE_HALF_TICKS = 3

_STATUS_CLASS: dict[TaskStatus, str] = {
    TaskStatus.QUEUED: "-queued",
    TaskStatus.ACTIVE: "-active",
    TaskStatus.DONE: "-done",
    TaskStatus.FAILED: "-failed",
    TaskStatus.CANCELLED: "-cancelled",
}

_STATUS_CLASSES: tuple[str, ...] = tuple(_STATUS_CLASS.values())

# Pill palette: background color per task type. Sync/add/remove share
# $secondary (data-mutating ops), download uses $accent (network), wiki
# uses $warning (CPU-heavy generation), crawl uses $primary (external).
_TASK_TYPE_BG: dict[str, str] = {
    TaskType.DOWNLOAD.value: "$accent",
    TaskType.SYNC.value: "$secondary",
    TaskType.ADD.value: "$secondary",
    TaskType.REMOVE.value: "$secondary",
    TaskType.CRAWL.value: "$primary",
    TaskType.WIKI.value: "$warning",
    TaskType.SETUP.value: "$warning-darken-1",
}
_TASK_TYPE_BG_FALLBACK = "$primary"

# Pill palette: status badge. QUEUED is muted so only the running ones
# pop; DONE / FAILED / CANCELLED use brightened backgrounds so terminal
# states stand out against the matching left-rail color.
_STATUS_BG: dict[TaskStatus, str] = {
    TaskStatus.QUEUED: "$surface-lighten-2",
    TaskStatus.ACTIVE: "$primary",
    TaskStatus.DONE: "$success-lighten-2",
    TaskStatus.FAILED: "$error-lighten-2",
    TaskStatus.CANCELLED: "$warning-lighten-2",
}


_TERMINAL_STATUSES: frozenset[TaskStatus] = frozenset(
    {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
)


def _build_head(task: Task, elapsed: str) -> Content:
    """Build the top line: name + type pill + status pill, elapsed trailing.

    Kept as a module-level helper so tests can exercise the pill
    composition directly without spinning up the full widget tree.
    """
    type_bg = _TASK_TYPE_BG.get(task.task_type, _TASK_TYPE_BG_FALLBACK)
    status_bg = _STATUS_BG[task.status]
    status_fg = "$text bold" if task.status in _TERMINAL_STATUSES else "$text"
    parts = [
        Content.styled(task.name, "bold"),
        Content(" "),
        pill(task.task_type, type_bg, "$text"),
        Content(" "),
        pill(task.status.value, status_bg, status_fg),
    ]
    if elapsed:
        parts.append(Content(" "))
        parts.append(Content.styled(elapsed, "dim"))
    return Content.assemble(*parts)


def _format_elapsed(task: Task) -> str:
    """Return elapsed time as MM:SS, a status tag, or empty.

    Terminal states (DONE / FAILED / CANCELLED) freeze at ``completed_at``
    so the timer doesn't keep climbing for rows that are just waiting out
    their 2-second flash before removal.
    """
    if task.status == TaskStatus.QUEUED:
        return "queued"
    if task.started_at is None:
        return ""
    end = task.completed_at if task.completed_at is not None else monotonic()
    seconds = max(0, int(end - task.started_at))
    mm, ss = divmod(seconds, 60)
    return f"{mm:02d}:{ss:02d}"


class TaskRow(Widget, can_focus=True):
    """One task, rendered as three stacked lines.

    Focusable so ``Tab`` / ``j`` / ``k`` in the Task Center moves between
    rows and ``c`` cancels the focused task.
    """

    DEFAULT_CSS = ""  # all styling lives in task_center.tcss

    def __init__(self, task_id: str, **kwargs: object) -> None:
        super().__init__(id=f"task-{task_id}", **kwargs)  # type: ignore[arg-type]
        self._task_id = task_id

    def compose(self) -> ComposeResult:
        # Widget with yielded children lays them out vertically by default.
        # An explicit Vertical wrapper would inherit ``height: 1fr`` and
        # stretch each row to fill the scroll viewport, painting the
        # border-left rail down the whole empty stretch.
        yield Label("", id="row-head", classes="row-head")
        yield Label("", id="row-meta", classes="row-meta")
        yield Static("", id="row-bar", classes="row-bar")

    def update(self, task: Task, tick: int) -> None:
        """Re-render from a Task snapshot. Safe to call every poll tick.

        Quietly no-ops until the row's child labels have mounted, so the
        first few poll ticks (before compose settles) don't error.
        """
        # State class: exactly one of the 5 modifier classes is active.
        target_class = _STATUS_CLASS.get(task.status, "")
        for cls in _STATUS_CLASSES:
            self.set_class(cls == target_class, cls)
        # 1 Hz rail pulse on the active row only.
        self.set_class(
            task.status == TaskStatus.ACTIVE and (tick // _PULSE_HALF_TICKS) % 2 == 0,
            "-pulse",
        )

        try:
            head = self.query_one("#row-head", Label)
            meta = self.query_one("#row-meta", Label)
            bar = self.query_one("#row-bar", Static)
        except Exception:
            return  # compose hasn't finished; retry on next poll

        elapsed = _format_elapsed(task)
        head.update(_build_head(task, elapsed))

        # A DONE task's detail is whatever the last progress tick wrote
        # ("442/610 MB", "Syncing foo.md..."): stale and confusing now that the
        # bar reads 100%. FAILED / CANCELLED keep their detail: it's the reason.
        is_done = task.status == TaskStatus.DONE
        detail = "" if is_done else (task.detail or "")
        pct = "" if task.indeterminate or is_done else f"[b]{task.progress:.1f}%[/b]"
        meta.update("  ".join(p for p in (detail, pct) if p))

        if task.indeterminate:
            # Terminal rows freeze the bar so a cancelled/failed/done
            # task doesn't keep reading as live work.
            if task.status in _TERMINAL_STATUSES:
                bar.update(frozen_indeterminate_cell())
            else:
                bar.update(indeterminate_cell(tick))
        else:
            bar.update(progress_cell(task.progress))

    def flash_completed(self) -> None:
        """Mark the row as 'just completed' for a 2-second visual flash."""
        self.add_class("-just-completed")
