"""Interactive REPL and one-shot mode for the Keiro CLI.

Handles three invocation patterns:

- ``keiro`` — interactive streaming chat REPL
- ``keiro "prompt"`` — one-shot: print response, exit
- ``echo ctx | keiro "prompt"`` — pipe stdin as context

When stdout is not a TTY (piped to another program), output is raw text
with no decorations or colors.
"""

from __future__ import annotations

import os
import pathlib
import re
import sys
import threading
import time
from collections.abc import Iterable
from typing import TYPE_CHECKING

from keiro.cli.animations import _SPINNER_FRAMES, _shimmer_ansi, animations_enabled
from keiro.client import KeirError

_HISTORY_PATH = pathlib.Path.home() / ".keiro" / "history"
_HISTORY_MAX_LINES = 500
_MAX_HISTORY_MESSAGES = 40  # 20 turn pairs

from dataclasses import dataclass

if TYPE_CHECKING:
    from keiro.model_api import AdminMetrics, ModelsAPI


@dataclass
class SessionMetrics:
    """Cumulative metrics across REPL requests."""

    request_count: int = 0
    total_prompt_tokens: int = 0
    total_completion_tokens: int = 0
    total_cost_usd: float = 0.0

    def record(self, metrics: "AdminMetrics") -> None:
        """Accumulate metrics from a single request."""
        self.request_count += 1
        self.total_prompt_tokens += metrics.prompt_tokens
        self.total_completion_tokens += metrics.completion_tokens
        if metrics.cost_usd is not None:
            self.total_cost_usd += metrics.cost_usd


# Built-in tools enabled by default in the CLI.  Uses canonical type
# names; each provider translates to its native equivalent (e.g.,
# Anthropic → web_search_20250305, Google → google_search).
_DEFAULT_TOOLS: list[dict[str, str]] = [{"type": "web_search"}]


def _report_keir_error(exc: KeirError) -> None:
    """Write a formatted KeirError to stderr with actionable guidance."""
    msg = exc.user_message
    if exc.status_code == 401:
        sys.stderr.write(f"\033[31merror [{exc.status_code}]: {msg}\033[0m\n")
    elif exc.status_code == 429:
        sys.stderr.write(f"\033[33merror [{exc.status_code}]: {msg}\033[0m\n")
    elif exc.status_code == 404:
        sys.stderr.write(
            f"\033[31merror [{exc.status_code}]: {msg}\033[0m\n"
            "\033[2mHint: run 'keiro models' to see available models.\033[0m\n"
        )
    elif exc.status_code == 422:
        sys.stderr.write(
            f"\033[31merror [{exc.status_code}]: {msg}\033[0m\n"
            "\033[2mHint: check your request parameters.\033[0m\n"
        )
    elif exc.status_code == 400:
        # Validation errors — status code is noise for local checks.
        sys.stderr.write(f"\033[31merror: {msg}\033[0m\n")
    else:
        sys.stderr.write(f"\033[31merror [{exc.status_code}]: {msg}\033[0m\n")


def _is_tty_out() -> bool:
    return sys.stdout.isatty()


def _is_tty_in() -> bool:
    return sys.stdin.isatty()


def _read_stdin_context() -> str | None:
    """Read piped stdin content. Returns None if stdin is a TTY."""
    if _is_tty_in():
        return None
    try:
        return sys.stdin.read()
    except (OSError, UnicodeDecodeError):
        return None


def _spinner_loop(stop: threading.Event, start: float) -> None:
    """Write shimmer-animated spinner frames to stdout until *stop* is set.

    Combines a braille spinner character, accumulating dots, a shimmer
    highlight sweep across the label, and an elapsed counter after 2 s.
    """
    frame = 0
    dot_cycle = 0.6  # seconds per full dot cycle (1 → 2 → 3 → 1)
    shimmer_cycle = 2.0  # seconds per full shimmer sweep
    while not stop.wait(0.06):
        char = _SPINNER_FRAMES[frame % len(_SPINNER_FRAMES)]
        elapsed = time.monotonic() - start

        # Accumulating dots: Thinking. → Thinking.. → Thinking...
        dot_count = int((elapsed % dot_cycle) / dot_cycle * 3) + 1
        dots = "." * dot_count + " " * (3 - dot_count)
        label = f"Thinking{dots}"

        if elapsed > 2.0:
            label += f" ({elapsed:.0f}s)"

        shimmer_phase = (elapsed % shimmer_cycle) / shimmer_cycle
        styled = _shimmer_ansi(label, shimmer_phase)

        sys.stdout.write(f"\r\033[2m{char}\033[0m {styled}\033[K")
        sys.stdout.flush()
        frame += 1


