"""TuiMaterializer -- bridges A2A event queue to Output Rendering Framework.

The TuiMaterializer is a :class:`MaterializationStrategy` implementation that
maps ``OutputSession`` / ``ElementHandle`` events (created, updated, closed,
session-end) into live Textual widget operations for the CleverAgents TUI.

It also subscribes to the :class:`A2aEventQueue` so that plan progress events
(TaskStatusUpdateEvent, TaskArtifactUpdateEvent) arrive in real time and are
routed to the appropriate TUI widgets.  Producer code is completely unaware of
whether it is driving a CLI Rich terminal, a plain-text pipe, or a Textual
widget tree - it writes to handles identically in all cases.

Widget mapping per ADR-044 ::

    +---------------------+----------------------------------+
    | ElementHandle Type  | Textual Widget                   |
    +---------------------+----------------------------------+
    | PanelHandle         | Static container + Collapsible   |
    | TableHandle         | DataTable                        |
    | TreeHandle          | Tree                             |
    | ProgressHandle      | ProgressBar / Throbber           |
    | StatusHandle        | Label (semantic CSS class)       |
    | CodeHandle          | Read-only TextArea               |
    | DiffHandle          | Custom DiffView widget           |
    | SeparatorHandle     | Rule                             |
    | ActionHintHandle    | Static (muted commands)          |
    +---------------------+----------------------------------+

Real-time A2A event subscription ::

    A2aEventQueue -> EventBusBridge -> TuiMaterializer widget routes

The materialiser handles all ElementHandle types from day one - partial
implementation would cause silent failures when CLI commands run in a TUI
context (ADR-044 requirement).

Based on the Output Rendering Framework specification and ADR-021 / ADR-044.
"""

from __future__ import annotations

import contextlib
import threading
import time
from typing import Any, ClassVar

from cleveragents.cli.output.handles._models import (
    ActionHint,
    CodeBlock,
    DiffBlock,
    DiffHunk,
    DiffLine,
    ElementClosed,
    ElementCreated,
    ElementUpdated,
    Panel,
    ProgressIndicator,
    Separator,
    SessionEnd,
    StatusMessage,
    Table,
    TextBlock,
    Tree,
    TreeNode,
)

try:
    from textual.widgets import Static  # type: ignore[import-untyped]  # noqa: F401

    _A2A_AVAILABLE = True
except ImportError:
    _A2A_AVAILABLE = False


# ---------------------------------------------------------------------------
# Reactive element mapping -- each handle kind produces a TUI widget tree
# ---------------------------------------------------------------------------


class _TuiWidget:
    """Lightweight descriptor tracking one TUI widget instance and its
    associated element handle.

    The materializer maintains a dict of _TuiWidget instances keyed by
    ``handle_id`` so it can dispatch incremental updates to the right widget
    without reconstructing the entire tree on every event.
    """

    __slots__ = ("closed", "declared_at", "element_type", "handle_id", "widget")

    def __init__(
        self,
        widget: Any,
        handle_id: str,
        element_type: str,
        declared_at: float,
    ) -> None:
        self.widget = widget
        self.handle_id = handle_id
        self.element_type = element_type
        self.declared_at = declared_at
        self.closed = False


# ---------------------------------------------------------------------------
# Reactive panel builder -- maps Panel/PanelEntry -> TUI widgets
# ---------------------------------------------------------------------------


def _build_panel_display(widget: Any, panel: Panel) -> None:
    """Render a Panel into an existing TUI widget."""
    if not _A2A_AVAILABLE:  # pragma: no cover - Textual fallback path
        return

    entries_text: list[str] = []
    for entry in panel.entries:
        icon_str = f"{entry.icon} " if entry.icon else ""
        key_label = (
            f" {icon_str}{entry.key}: {entry.value}"
            if entry.style_hint != "error"
            else f" :material-check_circle: {icon_str}{entry.key}: {entry.value}"
        )
        entries_text.append(key_label)

    rendered = f"[title]{panel.title}[/title]\n" + "\n".join(entries_text)

    if hasattr(widget, "label"):
        widget.label = panel.title
    if hasattr(widget, "_static"):
        widget._static.update(rendered)


# ---------------------------------------------------------------------------
# Reactive table builder -- maps Table rows -> DataTable widgets
# ---------------------------------------------------------------------------


