"""Session lifecycle helpers for the talk command.

Spec source: talk-prompt-toolkit-gate-review-fixes.extract-talk-session-lifecycle
Extracted from talk_cmd.py to isolate lifecycle and approval flow logic.
"""

# @invar:allow file_size: talk session aggregates approval, child routing, switch, and interact handlers in one dispatch module

from __future__ import annotations

import json
from collections.abc import Callable
from typing import Any

from rich.console import Console
from rich.markdown import Markdown
from rich.markup import escape as rich_escape
from returns.result import Failure

from anima.core.lifecycle import LifecycleEvent
from anima.core.schemas import AgentState
from anima.core.talk_history import (
    filter_conversation_events,
    format_conversation_turns,
)
from anima.shell.cli_commands.talk_runtime import (
    cleanup_stdin,
    read_stdin_loop,
    read_stdin_loop_console,
    run_talk_session,
    wait_for_interact_or_input_or_stream,
)
from anima.shell.cli_commands.talk_deps import TalkDeps
from anima.shell.state import AgentStateData


# @invar:allow shell_result: Reads local state log for talk prompt context
# @shell_orchestration: Uses read_log shell API (indirect filesystem I/O) for prompt context
# @shell_complexity: Approval-reason scan branches filter log schema variants defensively
def latest_approval_reason(agent_id: str, *, deps: TalkDeps) -> str | None:
    """Return the most recent approval reason from log entries."""
    log_result = deps.read_log(agent_id)
    if isinstance(log_result, Failure):
        return None
    for entry in reversed(log_result.unwrap()):
        if not isinstance(entry, dict) or entry.get("event") not in {
            "approval_requested",
            "request_approval",
        }:
            continue
        details = entry.get("details")
        if isinstance(details, dict):
            reason = details.get("reason")
            if isinstance(reason, str) and reason.strip():
                return reason.strip()
    return None


