"""ConsoleRenderer: owns all console output — lifecycle panels and content blocks.

Handles **all** inter-block whitespace, lifecycle panel printing, and content
block rendering so that the caller (``stream_handler``) never needs to call
``console.print()`` directly.
"""

from __future__ import annotations

from datetime import datetime

from rich.console import Console
from rich.text import Text

from yera.console.block_renderers import (
    render_action,
    render_buttons,
    render_date,
    render_input_request,
    render_markdown,
    render_slider,
    render_spinner,
    render_table,
)
from yera.console.panels import agent_panel, exit_panel, startup_panel
from yera.console.render_result import AwaitUserResult, BlockResult, InformationalResult
from yera.events.models import (
    ActionData,
    BlockData,
    ButtonsData,
    DatePickerData,
    ExitEventData,
    InputRequestData,
    MarkdownData,
    SliderData,
    SpinnerData,
    TableData,
)
from yera.events.models.out_event import OutputEvent


class ConsoleRenderer:
    """Owns all console output: lifecycle panels and content blocks.

    Uses type-based match on BlockData for dispatch.  Returns collected user
    input (if any) so the caller can decide how to handle it — no event stream
    side effects.

    Whitespace contract
    -------------------
    * **Startup panel** — printed first with no preceding whitespace.
    * **Agent panel** — preceded by a blank line (terminating any pending
      content line first) and followed by a blank line before content.
    * **Between content blocks** — a blank line is emitted whenever the
      ``block_id`` changes (i.e. a new block starts).
    * **Markdown streaming** — successive chunks with the *same* ``block_id``
      are printed with ``end=""`` so they concatenate on one line.
    * **Non-markdown informational blocks** (tables, actions, …) — printed with
      the default ``end`` (newline) which terminates the line.
    * **Await-user blocks** — the prompt prints with ``end=""`` (the line
      is left unterminated), so ``_pending_newline`` is set to ``True``.
    * **Exit panel** — :meth:`print_exit` terminates any pending content line,
      emits a blank separator, prints the panel, and adds a trailing blank line.
    """

    def __init__(self, console: Console, *, interactive: bool = False) -> None:
        """Initialise with the console to render to.

        Args:
            console: The Rich console instance used for all output.
            interactive: Whether stdin is an interactive terminal.  When
                ``True``, the renderer assumes that user input prompts are
                line-terminated by the terminal's echo of the Enter keystroke,
                so no extra newline is needed afterwards.
        """
        self._console = console
        self._interactive = interactive
        self._last_block_id: str | None = None
        self._pending_newline: bool = False
        self._current_agent_id: str | None = None

    # ------------------------------------------------------------------
    # Lifecycle panels
    # ------------------------------------------------------------------

    def print_startup(
        self, start_time: datetime, top_level_agent_name: str | None = None
    ) -> None:
        """Print the Startup panel.  No surrounding whitespace — it is the first output."""
        self._console.print(startup_panel(start_time, top_level_agent_name))

    def print_agent(self, agent_id: str) -> None:
        """Print the Agent Identity panel if the agent has changed.

        No-op when called with the same ``agent_id`` that is already current.
        Otherwise terminates any pending content line, emits a blank-line
        separator, prints the panel, then emits another blank line before
        subsequent content.  Handles both the first-agent case (after Startup)
        and subsequent-agent transitions.
        """
        if agent_id == self._current_agent_id:
            return
        if self._pending_newline:
            self._console.print()  # terminate unterminated content line
            self._pending_newline = False
        self._console.print()  # blank line before panel
        self._console.print(agent_panel(agent_id))
        self._console.print()  # blank line after panel
        self._current_agent_id = agent_id
        self._last_block_id = (
            None  # reset so render() doesn't add a redundant separator
        )

    def print_exit(
        self,
        exit_data: ExitEventData,
        run_time_seconds: float,
        failing_agent_name: str | None,
    ) -> None:
        """Finalise content output and print the Exit panel with a trailing blank line."""
        self.finalise()
        self._console.print(exit_panel(exit_data, run_time_seconds, failing_agent_name))
        self._console.print()  # trailing blank line

    # ------------------------------------------------------------------
    # Content blocks
    # ------------------------------------------------------------------

    def print_event(self, event: OutputEvent) -> str | None:
        """Render an output event to the console.

        Handles between-block blank lines, markdown streaming (``end=""`` so
        chunks stay on one line), and line termination for non-markdown blocks.
        The caller must **not** emit any extra newlines after calling this.
        """
        is_new_block = (
            event.block_id != self._last_block_id and self._last_block_id is not None
        )
        if is_new_block:
            self._end_previous_block()

        result = self._render_block(event.data)

        match result:
            case InformationalResult(renderable=renderable):
                if event.block_type == "markdown":
                    self._console.print(renderable, end="")
                    self._pending_newline = True
                else:
                    self._console.print(renderable)
                    self._pending_newline = False
                self._last_block_id = event.block_id
                return None

            case AwaitUserResult(collect_input=collect_input):
                user_input = collect_input(self._console)
                # In interactive terminals the user's Enter keystroke
                # terminates the prompt line; in piped mode it stays
                # unterminated.
                self._pending_newline = not self._interactive
                self._last_block_id = event.block_id
                return user_input

    def finalise(self) -> None:
        """Terminate any pending line and emit a blank line.

        Called by :meth:`print_exit` before the Exit panel, or can be called
        directly if the caller needs to insert a gap after the last content
        block.
        """
        if self._pending_newline:
            self._console.print()  # terminate unterminated line
        self._console.print()  # blank line before Exit panel

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _end_previous_block(self) -> None:
        """Terminate the previous block's line (if needed) and add a blank-line separator."""
        if self._pending_newline:
            self._console.print()  # terminate unterminated line
            self._pending_newline = False
        self._console.print()  # blank line between blocks

    @staticmethod
    def _render_block(data: BlockData) -> BlockResult:
        """Dispatch to the appropriate block renderer based on data type."""
        match data:
            case MarkdownData():
                return render_markdown(data)
            case ActionData():
                return render_action(data)
            case SpinnerData():
                return render_spinner(data)
            case TableData():
                return render_table(data)
            case InputRequestData():
                return render_input_request(data)
            case ButtonsData():
                return render_buttons(data)
            case SliderData():
                return render_slider(data)
            case DatePickerData():
                return render_date(data)
            case _:
                return InformationalResult(
                    renderable=Text(data.model_dump_json(indent=2))
                )