_STREAM_SENTINEL = object()


def _stream_in_background(
    iterator: Iterable[str],
    q: "queue.Queue[str | object]",
) -> None:
    """Consume a stream iterator, pushing chunks to a queue.

    Runs in a daemon thread so the main thread stays responsive to
    signals (Ctrl-C). Pushes ``_STREAM_SENTINEL`` when the stream
    is exhausted or an error occurs.
    """
    try:
        for chunk in iterator:
            q.put(chunk)
    except Exception as exc:
        q.put(exc)
    finally:
        q.put(_STREAM_SENTINEL)


def _stream_response(
    chunks: Iterable[str],
    *,
    decorated: bool,
) -> str:
    """Consume a streaming response, printing chunks as they arrive.

    Stream consumption runs in a daemon thread so Ctrl-C is always
    responsive — even during the EB1 thinking phase when the server
    sends no data for 5-30 seconds.

    When running in a TTY, streams raw tokens for real-time feedback,
    then erases the raw output and re-renders as formatted markdown.
    When stdout is not a TTY (piped), outputs raw text.

    Args:
        chunks: Iterator of text chunks from the API.
        decorated: If True, show spinner and render markdown.

    Returns:
        The full response text.
    """
    import queue

    start = time.monotonic()
    full_text: list[str] = []
    q: queue.Queue[str | object] = queue.Queue()

    # Launch stream consumer in a daemon thread so the main thread
    # remains interruptible by Ctrl-C at all times.
    stream_thread = threading.Thread(
        target=_stream_in_background,
        args=(chunks, q),
        daemon=True,
        name="keiro-stream",
    )
    stream_thread.start()

    # Phase 1: Wait for first chunk with optional spinner.
    show_spinner = decorated and animations_enabled()
    stop: threading.Event | None = None
    spinner_thread: threading.Thread | None = None

    if show_spinner:
        stop = threading.Event()
        spinner_thread = threading.Thread(
            target=_spinner_loop,
            args=(stop, start),
            daemon=True,
            name="keiro-thinking",
        )
        spinner_thread.start()

    try:
        first_chunk = _queue_get_interruptible(q)
    finally:
        if stop is not None:
            stop.set()
        if spinner_thread is not None:
            spinner_thread.join(timeout=1.0)
        if show_spinner:
            sys.stdout.write("\r\033[K")
            sys.stdout.flush()

    if isinstance(first_chunk, Exception):
        raise first_chunk
    if first_chunk is _STREAM_SENTINEL or first_chunk is None:
        return ""

    # Phase 2: Stream raw tokens for immediate feedback.
    full_text.append(first_chunk)
    sys.stdout.write(first_chunk)
    sys.stdout.flush()

    while True:
        item = _queue_get_interruptible(q)
        if item is _STREAM_SENTINEL:
            break
        if isinstance(item, Exception):
            raise item
        full_text.append(item)
        sys.stdout.write(item)
        sys.stdout.flush()

    response_text = "".join(full_text)

    # Phase 3: Re-render as formatted markdown (TTY only).
    if decorated and _has_markdown(response_text):
        _replace_with_markdown(response_text)
    elif decorated:
        sys.stdout.write("\n")
        sys.stdout.flush()

    return response_text


def _queue_get_interruptible(
    q: "queue.Queue[str | object]",
    poll_interval: float = 0.1,
) -> "str | object":
    """Pull from a queue with short polling so Ctrl-C is responsive.

    ``queue.Queue.get(timeout=None)`` blocks in C code and defers
    ``KeyboardInterrupt``. Polling with a short timeout ensures the
    main thread checks for signals every 100ms.
    """
    import queue

    while True:
        try:
            return q.get(timeout=poll_interval)
        except queue.Empty:
            continue