# @invar:allow shell_result: Extracted slash-command helper returns handled-bool to preserve dispatcher contract
# @shell_complexity: Approval branch retains existing transition and error printing behavior
def handle_inline_approve(
    *,
    state_data: AgentStateData,
    agent_id: str,
    argument: str | None,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle inline /approve branch for awaiting-approval agents."""
    response_write_result = deps.write_approval_response(
        state_data.agent_id, True, argument
    )
    if isinstance(response_write_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INTERNAL",
            numeric_code=10,
            message=f"Failed to write approval response: {response_write_result.failure()}",
            details={"agent_id": agent_id, "state": state_data.state.value},
        )
        return True

    transition_result = deps.transition_state(
        state_data.agent_id, LifecycleEvent.approve, None, None
    )
    if isinstance(transition_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INTERNAL",
            numeric_code=10,
            message=f"Failed to transition state: {transition_result.failure()}",
            details={"agent_id": agent_id, "state": state_data.state.value},
        )
        return True

    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        console.file.write(
            json.dumps(
                {
                    "event": "approval_applied",
                    "agent_id": agent_id,
                    "state": AgentState.running.value,
                    "approved": True,
                    "note": argument,
                }
            )
            + "\n"
        )
    else:
        note_suffix = f" Note: {argument}" if argument else ""
        console.print(
            "[green]Approved inline. Agent resumed to running state.[/green]"
            f"[dim]{note_suffix}[/dim]"
        )
    return True


# @invar:allow shell_result: Extracted slash-command helper returns handled-bool to preserve dispatcher contract
def handle_inline_reject(
    *,
    state_data: AgentStateData,
    agent_id: str,
    argument: str | None,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle inline /reject branch for awaiting-approval agents."""
    transition_result = deps.transition_state(
        state_data.agent_id,
        LifecycleEvent.reject,
        None,
        None,
    )
    if isinstance(transition_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INTERNAL",
            numeric_code=10,
            message=f"Failed to transition state: {transition_result.failure()}",
            details={"agent_id": agent_id, "state": state_data.state.value},
        )
        return True

    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        console.file.write(
            json.dumps(
                {
                    "event": "approval_applied",
                    "agent_id": agent_id,
                    "state": AgentState.running.value,
                    "rejected": True,
                    "reason": argument,
                }
            )
            + "\n"
        )
    else:
        reason_suffix = f" Reason: {argument}" if argument else ""
        console.print(
            "[yellow]Rejected. Reason sent to agent.[/yellow]"
            f"[dim]{reason_suffix}[/dim]"
        )
    return True


# @shell_complexity: Local slash command dispatch with lifecycle transitions and state checks
# @invar:allow shell_result: REPL local command dispatcher returns handled-bool, not I/O Result
# @shell_orchestration: Inline slash dispatch preserves shell REPL transition wiring behavior
def handle_inline_slash_command(
    *,
    message: str,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle local talk slash commands."""
    command, argument = deps.parse_slash_command(message)
    if command is None:
        return False

    if command == "help":
        deps.print_help(json_output=json_output, console=console)
        return True

    state_result = deps.read_state(agent_id)
    if isinstance(state_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="AGENT_NOT_FOUND",
            numeric_code=300,
            message=f"Failed to read agent state: {state_result.failure()}",
            details={"agent_id": agent_id},
        )
        return True

    state_data = state_result.unwrap()
    if state_data.state != AgentState.awaiting_approval:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_STATE",
            numeric_code=1,
            message=f"Agent not awaiting approval (state: {state_data.state.value})",
            details={"agent_id": agent_id, "state": state_data.state.value},
        )
        return True

    if command == "approve":
        return handle_inline_approve(
            state_data=state_data,
            agent_id=agent_id,
            argument=argument,
            json_output=json_output,
            console=console,
            deps=deps,
        )

    return handle_inline_reject(
        state_data=state_data,
        agent_id=agent_id,
        argument=argument,
        json_output=json_output,
        console=console,
        deps=deps,
    )


# @invar:allow shell_result: Child resolution helper returns target info, no Result needed
# @shell_orchestration: Resolves child_id prefix to full ID from parent's children list
def _resolve_child_target(
    *,
    argument: str | None,
    agent_id: str,
    deps: TalkDeps,
) -> tuple[str | None, str | None]:
    """Parse child target prefix from argument and resolve to full child ID.

    Args:
        argument: The argument string from the slash command.
        agent_id: The parent agent ID.
        deps: Talk dependencies.

    Returns:
        Tuple of (resolved_child_id, remaining_note). If no child prefix found,
        returns (None, argument).
    """
    if argument is None:
        return (None, None)

    parts = argument.split(None, 1)
    if not parts:
        return (None, None)

    prefix = parts[0]
    if not prefix.startswith("inst_"):
        return (None, argument)

    remaining_note = parts[1] if len(parts) > 1 else None

    # Resolve prefix to full child ID
    parent_state_result = deps.read_state(agent_id)
    if isinstance(parent_state_result, Failure):
        return (None, argument)

    parent_state = parent_state_result.unwrap()
    matches = [cid for cid in parent_state.children if cid.startswith(prefix)]

    if len(matches) == 1:
        return (matches[0], remaining_note)
    if len(matches) > 1:
        # Ambiguous prefix, try exact match
        if prefix in matches:
            return (prefix, remaining_note)
        return (None, argument)

    # No match found — prefix might be a full ID not in children list
    return (None, argument)


# @invar:allow shell_result: Direct filesystem approval for child agents, no MCP needed
# @shell_complexity: Child approval uses direct state writes since child may not have accessible MCP socket
# @shell_orchestration: Cross-process approval routing delegates to existing inline handlers
def _handle_child_approval(
    *,
    child_id: str,
    command: str,
    note: str | None,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle /approve or /reject targeting a specific child agent via direct state writes.

    Args:
        child_id: The child agent instance ID.
        command: Either "approve" or "reject".
        note: Optional note/reason for the decision.
        json_output: Whether to emit JSON output.
        console: Rich console for output.
        deps: Talk dependencies.

    Returns:
        True if the command was handled (even if with error).
    """
    child_state_result = deps.read_state(child_id)
    if isinstance(child_state_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="AGENT_NOT_FOUND",
            numeric_code=300,
            message=f"Failed to read child agent state: {child_state_result.failure()}",
            details={"child_id": child_id},
        )
        return True

    child_state = child_state_result.unwrap()
    if child_state.state != AgentState.awaiting_approval:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_STATE",
            numeric_code=1,
            message=f"Child agent not awaiting approval (state: {child_state.state.value})",
            details={"child_id": child_id, "state": child_state.state.value},
        )
        return True

    if command == "approve":
        return handle_inline_approve(
            state_data=child_state,
            agent_id=child_id,
            argument=note,
            json_output=json_output,
            console=console,
            deps=deps,
        )

    return handle_inline_reject(
        state_data=child_state,
        agent_id=child_id,
        argument=note,
        json_output=json_output,
        console=console,
        deps=deps,
    )


# @invar:allow shell_result: Finds children awaiting approval for auto-targeting
# @shell_orchestration: Polls child states for approval auto-resolution
def _find_children_awaiting(
    agent_id: str,
    deps: TalkDeps,
) -> list[str]:
    """Return child IDs that are currently in awaiting_approval state.

    Args:
        agent_id: The parent agent ID.
        deps: Talk dependencies.

    Returns:
        List of child instance IDs in awaiting_approval state.
    """
    parent_result = deps.read_state(agent_id)
    if isinstance(parent_result, Failure):
        return []

    parent = parent_result.unwrap()
    awaiting: list[str] = []
    for child_id in parent.children:
        child_result = deps.read_state(child_id)
        if isinstance(child_result, Failure):
            continue
        if child_result.unwrap().state == AgentState.awaiting_approval:
            awaiting.append(child_id)
    return awaiting


# @invar:allow shell_result: Inline MCP approval bridge returns handled-bool for REPL command dispatcher
# @invar:allow function_size: Inline MCP approval path preserves command validation, payload parsing, and output parity with child routing
# @shell_complexity: Inline MCP approval bridge handles validation, transport errors, child routing, and JSON/human output parity
# @shell_orchestration: Inline approval bridge preserves state checks and delegates final transition to MCP handler or child direct writes
async def handle_inline_approval_via_mcp(
    *,
    session: object,
    message: str,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle /approve and /reject via MCP or direct state writes for child agents.

    Routing logic:
    - ``/approve inst_xxx [note]`` targets the child matching prefix ``inst_xxx``.
    - Bare ``/approve [note]``: if main agent is awaiting_approval, targets main agent.
      Otherwise if exactly 1 child is awaiting, auto-targets that child.
      Otherwise if multiple children are awaiting, shows disambiguation error.

    Args:
        session: MCP client session (for main agent approval via MCP).
        message: The raw user input message.
        agent_id: The main (parent) agent instance ID.
        json_output: Whether to emit JSON output.
        console: Rich console for output.
        deps: Talk dependencies for state/log access.

    Returns:
        True if the command was handled (even if with error), False if not an approve/reject command.
    """
    command, argument = deps.parse_slash_command(message)
    if command not in {"approve", "reject"}:
        return False

    # Check for explicit child target in argument
    child_target, remaining_note = _resolve_child_target(
        argument=argument,
        agent_id=agent_id,
        deps=deps,
    )

    if child_target is not None:
        return _handle_child_approval(
            child_id=child_target,
            command=command,
            note=remaining_note,
            json_output=json_output,
            console=console,
            deps=deps,
        )

    # No explicit child target — check main agent first
    state_result = deps.read_state(agent_id)
    if isinstance(state_result, Failure):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="AGENT_NOT_FOUND",
            numeric_code=300,
            message=f"Failed to read agent state: {state_result.failure()}",
            details={"agent_id": agent_id},
        )
        return True

    state_data = state_result.unwrap()

    # If main agent is awaiting approval, route to main agent via MCP (existing path)
    if state_data.state == AgentState.awaiting_approval:
        return await _handle_main_agent_approval_via_mcp(
            session=session,
            command=command,
            argument=argument,
            agent_id=agent_id,
            json_output=json_output,
            console=console,
            deps=deps,
        )

    # Main agent is NOT awaiting approval — try auto-targeting a child
    children_awaiting = _find_children_awaiting(agent_id, deps)

    if len(children_awaiting) == 1:
        return _handle_child_approval(
            child_id=children_awaiting[0],
            command=command,
            note=argument,
            json_output=json_output,
            console=console,
            deps=deps,
        )

    if len(children_awaiting) > 1:
        prefixes = ", ".join(cid[:8] for cid in children_awaiting)
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="AMBIGUOUS_TARGET",
            numeric_code=2,
            message=f"Multiple children awaiting approval. Specify: /{command} <child_id> (candidates: {prefixes})",
            details={"agent_id": agent_id, "children": prefixes},
        )
        return True

    # No one is awaiting approval
    deps.print_inline_command_error(
        json_output=json_output,
        console=console,
        code="INVALID_STATE",
        numeric_code=1,
        message=f"Agent not awaiting approval (state: {state_data.state.value})",
        details={"agent_id": agent_id, "state": state_data.state.value},
    )
    return True


# @invar:allow shell_result: MCP approval path for main agent, preserved from original
# @shell_complexity: MCP call with error handling and JSON/human output parity
async def _handle_main_agent_approval_via_mcp(
    *,
    session: object,
    command: str,
    argument: str | None,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle /approve and /reject for the main agent via MCP tools.

    Args:
        session: MCP client session.
        command: Either "approve" or "reject".
        argument: Optional note/reason.
        agent_id: The main agent instance ID.
        json_output: Whether to emit JSON output.
        console: Rich console for output.
        deps: Talk dependencies.

    Returns:
        True if handled, False on transport failure (caller should fall through).
    """
    session_any = session  # type: Any
    tool_name = "anima.approve" if command == "approve" else "anima.reject"
    try:
        tool_result = await session_any.call_tool(tool_name, {"note": argument})
    except Exception:
        return False

    if not tool_result.content:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_RESPONSE",
            numeric_code=10,
            message="Malformed approval response from agent",
            details={"agent_id": agent_id},
        )
        return True

    first = tool_result.content[0]
    first_text = getattr(first, "text", None)
    if not isinstance(first_text, str):
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_RESPONSE",
            numeric_code=10,
            message="Unexpected approval response type from agent",
            details={"agent_id": agent_id},
        )
        return True

    try:
        payload_obj = json.loads(first_text)
    except json.JSONDecodeError:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_RESPONSE",
            numeric_code=10,
            message="Malformed approval response from agent",
            details={"agent_id": agent_id},
        )
        return True

    if isinstance(payload_obj, dict) and isinstance(payload_obj.get("error"), dict):
        error_payload = payload_obj["error"]
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code=str(error_payload.get("code", "INTERNAL")),
            numeric_code=int(error_payload.get("numeric_code", 10)),
            message=str(
                error_payload.get("message", "Approval command failed unexpectedly")
            ),
            details={"agent_id": agent_id},
        )
        return True

    if command == "approve":
        if json_output:
            console.file.write(
                json.dumps(
                    {
                        "event": "approval_applied",
                        "agent_id": agent_id,
                        "state": AgentState.running.value,
                        "approved": True,
                        "note": argument,
                    }
                )
                + "\n"
            )
        else:
            note_suffix = f" Note: {argument}" if argument else ""
            console.print(
                "[green]Approved inline. Agent resumed to running state.[/green]"
                f"[dim]{note_suffix}[/dim]"
            )
        return True

    if json_output:
        console.file.write(
            json.dumps(
                {
                    "event": "approval_applied",
                    "agent_id": agent_id,
                    "state": AgentState.running.value,
                    "rejected": True,
                    "reason": argument,
                }
            )
            + "\n"
        )
    else:
        reason_suffix = f" Reason: {argument}" if argument else ""
        console.print(
            "[yellow]Rejected. Reason sent to agent.[/yellow]"
            f"[dim]{reason_suffix}[/dim]"
        )
    return True


