"""TtySink -- render the session's lifecycle lines onto the terminal.

Every emit site carries its literal line in ``data["message"]``; the sink
prints it verbatim (no markup, no wrapping, so piped output and golden tests
stay byte-identical) with a light touch of rich styling when the stream is
an interactive terminal: the ``[spens]`` token is highlighted, warnings turn
yellow and errors red.  On non-terminal streams rich emits no escape codes
at all, so nothing changes for pipes and captured output.

The one structured exception is the terminal ``finished`` event: it carries
the machine-readable ``summary`` dict (written verbatim to ``events.jsonl``
for jsonl consumers), and the sink renders it as the session-recap panel
via :mod:`spens.ui`.  ``agent_output`` chunks are the output itself and are
written raw; in tty *interactive* mode agent output never passes through the
sink at all (the agent's docker ``-it`` stream is passed through to the
terminal directly).
"""

from __future__ import annotations

import sys
from typing import IO

from rich.text import Text

from spens import ui
from spens.events import Event

#: Style applied to the leading ``[spens]`` token per event type.
_PREFIX_STYLES = {
    "warning": "bold yellow",
    "error": "bold red",
    "canceled": "bold yellow",
}


class TtySink:
    def __init__(self, stream: IO[str] | None = None) -> None:
        self.stream = stream if stream is not None else sys.stdout
        self.console = ui.new_console(self.stream)

    def on_event(self, event: Event) -> None:
        if event.event == "agent_output":
            # Agent output is the agent's own stream -- always verbatim.
            chunk = event.data.get("chunk")
            if chunk:
                self.stream.write(f"{chunk}\n")
            return

        if event.event == "finished":
            summary = event.data.get("summary")
            if isinstance(summary, dict):
                # The event's exit_code is authoritative (it comes from the
                # agent container directly); a summary that could not read
                # it back from the audit state still renders the verdict.
                if "exit_code" not in summary and "exit_code" in event.data:
                    summary = {**summary, "exit_code": event.data["exit_code"]}
                ui.print_summary(self.console, summary)

        text = event.data.get("message")
        if text:
            # The message is the complete literal line; a leading newline
            # (e.g. the SIGINT notice) is part of it.  no_wrap + overflow
            # "ignore" keep long lines verbatim regardless of terminal width.
            line = Text(text, no_wrap=True, overflow="ignore")
            style = _PREFIX_STYLES.get(event.event, "bold cyan")
            start = text.find("[spens]")
            if start != -1:
                line.stylize(style, start, start + len("[spens]"))
            elif event.event in _PREFIX_STYLES:
                line.stylize(style)
            # soft_wrap: render the line exactly as-is -- rich must never
            # wrap or crop a spens message at the console width.
            self.console.print(line, soft_wrap=True)

    def close(self) -> None:
        pass