def _has_markdown(text: str) -> bool:
    """Detect whether text contains markdown that benefits from rendering.

    Returns True if the text has headers, code fences, bold, or lists.
    Plain conversational text is left as-is.
    """
    for line in text.split("\n"):
        stripped = line.lstrip()
        if stripped.startswith(("# ", "## ", "### ", "#### ")):
            return True
        if stripped.startswith("```"):
            return True
        if stripped.startswith(("- ", "* ", "1. ")):
            return True
    if "**" in text:
        return True
    return False


_ANSI_ESCAPE_RE = re.compile(r"\033\[[0-9;]*[A-Za-z]")


def _strip_ansi(text: str) -> str:
    """Remove ANSI escape sequences from text."""
    return _ANSI_ESCAPE_RE.sub("", text)


def _visible_len(text: str) -> int:
    """Length of text excluding ANSI escape sequences."""
    return len(_strip_ansi(text))


def _count_terminal_lines(text: str) -> int:
    """Count how many terminal lines *text* occupies, including wraps."""
    try:
        cols = os.get_terminal_size().columns
    except (OSError, ValueError):
        cols = 80
    total = 0
    for line in text.split("\n"):
        # Each line occupies at least 1 row; long lines wrap.
        total += max(1, (_visible_len(line) + cols - 1) // cols)
    return total


def _replace_with_markdown(text: str) -> None:
    """Erase the raw streamed text and re-render as formatted markdown.

    Moves the cursor up to the start of the raw output, clears to end
    of screen, then prints the Rich-rendered markdown in its place.
    """
    from rich.console import Console
    from rich.markdown import Markdown
    from rich.padding import Padding

    # Erase the raw output by moving cursor up and clearing.
    # move_up is (total_rows - 1) because the cursor sits ON the last
    # row of text, not below it.  The \r resets the column to 0 so
    # Rich starts printing at the left margin (without it, the first
    # line of markdown inherits whatever column the raw stream ended at).
    num_lines = _count_terminal_lines(text)
    move_up = max(num_lines - 1, 0)
    if move_up > 0:
        sys.stdout.write(f"\033[{move_up}A")
    sys.stdout.write("\r\033[J")
    sys.stdout.flush()

    console = Console(highlight=False)
    console.print(Padding(Markdown(text), (0, 0, 0, 1)))


def one_shot(
    prompt: str,
    *,
    model: str,
    context: str | None = None,
    admin_token: str | None = None,
    admin: bool = False,
) -> int:
    """Run a single prompt, stream the response, and exit.

    Args:
        prompt: User prompt text.
        model: Model identifier.
        context: Optional piped context to prepend as a system message.
        admin_token: Admin token for server-side metrics. When set,
            an admin metrics panel is printed to stderr after the response.
        admin: Whether admin mode is active (independent of token).

    Returns:
        Exit code.
    """
    from keiro.model_api import ModelsAPI

    decorated = _is_tty_out()
    api = ModelsAPI(admin_token=admin_token)

    try:
        kwargs: dict[str, object] = {"tools": _DEFAULT_TOOLS}
        if context:
            kwargs["instructions"] = context

        messages = [{"role": "user", "content": prompt}]
        _stream_with_admin(
            api,
            model,
            messages,
            decorated=decorated,
            admin=admin,
            **kwargs,
        )

        if not decorated:
            # Ensure trailing newline for piped output.
            sys.stdout.write("\n")
            sys.stdout.flush()
    except KeyboardInterrupt:
        sys.stdout.write("\n")
        return 130
    except KeirError as exc:
        _report_keir_error(exc)
        return 1
    except Exception as exc:
        sys.stderr.write(f"error: {exc}\n")
        return 1
    finally:
        api.close()

    return 0


def _read_fenced_block(opening_line: str) -> str:
    """Read lines until a closing ``` fence, returning the full block.

    Supports pasting a fenced code block: the opening ``` triggers
    multi-line accumulation, and the closing ``` terminates it.
    Any text after the opening ``` (e.g., a language tag) is preserved.
    """
    lines = [opening_line]
    while True:
        try:
            line = input("\001\033[2m\002...\001\033[0m\002 ")
        except EOFError:
            break
        lines.append(line)
        if line.strip() == "```":
            break
    return "\n".join(lines)


def _drain_pasted_lines() -> list[str]:
    """Read any lines already buffered in stdin from a paste operation.

    Uses ``select`` to check if data is immediately available (within
    50ms). This distinguishes pasted content (instant) from a human
    pressing Enter then typing (>50ms gap). Returns an empty list if
    stdin is not a TTY or no data is pending.
    """
    if not _is_tty_in():
        return []
    try:
        import select
    except ImportError:
        return []

    lines: list[str] = []
    while True:
        ready, _, _ = select.select([sys.stdin], [], [], 0.05)
        if not ready:
            break
        try:
            line = sys.stdin.readline()
        except (OSError, UnicodeDecodeError):
            break
        if not line:  # EOF
            break
        lines.append(line.rstrip("\n"))
    return lines


def _init_readline() -> None:
    """Enable readline for up-arrow history and persistent history file.

    Loads previous session history from ``~/.keiro/history`` and registers
    an atexit hook to save it back on exit.
    """
    try:
        import readline  # noqa: F811
    except ImportError:
        return  # Windows without pyreadline; graceful degradation.

    import atexit

    # Load history from prior sessions.
    try:
        readline.read_history_file(str(_HISTORY_PATH))
    except (FileNotFoundError, OSError):
        pass

    readline.set_history_length(_HISTORY_MAX_LINES)

    def _save() -> None:
        try:
            _HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
            readline.write_history_file(str(_HISTORY_PATH))
        except OSError:
            pass

    atexit.register(_save)


def repl(
    *,
    model: str,
    admin_token: str | None = None,
    admin: bool = False,
) -> int:
    """Run an interactive streaming chat REPL.

    Args:
        model: Default model identifier.
        admin_token: Admin token for server-side metrics. ``/admin``
            toggles the metrics panel during the session.
        admin: Whether admin mode is active (independent of token).

    Returns:
        Exit code.
    """
    from keiro.model_api import ModelsAPI

    _init_readline()
    api = ModelsAPI(admin_token=admin_token)
    admin_active = admin
    session = SessionMetrics()

    _print_welcome(model, admin=admin_active)
    history: list[dict[str, str]] = []

    try:
        while True:
            try:
                prompt = input(
                    f"\001\033[2m\002{model}\001\033[0m\002"
                    " \001\033[1m\002\u276f\001\033[0m\002 "
                )
            except EOFError:
                sys.stdout.write("\n")
                break

            prompt = prompt.strip()
            if not prompt:
                continue

            # Detect multi-line input: explicit ``` fencing or pasted lines.
            if prompt.startswith("```"):
                prompt = _read_fenced_block(prompt)
            else:
                extra = _drain_pasted_lines()
                if extra:
                    prompt = prompt + "\n" + "\n".join(extra)

            command = prompt.lower()

            if command in ("exit", "quit", "/q", "/quit", "/exit"):
                break

            if command == "/help":
                _print_help()
                continue

            if command == "/clear":
                history.clear()
                # Clear terminal screen and move cursor to top.
                sys.stdout.write("\033[2J\033[H")
                sys.stdout.flush()
                _print_welcome(model, admin=admin_active)
                continue

            if command == "/admin":
                admin_active = not admin_active
                if admin_active:
                    note = ""
                    if not admin_token:
                        note = " (admin access depends on API key role)"
                    sys.stderr.write(
                        f"\033[33m\u25c6 admin mode on{note}\033[0m\n\n",
                    )
                else:
                    sys.stderr.write(
                        "\033[2m\u25c7 admin mode off\033[0m\n\n",
                    )
                sys.stderr.flush()
                continue

            if command.startswith("/model"):
                parts = prompt.split(None, 1)
                if len(parts) < 2:
                    sys.stderr.write(
                        f"\033[2mModel: {model}\033[0m\n\n",
                    )
                    continue
                new_model = parts[1].strip()
                from keiro.ensemble.catalog import select_variant
                from keiro.ensemble.variants import normalize_model_name

                variant = select_variant(new_model)
                normalized = normalize_model_name(new_model)
                if variant.matches(normalized):
                    model = variant.key
                else:
                    model = new_model
                history.clear()
                sys.stderr.write(
                    f"\033[2mSwitched to {model}. History cleared.\033[0m\n\n",
                )
                continue

            messages = history + [{"role": "user", "content": prompt}]
            sys.stdout.write("\n")
            try:
                response_text = _stream_with_admin(
                    api,
                    model,
                    messages,
                    decorated=True,
                    admin=admin_active,
                    session=session,
                    tools=_DEFAULT_TOOLS,
                )
                if response_text:
                    history.append({"role": "user", "content": prompt})
                    history.append(
                        {"role": "assistant", "content": response_text},
                    )
                    if len(history) > _MAX_HISTORY_MESSAGES:
                        del history[:-_MAX_HISTORY_MESSAGES]
            except KeyboardInterrupt:
                # Ctrl-C during streaming: cancel and return to prompt.
                sys.stdout.write("\n")
                sys.stderr.write(
                    "\033[2mInterrupted.\033[0m\n",
                )
                sys.stderr.flush()
            except KeirError as exc:
                _report_keir_error(exc)
            except Exception as exc:
                sys.stderr.write(f"error: {exc}\n")

            # Visual separator between turns for readability.
            try:
                cols = os.get_terminal_size().columns
            except (OSError, ValueError):
                cols = 80
            sys.stdout.write(f"\n\033[2m{'─' * min(cols, 72)}\033[0m\n")

    except KeyboardInterrupt:
        sys.stdout.write("\n")
    finally:
        api.close()

    return 0


# ---------------------------------------------------------------------------
# Startup banner
# ---------------------------------------------------------------------------

# Unicode block-character logo — rounded filled rectangle (2 lines).
_LOGO = (
    " \u2590\u259b\u2588\u2588\u259c\u258c ",  # ▐▛██▜▌
    " \u2590\u2599\u2588\u2588\u259f\u258c ",  # ▐▙██▟▌
)


def _print_help() -> None:
    """Print available REPL commands to stderr."""
    sys.stderr.write(
        "\033[2m"
        "Commands:\n"
        "  /help           Show this help\n"
        "  /model <name>   Switch model (clears history)\n"
        "  /model          Show current model\n"
        "  /clear          Clear conversation and screen\n"
        "  /admin          Toggle admin metrics panel\n"
        "  /quit, /exit    Exit the REPL\n"
        "  ```             Start multi-line input\n"
        "  Ctrl-C          Cancel current response\n"
        "\033[0m\n"
    )
    sys.stderr.flush()


def _model_description(model: str) -> str:
    """Short human-readable description for a model identifier."""
    from keiro.cli.models import get_model_description

    desc = get_model_description(model)
    if desc:
        return desc
    if model.endswith("-preview"):
        return "adaptive model"
    if not model.startswith("eb1"):
        return "direct model"
    return "compound AI model"


def _print_welcome(model: str, *, admin: bool = False) -> None:
    """Print a branded startup banner with logo, version, and model info."""
    from rich.console import Console
    from rich.text import Text

    import keiro

    console = Console(highlight=False)

    try:
        term_width = os.get_terminal_size().columns
    except (OSError, ValueError):
        term_width = 80

    desc = _model_description(model)

    # Line 1: logo + product name + version.
    line1 = Text()
    line1.append(_LOGO[0], style="bold")
    line1.append("  \u7d4c\u8def", style="bold")  # 経路 = route/path
    line1.append(" keiro", style="bold")
    line1.append(f" v{keiro.__version__}", style="dim")

    # Line 2: logo + model + description.
    line2 = Text()
    line2.append(_LOGO[1], style="bold")
    line2.append(f"  {model}", style="bold cyan")
    line2.append(f" \u00b7 {desc}", style="dim")

    console.print()
    console.print(line1)
    console.print(line2)
    if admin:
        line3 = Text()
        line3.append("           \u25c6 admin mode", style="yellow")
        console.print(line3)
    console.print()

    # Separator with keyboard hints (right-aligned, Claude Code style).
    hints = "/help \u00b7 /model \u00b7 /clear \u00b7 /admin \u00b7 Ctrl-C"
    pad = term_width - len(hints) - 3
    if pad > 4:
        sep = "\u2500" * pad + " " + hints + " \u2500"
    else:
        sep = "\u2500" * term_width
    console.print(f"[dim]{sep}[/dim]")


def _format_duration(ms: float) -> str:
    """Format milliseconds as a compact human-readable duration."""
    if ms < 1000:
        return f"{ms:.0f}ms"
    return f"{ms / 1000:.1f}s"


# Demo-friendly display names for InferenceTier enum values sent by
# the GNN server (e.g., "TRIVIAL", "DISCRIMINATIVE", "UNCERTAIN").
_TIER_DISPLAY: dict[str, str] = {
    "TRIVIAL": "optimized",
    "DISCRIMINATIVE": "routed",
    "UNCERTAIN": "Network of Networks",
    "OUT_OF_DISTRIBUTION": "adaptive",
}

# Maps internal pipeline step names to product-level labels.
_STEP_RENAME: dict[str, str] = {
    "gnn": "routing",
    "route": "routing",
    "resolve": "routing",
    "relay": "generation",
    "execute": "generation",
    "provider": "generation",
}

# Minimum probability to show a model in the ranking bars.
_BAR_MIN_PROB = 0.02


def _find_baseline_prob(
    distribution: dict[str, float],
) -> tuple[str | None, float]:
    """Find the baseline model (Opus) in the GNN distribution.

    Returns (model_name, probability) or (None, 0.0) if not found.
    """
    for name, prob in distribution.items():
        if "opus" in name.lower():
            return name, prob
    return None, 0.0


def _render_routing_header(metrics: "AdminMetrics") -> None:
    """Render a routing summary to stderr before streaming.

    Called via the ``on_header`` callback when the SSE header event
    arrives, before any text deltas.  Adapts display to routing tier:
    TRIVIAL gets a one-liner, DISCRIMINATIVE shows top models,
    UNCERTAIN shows the full distribution.
    """
    parts: list[str] = []

    tier = metrics.gnn_tier or ""
    tier_label = _TIER_DISPLAY.get(tier, tier)
    is_trivial = tier == "TRIVIAL"

    if tier_label:
        parts.append(tier_label)

    ranked: list[tuple[str, float]] = []
    if metrics.gnn_distribution:
        ranked = sorted(
            metrics.gnn_distribution.items(),
            key=lambda kv: kv[1],
            reverse=True,
        )
        # Filter out models below minimum probability.
        ranked = [(n, p) for n, p in ranked if p >= _BAR_MIN_PROB]

        top_name, top_prob = ranked[0] if ranked else ("", 0.0)
        baseline_name, baseline_prob = _find_baseline_prob(
            metrics.gnn_distribution,
        )
        if baseline_name and baseline_prob > 0:
            if top_name == baseline_name:
                parts.append("Opus optimal")
            elif top_prob / baseline_prob >= 1.05:
                parts.append(
                    f"{top_prob / baseline_prob:.1f}x over Opus",
                )

    if metrics.gnn_route_time_ms is not None:
        parts.append(f"{metrics.gnn_route_time_ms:.0f}ms")

    if not parts:
        return

    line = " \u00b7 ".join(parts)
    sys.stderr.write(f"\n\033[2m  \u25b8 {line}\033[0m\n")

    # TRIVIAL tier: one-line summary, no ranking bars.
    if is_trivial or not ranked:
        sys.stderr.write("\n")
        sys.stderr.flush()
        return

    # Render ranking bars with decimal probabilities.
    # Top model highlighted in cyan, others dim.
    top_name = ranked[0][0] if ranked else ""
    max_name = max(len(name) for name, _ in ranked)
    top_prob = ranked[0][1]
    blank_label = " " * len(_RANKING_LABEL)

    for i, (name, prob) in enumerate(ranked):
        ratio = prob / top_prob if top_prob > 0 else 0
        full_width = ratio * _BAR_MAX_WIDTH
        full_blocks = int(full_width)
        frac = full_width - full_blocks
        partial = _BAR_BLOCKS[int(frac * 8)]
        bar = "\u2588" * full_blocks + (
            partial if partial != " " else ""
        )
        label = _RANKING_LABEL if i == 0 else blank_label

        if name == top_name:
            # Cyan highlight for the selected model.
            sys.stderr.write(
                f"\033[2m  {label}  "
                f"\033[36m{name:<{max_name}}\033[2m  "
                f"{bar:<{_BAR_MAX_WIDTH}}  {prob:.2f}\033[0m\n",
            )
        else:
            sys.stderr.write(
                f"\033[2m  {label}  "
                f"{name:<{max_name}}  "
                f"{bar:<{_BAR_MAX_WIDTH}}  {prob:.2f}\033[0m\n",
            )

    sys.stderr.write("\n")
    sys.stderr.flush()


def _stream_with_admin(
    api: ModelsAPI,
    model: str,
    messages: list[dict[str, str]],
    *,
    decorated: bool,
    admin: bool,
    session: SessionMetrics | None = None,
    **stream_kwargs: object,
) -> str:
    """Stream a response, optionally rendering the admin metrics panel.

    Args:
        api: Models API client.
        model: Model identifier.
        messages: Conversation history as a list of role/content dicts.
        decorated: If True, show spinner and formatting.
        admin: If True, render admin metrics panel after the response.
        session: Cumulative session metrics tracker.
        **stream_kwargs: Extra kwargs forwarded to the API (tools, etc.).

    Returns:
        The full response text.
    """
    if admin:
        stream = api.responses_stream_admin(
            model,
            messages=messages,
            on_header=_render_routing_header,
            **stream_kwargs,
        )
        text = _stream_response(stream, decorated=decorated)
        if session is not None:
            session.record(stream.metrics)
        _render_admin_panel(stream.metrics, session=session)
    else:
        chunks = api.responses_stream(
            model,
            messages=messages,
            **stream_kwargs,
        )
        text = _stream_response(chunks, decorated=decorated)
    return _strip_ansi(text) if decorated else text


_BAR_BLOCKS = " ▏▎▍▌▋▊▉█"
_BAR_MAX_WIDTH = 20
_RANKING_LABEL = "  ranking"


def _render_admin_panel(
    metrics: AdminMetrics,
    session: SessionMetrics | None = None,
) -> None:
    """Render a compact admin metrics panel to stderr.

    Shows operational metrics only.  Routing decisions and model
    ranking are rendered in the pre-stream header.  Architecture
    internals (provider names, judge model, candidate success/failure
    counts) are deliberately omitted.
    """
    from rich.console import Console
    from rich.panel import Panel

    content_lines: list[str] = []

    if metrics.admin_granted is False:
        content_lines.append(
            "[yellow]no admin access (check API key role)[/yellow]",
        )

    # Show which model was selected for the response.
    selected = metrics.primary_source_model
    if not selected and metrics.gnn_distribution:
        top = max(
            metrics.gnn_distribution.items(), key=lambda kv: kv[1],
        )
        selected = top[0]
    if selected:
        content_lines.append(f"[dim]selected[/dim]    {selected}")

    if metrics.total_tokens > 0:
        content_lines.append(
            f"[dim]tokens[/dim]      "
            f"{metrics.prompt_tokens:,} in \u00b7 "
            f"{metrics.completion_tokens:,} out",
        )

    lat_parts: list[str] = []
    if metrics.ttfb_ms is not None:
        lat_parts.append(f"ttfb {_format_duration(metrics.ttfb_ms)}")
    if metrics.total_ms is not None:
        lat_parts.append(f"total {_format_duration(metrics.total_ms)}")
    if lat_parts:
        content_lines.append(
            f"[dim]latency[/dim]     {' \u00b7 '.join(lat_parts)}",
        )

    if metrics.timings_ms:
        steps = [
            f"{_STEP_RENAME.get(k, k)} {_format_duration(v)}"
            for k, v in metrics.timings_ms.items()
            if k != "total_ms"
        ]
        if steps:
            content_lines.append(
                f"[dim]pipeline[/dim]    {' \u2192 '.join(steps)}",
            )

    if metrics.cost_usd is not None:
        content_lines.append(
            f"[dim]cost[/dim]        ${metrics.cost_usd:.4f}",
        )

    if session is not None and session.request_count > 1:
        total_tokens = (
            session.total_prompt_tokens + session.total_completion_tokens
        )
        content_lines.append(
            f"[dim]session[/dim]     "
            f"{session.request_count} requests \u00b7 "
            f"{total_tokens:,} tokens \u00b7 "
            f"${session.total_cost_usd:.4f}",
        )

    if not content_lines:
        return

    console = Console(stderr=True, highlight=False)
    panel = Panel(
        "\n".join(content_lines),
        title="[dim]EB1[/dim]",
        title_align="left",
        style="dim",
        expand=False,
        padding=(0, 1),
    )
    console.print(panel)