# @invar:allow shell_result: Reads local state log for history replay
# @shell_orchestration: Uses read_log shell API (filesystem I/O) for history
# @shell_complexity: Log filtering and branching for history format variants
def replay_talk_history(
    *,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
    persona_name: str = "agent",
) -> None:
    """Replay prior conversation history from log.jsonl before live mode."""
    log_result = deps.read_log(agent_id)
    if isinstance(log_result, Failure):
        return

    entries = log_result.unwrap()
    conversation_entries = filter_conversation_events(entries)
    turns = format_conversation_turns(conversation_entries)
    if not turns:
        return

    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        console.file.write(
            json.dumps(
                {
                    "event": "history_replay",
                    "agent_id": agent_id,
                    "turns": [
                        {
                            "speaker": speaker,
                            "turn_number": turn_num,
                            "text": text,
                        }
                        for speaker, turn_num, text in turns
                    ],
                }
            )
            + "\n"
        )
    else:
        for speaker, _turn_num, text in turns:
            if speaker == "user":
                console.print(f"[dim]--- {rich_escape(text)}[/dim]")
            else:
                console.print(f"\n[dim bold]{persona_name}[/dim bold]")
                console.print(f"[dim]{rich_escape(text)}[/dim]")

    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        console.file.write(json.dumps({"event": "history_replay_complete"}) + "\n")
    else:
        console.print("\n[dim]--------- live ---------[/dim]\n")