def _init_table_display(widget: Any, table: Table) -> None:
    """Populate a DataTable widget headers and initial rows."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    for col in table.columns:
        with contextlib.suppress(Exception):
            widget.add_column(col.name)

    for row in table.rows:
        with contextlib.suppress(TypeError):
            widget.add_row(*[str(v) for v in row.values()])


def _add_table_row(widget: Any, row_dict: dict[str, Any]) -> None:
    """Append a single row to the DataTable."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return
    with contextlib.suppress(TypeError):
        widget.add_row(*[str(v) for v in row_dict.values()])


# ---------------------------------------------------------------------------
# Reactive tree builder -- maps Tree -> textual.widgets.Tree widgets
# ---------------------------------------------------------------------------


def _populate_tree(widget: Any, tree: Tree) -> None:
    """Add a TreeNode hierarchy to a Textual Tree widget."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    def _add_node(parent_label: str, children_list: list[TreeNode]) -> None:
        if not children_list:
            return
        for child in children_list:
            try:
                node = widget.add_node(
                    parent_label, label=child.label, collapsed=child.collapsed
                )
                _add_node(node, child.children)
            except Exception:
                pass

    if not hasattr(widget, "_added"):
        _add_node("root", [tree.root])


# ---------------------------------------------------------------------------
# Reactive progress indicator -- maps ProgressIndicator -> ProgressBar/Throbber
# ---------------------------------------------------------------------------


def _update_progress(widget: Any, status_msg: StatusMessage) -> None:
    """Write a status message to the progress widget."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return
    with contextlib.suppress(AttributeError):
        widget.update_label(status_msg.message)


# ---------------------------------------------------------------------------
# Status message display -- maps StatusMessage -> Label with CSS class
# ---------------------------------------------------------------------------


def _display_status(widget: Any, status_message: StatusMessage) -> None:
    """Render a StatusMessage as a styled label."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    widget.update(status_message.message)

    css_classes = {
        "ok": "status--ok",
        "info": "status--info",
        "warn": "status--warn",
        "error": "status--error",
    }
    cls = css_classes.get(status_message.level, "")
    if cls:
        widget.add_class(cls)


# ---------------------------------------------------------------------------
# Code block display -- maps CodeBlock -> read-only TextArea
# ---------------------------------------------------------------------------


def _render_code(widget: Any, code_block: CodeBlock) -> None:
    """Set the content of a read-only TextArea."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    try:
        widget.read_only = True
        if hasattr(widget, "text"):
            widget.text = code_block.content
        elif hasattr(widget, "update"):
            widget.update(code_block.content)
    except Exception:
        pass


# ---------------------------------------------------------------------------
# Diff display -- maps DiffBlock -> custom DiffView widgets
# ---------------------------------------------------------------------------


class _DiffViewStub:
    """Stubs for the DiffView widget when Textual is not fully available."""

    def __init__(self, file_a: str | None = None, file_b: str | None = None) -> None:
        self.file_a = file_a
        self.file_b = file_b
        self._hunks: list[str] = []

    def add_hunk(self, hunk: DiffHunk) -> None:
        lines = [hunk.header]
        for line in hunk.lines:
            if line.line_type == "add":
                prefix = "+"
            elif line.line_type == "remove":
                prefix = "-"
            else:
                prefix = " "
            lines.append(f"{prefix} {line.content}")
        self._hunks.append("\n".join(lines))

    @property
    def rendered_text(self) -> str:
        return "\n\n".join(self._hunks)


def _render_diff(widget: Any, diff_block: DiffBlock) -> None:
    """Render a DiffBlock into its Display widget."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    for hunk in diff_block.hunks:
        if hasattr(widget, "add_hunk"):
            widget.add_hunk(hunk)


# ---------------------------------------------------------------------------
# Separator display -- maps Separator -> Rule widgets
# ---------------------------------------------------------------------------


def _render_separator(widget: Any, sep: Separator) -> None:
    """Apply the rule style."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return
    rule_styles = {"line": "=", "blank": "", "double": "="}
    char = rule_styles.get(sep.style, "-")
    if hasattr(widget, "label"):
        widget.label = char * 30


# ---------------------------------------------------------------------------
# Action hint display -- maps ActionHint -> Static with muted commands
# ---------------------------------------------------------------------------


