"""Support helpers for talk runtime input and transient UI state.

Spec source: talk-prompt-toolkit-gate-review-fixes.extract-talk-session-lifecycle
Keeps talk_runtime focused on the session state machine.
"""

from __future__ import annotations

import asyncio
import json
import sys
from collections.abc import Awaitable, Callable
from typing import Any, cast

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

from anima.shell.cli_commands.talk_deps import TalkDeps


# @invar:allow shell_result: Async stdin reader, no Result needed
# @invar:allow function_size: Background task with timeout polling and shutdown coordination
# @shell_complexity: Polling loop with timeout, EOF, and cancellation handling
# @shell_orchestration: Async queue pump coordinates shell input adapters for interactive talk session
async def read_stdin_loop(
    *,
    prompt_session: object,
    input_queue: "asyncio.Queue[str | None]",
    read_input: Callable[[object], Awaitable[str | None]],
    prompt_message_provider: Callable[[], str | None] | None = None,
    resume_event: asyncio.Event | None = None,
) -> None:
    """Background task to read prompt_toolkit input and queue it."""

    async def _read_with_current_prompt() -> str | None:
        prompt_text = prompt_message_provider() if prompt_message_provider else None

        prompt_async = getattr(prompt_session, "prompt_async", None)
        if callable(prompt_async) and prompt_text is not None:
            try:
                return await cast(Any, prompt_async(message=prompt_text))
            except TypeError:
                return await read_input(cast(Any, prompt_session))
        return await read_input(cast(Any, prompt_session))

    try:
        while True:
            if resume_event is not None:
                await resume_event.wait()

            prompt_text = prompt_message_provider() if prompt_message_provider else None

            # When prompt is hidden (empty string during streaming), race
            # prompt_async against resume_event. If resume fires first
            # (streaming ended), cancel the prompt and show "--- " instead.
            # This prevents the loop from being stuck in prompt_async("")
            # after streaming completes.
            if prompt_text is not None and prompt_text == "" and resume_event is not None:
                resume_event.clear()
                read_task = asyncio.create_task(_read_with_current_prompt())
                resume_wait = asyncio.create_task(resume_event.wait())
                _done, _pending = await asyncio.wait(
                    [read_task, resume_wait],
                    return_when=asyncio.FIRST_COMPLETED,
                )
                if read_task in _done:
                    # User typed something during streaming
                    resume_wait.cancel()
                    try:
                        await resume_wait
                    except asyncio.CancelledError:
                        pass
                    message = read_task.result()
                    await input_queue.put(message)
                    if message is None:
                        return
                    if isinstance(message, str) and message.strip().lower() == "/quit":
                        await input_queue.put(None)
                        return
                else:
                    # resume_event fired (streaming ended) — cancel the hidden prompt
                    read_task.cancel()
                    try:
                        await read_task
                    except asyncio.CancelledError:
                        pass
                    # Loop back to show the visible "--- " prompt
                continue

            message = await _read_with_current_prompt()
            await input_queue.put(message)
            if resume_event is not None:
                resume_event.clear()
            if message is None:
                return
            if message.strip().lower() == "/quit":
                await input_queue.put(None)
                return
    except asyncio.CancelledError:
        pass


# @invar:allow shell_result: Async stdin helper forwards console input lines.
# @shell_complexity: Console stdin loop handles EOF/interrupt normalization and quit sentinel propagation
async def read_stdin_loop_console(
    *,
    console: Console,
    input_queue: "asyncio.Queue[str | None]",
    prompt_rich_prefix: str,
    prompt_text_provider: Callable[[], str] | None = None,
    resume_event: asyncio.Event | None = None,
) -> None:
    """Background task to read console.input and queue it."""
    try:
        while True:
            if resume_event is not None:
                await resume_event.wait()
            try:
                prompt_text = (
                    prompt_text_provider()
                    if prompt_text_provider is not None
                    else prompt_rich_prefix
                )
                message = await asyncio.to_thread(console.input, prompt_text)
            except (EOFError, KeyboardInterrupt):
                message = None
            await input_queue.put(message)
            if resume_event is not None:
                resume_event.clear()
            if message is None:
                return
            if message.strip().lower() == "/quit":
                await input_queue.put(None)
                return
    except asyncio.CancelledError:
        pass


# @invar:allow shell_result: Async cleanup helper, no Result needed
# @shell_orchestration: Async stdin task lifecycle helper is shell event-loop orchestration glue
async def cleanup_stdin(stdin_task: "asyncio.Task[None] | None") -> None:
    """Cancel and await stdin reader task."""
    if stdin_task is not None and not stdin_task.done():
        stdin_task.cancel()
        try:
            await stdin_task
        except (asyncio.TimeoutError, asyncio.CancelledError):
            pass


# @invar:allow shell_result: Terminal capability probe used by transient CLI indicator rendering
def supports_braille_spinner(console: Console) -> bool:
    """Return whether the current output encoding supports braille glyphs."""
    encoding = getattr(console.file, "encoding", None) or sys.stdout.encoding or "utf-8"
    try:
        "\u28ff".encode(encoding)
        return True
    except (LookupError, UnicodeEncodeError):
        return False


# @invar:allow shell_result: Formatting helper for transient thinking indicator text
def thinking_text(*, queued_count: int, supports_braille: bool) -> str:
    """Build thinking indicator text with optional queued count."""
    spinner = "\u28ff" if supports_braille else "..."
    queued_suffix = f" ({queued_count} queued)" if queued_count > 0 else ""
    return f"{spinner} thinking...{queued_suffix}"


def render_thinking_indicator(
    *,
    console: Console,
    json_output: bool,
    deps: TalkDeps,
    queued_count: int,
    supports_braille: bool,
) -> None:
    """Render thinking indicator updates for human and JSON output modes."""
    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        payload: dict[str, object] = {"event": "thinking"}
        if queued_count > 0:
            payload["queued"] = queued_count
        console.file.write(json.dumps(payload) + "\n")
        return

    indicator = thinking_text(
        queued_count=queued_count,
        supports_braille=supports_braille,
    )

    def _emit() -> None:
        console.file.write("\r\033[K")
        console.file.flush()
        console.print(Text(indicator, style="dim"))

    deps.emit_with_prompt_coordination(emit=_emit)


__all__ = [
    "cleanup_stdin",
    "read_stdin_loop",
    "read_stdin_loop_console",
    "render_thinking_indicator",
    "supports_braille_spinner",
    "thinking_text",
]