# @invar:allow shell_result: MCP stop-turn helper, I/O via MCP session
# @shell_complexity: MCP call with error handling and output format dispatch
async def execute_stop_turn(
    session: object,
    json_output: bool,
    console: Console,
    *,
    deps: TalkDeps,
) -> None:
    """Call anima.stop_turn via MCP and print the result."""
    from mcp.types import TextContent

    session_any = session  # type: Any
    stop_result = await session_any.call_tool("anima.stop_turn", {})
    if stop_result.content:
        first = stop_result.content[0]
        if isinstance(first, TextContent):
            if json_output:
                try:
                    parsed_stop = json.loads(first.text)
                    # Use pure json.dumps() for machine-readable output - no ANSI escapes.
                    # console.print_json() emits ANSI codes when force_terminal=True.
                    deps.emit_with_prompt_coordination(
                        emit=lambda: (
                            console.file.write(json.dumps(parsed_stop) + "\n"),
                            None,
                        )[1],
                    )
                except (json.JSONDecodeError, ValueError):
                    deps.print_inline_command_error(
                        json_output=json_output,
                        console=console,
                        code="INVALID_RESPONSE",
                        numeric_code=10,
                        message="Malformed stop response from agent",
                        details={"error": "invalid JSON"},
                    )
            else:
                deps.emit_with_prompt_coordination(
                    emit=lambda: console.print(
                        "[yellow]Stop requested. "
                        "Current agent turn will stop at the next node boundary.[/yellow]"
                    ),
                )