def _render_action_hint(widget: Any, hint: ActionHint) -> None:
    """Render command suggestions in a muted format."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return
    cmd_text = ", ".join(f"`{cmd}`" for cmd in hint.commands[:5])
    prefix = f"{hint.description}: " if hint.description else ""
    widget.update(f"{prefix}{cmd_text}")


# ---------------------------------------------------------------------------
# Text block display -- maps TextBlock -> Static text
# ---------------------------------------------------------------------------


def _render_text(widget: Any, text_block: TextBlock) -> None:
    """Set the text content."""
    if not _A2A_AVAILABLE:  # pragma: no cover
        return

    leading = " " * text_block.indent if text_block.indent else ""
    rendered = f"{leading}{text_block.content}"
    with contextlib.suppress(Exception):
        widget.update(rendered)


# ---------------------------------------------------------------------------
# Reactive A2A event subscription helper
# ---------------------------------------------------------------------------


class _A2aEventSubscriber:
    """Subscribes to an :class:`A2aEventQueue` and calls a callback on each
    incoming :class:`A2aEvent`.

    Usage::

        subscriber = _A2aEventSubscriber(event_queue)
        subscriber.subscribe(lambda event: materializer._handle_a2a_event(event))
    """

    def __init__(self, event_queue: Any) -> None:
        self._event_queue = event_queue
        self._sub_id: str | None = None

    def subscribe(self, callback: Any) -> str | None:
        """Register callback with the event queue. Returns subscription ID."""
        if self._event_queue is None:
            return None
        self._sub_id = self._event_queue.subscribe_local(callback)
        return self._sub_id

    def unsubscribe(self) -> None:
        """Remove our subscription."""
        if self._event_queue is not None and self._sub_id is not None:
            with contextlib.suppress(Exception):
                self._event_queue.unsubscribe(self._sub_id)
            self._sub_id = None


# ============================================================================
# TuiMaterializer -- the main class
# ============================================================================


class TuiMaterializer:
    """OutputRendering framework materialization strategy for the Textual TUI.

    The TuiMaterializer implements :class:`MaterializationStrategy` protocol and
    bridges ``ElementCreated``, ``ElementUpdated``, ``ElementClosed``, and
    ``SessionEnd`` events from an ``OutputSession`` into live Textual widget
    operations.

    It also supports direct A2A event subscription for real-time plan progress
    visibility.

    Thread Safety
    -------------
    Internal state is protected by ``_lock``.  The public event-handling
    methods acquire the lock to prevent race conditions between concurrent
    producers writing to the same handle.

    Parameters
    ----------
    event_queue:
        An :class:`A2aEventQueue` instance for real-time A2A event
        subscription.  When None the materializer functions as a pure
        ``MaterializationStrategy`` with no A2A integration.
    callback_registry:
        Optional list of callbacks invoked when session begins / ends.
    """

    strategy_name: str = "tui"
    supports_incremental_updates: bool = True

    # Mapping from element_kind strings to widget builder and updater names.
    _KIND_WIDGET_MAP: ClassVar[dict[str, tuple[str, str | None]]] = {
        "panel": ("_build_panel", "_update_panel"),
        "table": ("_build_table", "_add_table_row"),
        "tree": ("_build_tree", None),
        "status": ("_build_status", None),
        "progress": ("_build_progress", "_update_progress"),
        "code": ("_build_code_block", "_render_code"),
        "text": ("_build_text_block", None),
        "diff": ("_build_diff", None),
        "separator": ("_build_separator", None),
        "action_hint": ("_build_action_hint", None),
    }

    def __init__(
        self,
        event_queue: Any | None = None,
        callback_registry: list[Any] | None = None,
    ) -> None:
        self._event_queue = event_queue
        self._callbacks = callback_registry or []
        # Widget registry -- handle_id -> _TuiWidget.
        self._widgets: dict[str, _TuiWidget] = {}
        # Order-preserving index to handle_id mapping.
        self._index_map: dict[int, str] = {}
        self._next_index: int = 0
        # Thread safety lock.
        self._lock = threading.Lock()
        # Reactive A2A event subscriber (lazy).
        self._a2a_subscriber: _A2aEventSubscriber | None = None

    def on_session_begin(self, session: Any) -> None:
        """Called when a new OutputSession begins.

        Registers the A2A event subscriber if an event queue was provided.
        Invokes all registered callbacks.
        """
        with self._lock:
            if self._event_queue is not None and self._a2a_subscriber is None:
                self._a2a_subscriber = _A2aEventSubscriber(self._event_queue)

                def _on_a2a_event(event: Any) -> None:
                    """Callback invoked on each A2A event from the queue."""
                    self._handle_a2a_event(event)

                self._a2a_subscriber.subscribe(_on_a2a_event)

            # Fire begin callbacks.
            for cb in self._callbacks or []:
                try:
                    if callable(cb):
                        cb("session_begin", session)
                except Exception:  # pylint: disable=broad-except
                    pass

    def on_element_created(self, event: ElementCreated) -> None:
        """Handle an element created event.

        Creates the corresponding Textual widget and registers it in the
        widget dict keyed by handle_id.
        """
        with self._lock:
            kind = event.element_kind
            handle_id = event.handle_id

            idx = getattr(event, "declaration_index", 0)
            self._index_map[idx] = handle_id
            self._next_index += 1

            widget = self._make_widget(kind, event.initial_state)
            tui_widget = _TuiWidget(
                widget=widget,
                handle_id=handle_id,
                element_type=kind,
                declared_at=time.monotonic(),
            )
            self._widgets[handle_id] = tui_widget

            # Fire element-created callbacks.
            for cb in self._callbacks or []:
                try:
                    if callable(cb):
                        cb("element_created", event)
                except Exception:  # pylint: disable=broad-except
                    pass

    def on_element_updated(self, event: ElementUpdated) -> None:
        """Handle an incremental element update.

        Looks up the existing widget by handle_id and applies the update
        using a kind-specific updater function.
        """
        with self._lock:
            widget_tui = self._widgets.get(event.handle_id)
            if widget_tui is None or widget_tui.closed:
                return

            kind = event.element_kind
            snapshot = getattr(event, "element_snapshot", None)
            if snapshot is None:
                return

            self._apply_update(widget_tui.widget, kind, snapshot)

    def on_element_closed(self, event: ElementClosed) -> None:
        """Handle element closing.

        Marks the associated widget as closed and fires a close callback.
        The widget is retained in the registry for post-close inspection.
        """
        with self._lock:
            widget_tui = self._widgets.get(event.handle_id)
            if widget_tui is None:
                return

            widget_tui.closed = True

            # Fire close callbacks.
            for cb in self._callbacks or []:
                try:
                    if callable(cb):
                        cb("element_closed", event)
                except Exception:  # pylint: disable=broad-except
                    pass

    def on_session_end(self, event: SessionEnd) -> None:
        """Handle session end.

        Unsubscribes from A2A events and invokes all session-end callbacks.
        """
        with self._lock:
            if self._a2a_subscriber is not None:
                self._a2a_subscriber.unsubscribe()
                self._a2a_subscriber = None

            for cb in self._callbacks or []:
                try:
                    if callable(cb):
                        cb("session_end", event)
                except Exception:  # pylint: disable=broad-except
                    pass

    # ------------------------------------------------------------------
    # Internal widget factory and updater dispatch
    # ------------------------------------------------------------------

    def _make_widget(self, kind: str, element: Any = None) -> Any:
        """Create a new Textual widget for the given element kind."""
        factory_name = self._KIND_WIDGET_MAP.get(kind, ("", ""))[0]
        builder = getattr(self, factory_name, None)
        if builder is not None and element is not None:
            return builder(element)
        return type("TuiWidget", (), {"_kind": kind, "label": ""})

    # -- Element kind-specific builders ---

    def _build_panel(self, panel: Panel) -> Any:
        try:
            from textual.widgets import (  # type: ignore[import-untyped]
                Static as TwStatic,
            )

            widget = TwStatic(panel.title)
            _build_panel_display(widget, panel)
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackPanel", (), {"label": panel.title})

    def _build_table(self, table: Table) -> Any:
        try:
            from textual.widgets import DataTable  # type: ignore[import-untyped]

            widget = DataTable(title=table.title or "")
            _init_table_display(widget, table)
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackTable", (), {"title": str(table.title)})

    def _build_tree(self, tree: Tree) -> Any:
        try:
            from textual.widgets import Tree as TwTree  # type: ignore[import-untyped]

            widget = TwTree(f"[b]{tree.root.label}[/b]")
            _populate_tree(widget, tree)
            return widget
        except ImportError:  # pylint: disable=broad-exception-caught
            return type("FallbackTree", (), {"root_label": tree.root.label})

    def _build_status(self, status_msg: StatusMessage) -> Any:
        try:
            from textual.widgets import Label as TwLabel  # type: ignore[import-untyped]

            widget = TwLabel(status_msg.message)
            if status_msg.level == "error":
                widget.add_class("tui-status--red")
            elif status_msg.level == "warn":
                widget.add_class("tui-status--yellow")
            elif status_msg.level == "ok":
                widget.add_class("tui-status--green")
            else:
                widget.add_class("tui-status--info")
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackStatus", (), {"message": status_msg.message})

    def _build_progress(self, progress: ProgressIndicator) -> Any:
        try:
            from textual.widgets import (  # type: ignore[import-untyped]
                ProgressBar as TwProgressBar,
            )
            from textual.widgets import (  # type: ignore[import-untyped]
                Throbber as TwThrobber,
            )

            if progress.indeterminate or progress.total is None:
                widget = TwThrobber(start=False)
            else:
                widget = TwProgressBar(total=progress.total, finished="idle")
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackProgress", (), {"label": progress.label})

    def _build_code_block(self, code: CodeBlock) -> Any:
        try:
            from textual.widgets import (  # type: ignore[import-untyped]
                TextArea as TwTextArea,
            )

            widget = TwTextArea(code.content, read_only=True)
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackCode", (), {"_text": code.content})

    def _build_text_block(self, text: TextBlock) -> Any:
        try:
            from textual.widgets import (  # type: ignore[import-untyped]
                Static as TwStatic,
            )

            widget = TwStatic(text.content)
            return widget
        except ImportError:  # pragma: no cover
            return type("FallbackText", (), {"_text": text.content})

    def _build_diff(self, diff: DiffBlock) -> Any:
        try:
            widget = _DiffViewStub(diff.file_a, diff.file_b)
            for hunk in diff.hunks:
                widget.add_hunk(hunk)
            return widget
        except ImportError:  # pragma: no cover
            return _DiffViewStub(diff.file_a, diff.file_b)

    def _build_separator(self, sep: Separator) -> Any:
        try:
            from textual.widgets import Rule as TwRule  # type: ignore[import-untyped]

            widget = TwRule()
            return widget
        except ImportError:  # pragma: no cover
            return type("FbSep", (), {"_text": "\u2500" * 30})

    def _build_action_hint(self, hint: ActionHint) -> Any:
        try:
            from textual.widgets import (  # type: ignore[import-untyped]
                Static as TwStatic,
            )

            widget = TwStatic(f"[em]{', '.join(hint.commands)}[/]")
            return widget
        except ImportError:  # pragma: no cover
            return type("FbHint", (), {})

    def _apply_update(self, widget: Any, kind: str, snapshot: Any) -> None:
        """Dispatch an incremental update to the correct renderer function."""

        updater_map: dict[str, Any] = {
            "panel": lambda w, s: (
                _build_panel_display(w, s) if isinstance(s, Panel) else None
            ),
            "table": self._apply_table_update,
            "tree": self._apply_tree_update,
            "status": _display_status,
            "progress": self._apply_progress_update,
            "code": _render_code,
            "text": _render_text,
            "diff": _render_diff,
            "separator": _render_separator,
            "action_hint": _render_action_hint,
        }

        fn = updater_map.get(kind)
        if fn is not None:
            with contextlib.suppress(Exception):
                fn(widget, snapshot)  # type: ignore[arg-type]

    def _apply_panel_update(self, widget: Any, snapshot: Any) -> None:
        if isinstance(snapshot, Panel):
            _build_panel_display(widget, snapshot)

    def _apply_table_update(self, widget: Any, snapshot: Any) -> None:
        if isinstance(snapshot, Table):
            for row in snapshot.rows:
                _add_table_row(widget, row)

    def _apply_tree_update(self, widget: Any, snapshot: Any) -> None:
        if isinstance(snapshot, Tree) and snapshot.root.children:
            _populate_tree(widget, snapshot)

    def _apply_progress_update(self, widget: Any, snapshot: Any) -> None:
        if isinstance(snapshot, StatusMessage):
            _update_progress(widget, snapshot)

    # ------------------------------------------------------------------
    # A2A event handling -- real-time plan progress to TUI widgets
    # ------------------------------------------------------------------

    def _handle_a2a_event(self, event: Any) -> None:
        """Route an incoming A2A event to the appropriate TUI widget.

        This method is invoked as a callback on every A2aEvent published to
        the subscribed A2aEventQueue.
        """
        with self._lock:
            event_type = getattr(event, "event_type", "")
            data = getattr(event, "data", None) or {}

            if event_type == "TaskStatusUpdateEvent":
                self._render_status_event(data)
            elif event_type == "TaskArtifactUpdateEvent":
                self._render_artifact_event(data)

    def _render_status_event(self, data: dict[str, Any]) -> None:
        """Render a status update event into the TUI's conversation widget."""
        phase = str(data.get("phase", "") or "")
        state = str(data.get("state", "") or "")
        plan_id = str(data.get("plan_id", "") or "")

        clean_plan = plan_id.replace("-", "")
        for handle_id, tui_widget in self._widgets.items():
            if tui_widget.element_type == "progress" and (
                not clean_plan or clean_plan in handle_id.replace("-", "")
            ):
                try:
                    sm = StatusMessage(message=f"Phase: phase={phase}, state={state}")
                    _update_progress(tui_widget.widget, sm)
                except Exception:  # pylint: disable=broad-except
                    pass

    def _render_artifact_event(self, data: dict[str, Any]) -> None:
        """Render an artifact update event (code, diffs, status)."""
        kind = str(data.get("artifact_type", "") or "")
        artifact_data = data.get("data", data)

        if kind == "code" or data.get("type") == "code":
            content = str(artifact_data.get("content", ""))
            for _handle_id, tui_widget in self._widgets.items():
                if tui_widget.element_type == "code":
                    try:
                        widget = tui_widget.widget
                        if hasattr(widget, "text"):
                            widget.text = content
                    except Exception:  # pylint: disable=broad-except
                        pass

        elif kind == "diff" or data.get("type") == "diff":
            hunks = artifact_data.get("hunks", [])
            for _handle_id, tui_widget in self._widgets.items():
                if tui_widget.element_type == "diff":
                    try:
                        widget = tui_widget.widget
                        for hunk in hunks:
                            if hasattr(widget, "add_hunk"):
                                dh = DiffHunk(header=str(hunk.get("header", "")))
                                for ln in hunk.get("lines", []):
                                    dh.lines.append(
                                        DiffLine(  # type: ignore[call-arg]
                                            type=ln.get("type", "context"),
                                            content=ln.get("content", ""),
                                        )
                                    )
                                widget.add_hunk(dh)
                    except Exception:  # pylint: disable=broad-except
                        pass

        elif kind == "status" or data.get("type") == "status":
            status_msg = StatusMessage(
                message=str(artifact_data.get("message", "")),
                level=str(artifact_data.get("level", "info")),
                detail=str(artifact_data.get("detail", "")) or None,
            )
            for _handle_id, tui_widget in self._widgets.items():
                if tui_widget.element_type == "status":
                    with contextlib.suppress(Exception):
                        _display_status(tui_widget.widget, status_msg)

    def get_widget_registry(self) -> dict[str, _TuiWidget]:
        """Return a copy of the widget registry for inspection."""
        with self._lock:
            return dict(self._widgets)

    def clear(self) -> None:
        """Clear all registered widgets and flush state.

        Used when the TUI switches sessions or closes a plan.
        """
        with self._lock:
            if self._a2a_subscriber is not None:
                self._a2a_subscriber.unsubscribe()
                self._a2a_subscriber = None

            self._widgets.clear()
            self._index_map.clear()
            self._next_index = 0


# ============================================================================
# Convenience factory
# ============================================================================


def create_tui_materializer(
    event_queue: Any | None = None,
    callbacks: list[Any] | None = None,
) -> TuiMaterializer:
    """Factory function to create a TuiMaterializer with standard wiring.

    Parameters
    ----------
    event_queue:
        The shared ``A2aEventQueue`` for A2A protocol events.  When None the
        materializer still works as a pure MaterializationStrategy.
    callbacks:
        Optional callback list for session-level hooks.

    Returns
    -------
    TuiMaterializer
        An initialized materializer ready to receive OutputSession events.
    """
    return TuiMaterializer(
        event_queue=event_queue,
        callback_registry=callbacks,
    )


# ============================================================================
# Public exports
# ============================================================================


__all__ = [
    "TuiMaterializer",
    "_A2aEventSubscriber",
    "_DiffViewStub",
    "_add_table_row",
    "_build_panel_display",
    "_display_status",
    "_init_table_display",
    "_populate_tree",
    "_render_action_hint",
    "_render_code",
    "_render_diff",
    "_render_separator",
    "_render_text",
    "create_tui_materializer",
]