# @invar:allow shell_result: Dispatches MCP response content to console output
# @invar:allow function_size: Interactive response handler preserves JSON/text parity and approval prompt semantics
# @shell_complexity: MCP response type dispatch requires branching
def handle_interact_result(
    mcp_result: object,
    agent_id: str,
    json_output: bool,
    console: Console,
    was_awaiting_approval: bool,
    print_approval_prompt: Callable[[str | None], None],
    *,
    deps: TalkDeps,
    suppress_display: bool = False,
    persona_name: str = "agent",
) -> tuple[bool, bool]:
    """Parse an anima.interact MCP result and print output."""
    from mcp.types import TextContent

    mcp_result_any = mcp_result  # type: Any
    clear_thinking_indicator(
        json_output=json_output,
        console=console,
        deps=deps,
        suppress_display=suppress_display,
    )

    if not mcp_result_any.content:
        if json_output:
            # Use pure json.dumps() for machine-readable output - no ANSI escapes.
            # console.print_json() emits ANSI codes when force_terminal=True.
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "EMPTY_RESPONSE",
                            "numeric_code": 10,
                            "message": "No response from agent",
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print("[red]Error: No response from agent[/red]")
        return (False, was_awaiting_approval)

    first = mcp_result_any.content[0]
    if not isinstance(first, TextContent):
        if json_output:
            # Use pure json.dumps() for machine-readable output - no ANSI escapes.
            # console.print_json() emits ANSI codes when force_terminal=True.
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "INVALID_RESPONSE",
                            "numeric_code": 10,
                            "message": "Unexpected MCP response type",
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print("[red]Error: Unexpected response from agent[/red]")
        return (False, was_awaiting_approval)

    try:
        payload_obj = json.loads(first.text)
    except json.JSONDecodeError:
        payload_obj = {}

    if isinstance(payload_obj, dict) and isinstance(payload_obj.get("error"), dict):
        error_payload = payload_obj["error"]
        error_code = str(error_payload.get("code", "INTERNAL"))
        error_message = str(
            error_payload.get("message", "Agent returned an error response")
        )
        if json_output:
            # Use pure json.dumps() for machine-readable output - no ANSI escapes.
            # console.print_json() emits ANSI codes when force_terminal=True.
            console.file.write(json.dumps(payload_obj) + "\n")
        else:
            console.print(
                f"[yellow]Agent error ({error_code}): {error_message}[/yellow]"
            )
        return (True, was_awaiting_approval)

    parsed = deps.parse_talk_response(first.text)
    response = parsed["response"] or ""
    turn_number = parsed["turn_number"] or "0"

    new_was_awaiting = was_awaiting_approval
    if not json_output:
        payload = payload_obj if isinstance(payload_obj, dict) else {}
        meta = (
            payload.get("_meta")
            if isinstance(payload, dict) and isinstance(payload.get("_meta"), dict)
            else {}
        )
        response_state = payload.get("state") if isinstance(payload, dict) else None
        if response_state is None and isinstance(meta, dict):
            response_state = meta.get("state")
        if (
            response_state == AgentState.awaiting_approval.value
            and not was_awaiting_approval
        ):
            reason = (
                payload.get("approval_reason") if isinstance(payload, dict) else None
            )
            if reason is None and isinstance(meta, dict):
                reason = meta.get("approval_reason")
            print_approval_prompt(reason if isinstance(reason, str) else None)
            new_was_awaiting = True

    if not suppress_display:
        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 = {
                "agent_id": agent_id,
                "response": response,
                "turn_number": turn_number,
            }
            console.file.write(json.dumps(payload) + "\n")
        else:
            deps.emit_with_prompt_coordination(
                emit=lambda: console.print(f"[bold]{persona_name}[/bold]"),
            )
            deps.emit_with_prompt_coordination(
                emit=lambda: console.print(Markdown(response)),
            )

    return (False, new_was_awaiting)


def clear_thinking_indicator(
    *,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
    suppress_display: bool = False,
) -> None:
    """Finalize transient thinking indicator output for current interact turn."""
    if json_output:
        # Use pure json.dumps() for machine-readable output - no ANSI escapes.
        # console.print_json() emits ANSI codes when force_terminal=True.
        console.file.write(json.dumps({"event": "thinking_complete"}) + "\n")
        return

    if suppress_display:
        return

    deps.emit_with_prompt_coordination(
        emit=lambda: _clear_active_line(console),
    )


def _clear_active_line(console: Console) -> None:
    """Clear active terminal line used for transient thinking indicator."""
    console.file.write("\r\033[K")
    console.file.flush()


# @invar:allow shell_result: Inline MCP switch handler returns handled-bool for REPL command dispatcher
# @shell_complexity: MCP call with error handling and JSON/human output parity
async def handle_inline_switch(
    *,
    message: str,
    agent_id: str,
    session: object,
    json_output: bool,
    console: Console,
    deps: TalkDeps,
) -> bool:
    """Handle /switch <persona_ref> via MCP ctl_switch_persona tool.

    Parses the persona_ref argument from the message, calls the MCP tool,
    and displays the result.

    Args:
        message: The raw user input message.
        agent_id: The agent instance ID.
        session: MCP client session.
        json_output: Whether to emit JSON output.
        console: Rich console for output.
        deps: Talk dependencies.

    Returns:
        True if the command was handled (even if with error).
    """
    _, persona_ref = deps.parse_slash_command(message)
    if not persona_ref:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="INVALID_ARGUMENT",
            numeric_code=2,
            message="Usage: /switch <persona_ref>",
            details={"agent_id": agent_id},
        )
        return True

    session_any: Any = session
    try:
        tool_result = await session_any.call_tool(
            "ctl_switch_persona", {"persona_ref": persona_ref}
        )
    except Exception as exc:
        deps.print_inline_command_error(
            json_output=json_output,
            console=console,
            code="SWITCH_FAILED",
            numeric_code=10,
            message=f"Failed to call ctl_switch_persona: {exc}",
            details={"agent_id": agent_id, "persona_ref": persona_ref},
        )
        return True

    # Parse the MCP result for error responses
    if tool_result.content:
        first = tool_result.content[0]
        first_text = getattr(first, "text", None)
        if isinstance(first_text, str):
            try:
                payload = json.loads(first_text)
                if isinstance(payload, dict) and isinstance(
                    payload.get("error"), dict
                ):
                    error_payload = payload["error"]
                    deps.print_inline_command_error(
                        json_output=json_output,
                        console=console,
                        code=str(error_payload.get("code", "SWITCH_FAILED")),
                        numeric_code=int(error_payload.get("numeric_code", 10)),
                        message=str(
                            error_payload.get("message", "Persona switch failed")
                        ),
                        details={"agent_id": agent_id, "persona_ref": persona_ref},
                    )
                    return True
            except (json.JSONDecodeError, ValueError):
                pass

    # Success
    if json_output:
        console.file.write(
            json.dumps(
                {
                    "event": "persona_switch_requested",
                    "agent_id": agent_id,
                    "persona_ref": persona_ref,
                }
            )
            + "\n"
        )
    else:
        console.print(
            f"[green]Switching persona to {rich_escape(persona_ref)}. "
            f"Takes effect at next turn.[/green]"
        )
    return True
