"""Interactive session command: talk.

Spec source: plan step ``cli-refactor.cli-refactor-verify-cli-refactor-fix``
extracts talk command from cli.py without behavior changes.
"""

# @invar:allow function_size: CLI command implementations with interactive loop
# @invar:allow shell_result: interactive talk command returns shell exit status for transport and approval errors

from __future__ import annotations

from dataclasses import replace
import asyncio
import json
import sys
from functools import partial
from pathlib import Path

from deal import post, pre
from rich.console import Console
from returns.result import Failure, Success

from anima.app.session_service import DefaultSessionService, TalkAttachRequest
from anima.core.schemas import AgentState
from anima.shell.cli_commands._shared import resolve_agent
from anima.shell.cli_commands.talk_display import (
    emit_with_prompt_coordination,
    format_session_header,
    handle_connection_error,
    print_detach,
    print_help,
    print_inline_command_error,
)
from anima.shell.cli_commands.talk_helpers import (
    parse_talk_response,
    suppress_invalid_state_error,
)
from anima.shell.cli_commands.talk_deps import TalkDeps, default_deps
from anima.shell.cli_commands.talk_input import (
    PROMPT_RICH_PREFIX,
    create_prompt_session,
    read_input,
)
from anima.shell.cli_commands.talk_runtime import (
    cleanup_stdin,
    read_stdin_loop,
    read_stdin_loop_console,
    run_talk_session as run_talk_session_with_deps,
    wait_for_interact_or_input_or_stream,
)
from anima.shell.cli_commands.talk_session import (
    execute_stop_turn as _core_execute_stop_turn,
    handle_inline_approve,
    handle_inline_reject,
    handle_inline_slash_command as _core_handle_inline_slash_command,
    handle_interact_result,
    latest_approval_reason as _core_latest_approval_reason,
    replay_talk_history as _core_replay_talk_history,
)
from anima.shell.cli_commands.talk_streaming import (
    consume_sse_stream,
    probe_streaming_support,
)
from anima.shell.state import (
    get_state_dir,
    is_agent_process_alive,
    read_log,
    read_state,
    reap_stale_agent,
    ShellSessionProjectionPort,
)

EXIT_AGENT_NOT_FOUND = 2
STYLE_ERROR = "[red]{}[/red]"
STYLE_WARNING = "[yellow]{}[/yellow]"
STYLE_SUCCESS = "[green]{}[/green]"
STYLE_DIM = "[dim]{}[/dim]"


from anima.shell.cli_commands.talk_display import SLASH_HELP_LINES


# @invar:allow shell_result: Compatibility wrapper normalizes legacy keyword arguments
# @shell_orchestration: Compatibility wrapper preserves shell dependency injection for approval reason lookup
def latest_approval_reason(
    agent_id: str,
    *,
    deps: TalkDeps | None = None,
    read_log_func=None,
    **_kwargs: object,
) -> str | None:
    active_deps = deps if deps is not None else default_deps()
    if read_log_func is not None:
        active_deps = replace(active_deps, read_log=read_log_func)

    return _core_latest_approval_reason(agent_id, deps=active_deps)


# @invar:allow shell_result: Compatibility wrapper normalizes legacy keyword arguments
# @shell_orchestration: Compatibility wrapper preserves shell slash parser injection surface
def handle_inline_slash_command(
    *,
    message: str,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps | None = None,
    parse_slash_command_func=None,
    **_kwargs: object,
) -> bool:
    active_deps = deps if deps is not None else default_deps()
    if parse_slash_command_func is not None:
        active_deps = replace(
            active_deps,
            parse_slash_command=lambda line: parse_slash_command_func(line),
        )

    return _core_handle_inline_slash_command(
        message=message,
        agent_id=agent_id,
        json_output=json_output,
        console=console,
        deps=active_deps,
    )


# @invar:allow shell_result: Compatibility wrapper normalizes legacy keyword arguments
# @shell_orchestration: Compatibility wrapper preserves async stop-turn shell dependency hooks
async def execute_stop_turn(
    session: object,
    json_output: bool,
    console: Console,
    *,
    deps: TalkDeps | None = None,
    emit_with_prompt_coordination_func=None,
    **_kwargs: object,
) -> None:
    active_deps = deps if deps is not None else default_deps()
    if emit_with_prompt_coordination_func is not None:
        active_deps = replace(
            active_deps,
            emit_with_prompt_coordination=emit_with_prompt_coordination_func,
        )

    await _core_execute_stop_turn(
        session,
        json_output,
        console,
        deps=active_deps,
    )


# @invar:allow shell_result: Compatibility wrapper normalizes legacy keyword arguments
# @shell_orchestration: Compatibility wrapper preserves shell log-reader injection for history replay
def replay_talk_history(
    *,
    agent_id: str,
    json_output: bool,
    console: Console,
    deps: TalkDeps | None = None,
    read_log_func=None,
    persona_name: str = "agent",
    **_kwargs: object,
) -> None:
    active_deps = deps if deps is not None else default_deps()
    if read_log_func is not None:
        active_deps = replace(active_deps, read_log=read_log_func)

    _core_replay_talk_history(
        agent_id=agent_id,
        json_output=json_output,
        console=console,
        deps=active_deps,
        persona_name=persona_name,
    )


# Legacy test/back-compat aliases without wrapper function definitions.
_print_inline_command_error = print_inline_command_error
_latest_approval_reason = partial(latest_approval_reason, deps=default_deps())
_handle_inline_approve = partial(handle_inline_approve, deps=default_deps())
_handle_inline_reject = partial(handle_inline_reject, deps=default_deps())
_execute_stop_turn = partial(_core_execute_stop_turn, deps=default_deps())
_handle_interact_result = partial(handle_interact_result, deps=default_deps())
_emit_with_prompt_coordination = emit_with_prompt_coordination
_print_help = partial(print_help, slash_help_lines=SLASH_HELP_LINES)
_print_detach = print_detach
_handle_connection_error = handle_connection_error
_make_talk_deps = default_deps
_read_stdin_loop = partial(read_stdin_loop, read_input=read_input)
_read_stdin_loop_console = partial(
    read_stdin_loop_console,
    prompt_rich_prefix=PROMPT_RICH_PREFIX,
)
_wait_for_interact_or_input_or_stream = partial(
    wait_for_interact_or_input_or_stream,
    deps=default_deps(),
)
_cleanup_stdin = cleanup_stdin

# Backward-compatible module export for tests importing run_talk_session directly.
run_talk_session = partial(run_talk_session_with_deps, deps=default_deps())


# @invar:allow shell_result: Pure helper, no I/O, no Result needed
# @shell_orchestration: Pure classification helper scoped to talk CLI command module
@pre(lambda state: isinstance(state, AgentState))
@post(lambda result: result in {"interactive", "terminal", "non_interactive"})
def categorize_agent_state(state: AgentState) -> str:
    """Categorize an agent state for talk command routing.

    >>> categorize_agent_state(AgentState.running)
    'interactive'
    >>> categorize_agent_state(AgentState.completed)
    'terminal'
    >>> categorize_agent_state(AgentState.pending)
    'non_interactive'
    """
    if state in {
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }:
        return "interactive"

    if state in {AgentState.completed, AgentState.failed, AgentState.killed}:
        return "terminal"

    return "non_interactive"


# @invar:allow shell_result: Pure path builder helper retained in shell for command-local wiring
@pre(lambda state_dir, agent_id: len(state_dir) > 0 and len(agent_id) > 0)
@post(lambda result: result.endswith("mcp.sock"))
def build_socket_path(state_dir: str, agent_id: str) -> str:
    """Build the MCP socket path for an agent.

    >>> build_socket_path('/tmp/anima', 'agent-123')
    '/tmp/anima/agents/agent-123/mcp.sock'
    """
    return str(Path(state_dir) / "agents" / agent_id / "mcp.sock")


# @invar:allow shell_result: Pure validation helper retained in shell for command-local state handling
# @shell_orchestration: Pure validation helper scoped to talk CLI command module
@pre(lambda state: isinstance(state, AgentState))
@post(lambda result: isinstance(result, tuple) and len(result) == 2)
def validate_interactive_state(state: AgentState) -> tuple[bool, str]:
    """Validate whether agent state supports interactive talk.

    >>> validate_interactive_state(AgentState.running)
    (True, '')
    >>> validate_interactive_state(AgentState.completed)
    (False, 'Agent is in terminal state: completed')
    >>> validate_interactive_state(AgentState.pending)
    (False, 'Agent not interactive (state: pending)')
    """
    category = categorize_agent_state(state)
    if category == "interactive":
        return (True, "")
    if category == "terminal":
        return (False, f"Agent is in terminal state: {state.value}")
    return (False, f"Agent not interactive (state: {state.value})")


# @invar:allow entry_point_too_thick: Click command with interactive REPL and signal handling
# @invar:allow function_size: CLI command with interactive loop
# @shell_complexity: Socket validation, interactive state checks, and async session launch
def talk_impl(
    agent_id: str,
    json_output: bool,
    console: Console,
) -> None:
    """Implementation for anima talk command.

    Connects to the agent's MCP socket and provides a REPL for sending
    messages via anima.interact(). Ctrl-C to detach (does not kill agent).

    Exit codes: 0 success, 1 error (agent not interactive, connection error), 2 not found.
    """
    deps = default_deps()

    # 1. Prefix resolution
    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )
    instance_id = state.instance_id
    persona_name = state.persona_name or "agent"

    session_service = DefaultSessionService(
        projection_port=ShellSessionProjectionPort()
    )

    # 3.5. Stale state detection: check if running agent's process is alive
    # Spec source: vectl step stale-state-detection.talk-guard
    if state.state == AgentState.running:
        alive_result = deps.is_agent_process_alive(instance_id)
        if isinstance(alive_result, Success):
            is_alive = alive_result.unwrap()
            if not is_alive:
                # Process is dead - reap stale state
                reap_result = deps.reap_stale_agent(instance_id)
                if isinstance(reap_result, Failure):
                    # Reap failed - log and exit
                    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": "REAP_FAILED",
                                        "numeric_code": 317,
                                        "message": f"Failed to reap stale agent: {reap_result.failure()}",
                                        "details": {"agent_id": instance_id},
                                    }
                                }
                            )
                            + "\n"
                        )
                    else:
                        console.print(
                            STYLE_ERROR.format(
                                "Error: Agent process terminated but reap failed: "
                                f"{reap_result.failure()}"
                            )
                        )
                    sys.exit(1)

                # Successfully reaped - show stale state message
                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": "stale_state_updated",
                                "agent_id": instance_id,
                                "old_state": AgentState.running.value,
                                "new_state": AgentState.failed.value,
                                "message": "Agent process terminated unexpectedly",
                            }
                        )
                        + "\n"
                    )
                else:
                    console.print(
                        STYLE_WARNING.format(
                            f"Agent {instance_id[:8]} process has terminated."
                        )
                    )
                    console.print(
                        STYLE_DIM.format(
                            "Stale state cleaned up. Agent marked as failed."
                        )
                    )
                sys.exit(1)

    projection_result = session_service.project_talk_attach(
        TalkAttachRequest(instance_id=instance_id)
    )

    projection = None
    mutable_state = state
    presentation = None
    socket_path_str: str | None = None

    if isinstance(projection_result, Success):
        projection = projection_result.unwrap()
        mutable_state = projection.mutable_state
        presentation = projection.presentation
        socket_path_str = projection.session_state.transport.socket_path

    if mutable_state.state == AgentState.awaiting_approval and not json_output:
        reason = latest_approval_reason(instance_id, deps=deps)
        prompt_reason = reason if reason else "no reason provided"
        console.print(
            STYLE_WARNING.format(
                f"Agent requests approval: {prompt_reason}. /approve or /reject?"
            )
        )

    if presentation is None:
        category = categorize_agent_state(mutable_state.state)
        if category == "terminal":
            state_message = f"Agent is in terminal state: {mutable_state.state.value}"
            if json_output:
                console.file.write(
                    json.dumps(
                        {
                            "error": {
                                "code": "AGENT_TERMINAL",
                                "numeric_code": 313,
                                "message": state_message,
                                "details": {
                                    "agent_id": instance_id,
                                    "state": mutable_state.state.value,
                                },
                            }
                        }
                    )
                    + "\n"
                )
            else:
                console.print(STYLE_ERROR.format(f"Error: {state_message}"))
            sys.exit(1)

        if category == "non_interactive":
            state_message = (
                f"Agent not interactive (state: {mutable_state.state.value})"
            )
            if json_output:
                console.file.write(
                    json.dumps(
                        {
                            "error": {
                                "code": "AGENT_NOT_INTERACTIVE",
                                "numeric_code": 314,
                                "message": state_message,
                                "details": {
                                    "agent_id": instance_id,
                                    "state": mutable_state.state.value,
                                },
                            }
                        }
                    )
                    + "\n"
                )
            else:
                console.print(STYLE_ERROR.format(f"Error: {state_message}"))
            sys.exit(1)

    if presentation is not None and presentation.kind == "terminal_state":
        terminal_state = presentation.terminal_state or mutable_state.state
        state_message = f"Agent is in terminal state: {terminal_state.value}"
        if json_output:
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "AGENT_TERMINAL",
                            "numeric_code": 313,
                            "message": state_message,
                            "details": {
                                "agent_id": instance_id,
                                "state": terminal_state.value,
                            },
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print(STYLE_ERROR.format(f"Error: {state_message}"))
        sys.exit(1)

    if presentation is not None and presentation.kind == "non_interactive_state":
        state_message = f"Agent not interactive (state: {mutable_state.state.value})"
        if json_output:
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "AGENT_NOT_INTERACTIVE",
                            "numeric_code": 314,
                            "message": state_message,
                            "details": {
                                "agent_id": instance_id,
                                "state": mutable_state.state.value,
                            },
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print(STYLE_ERROR.format(f"Error: {state_message}"))
        sys.exit(1)

    if presentation is not None and presentation.kind == "transport_failure":
        failure = presentation.transport_failure
        if failure is not None and failure.code == "socket_not_found":
            if json_output:
                console.file.write(
                    json.dumps(
                        {
                            "error": {
                                "code": "MCP_SOCKET_NOT_FOUND",
                                "numeric_code": 321,
                                "message": failure.message,
                                "details": {
                                    "agent_id": instance_id,
                                    "socket_path": socket_path_str,
                                },
                            }
                        }
                    )
                    + "\n"
                )
            else:
                console.print(
                    STYLE_ERROR.format(
                        f"Error: Agent MCP socket not found at {socket_path_str}"
                    )
                )
                console.print(
                    STYLE_DIM.format(
                        f"Is the agent running? Check with 'anima status {instance_id[:8]}'"
                    )
                )
            sys.exit(1)

        if json_output:
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "MCP_CONNECTION_FAILED",
                            "numeric_code": 320,
                            "message": failure.message
                            if failure is not None
                            else "Agent transport is disconnected",
                            "details": {
                                "agent_id": instance_id,
                                "socket_path": socket_path_str,
                            },
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print(STYLE_ERROR.format("Error: Agent transport is disconnected"))
        sys.exit(1)

    if socket_path_str is None:
        state_dir = str(deps.get_state_dir())
        socket_path_str = build_socket_path(state_dir=state_dir, agent_id=instance_id)

    if presentation is None and not Path(socket_path_str).exists():
        if json_output:
            console.file.write(
                json.dumps(
                    {
                        "error": {
                            "code": "MCP_SOCKET_NOT_FOUND",
                            "numeric_code": 321,
                            "message": f"Agent MCP socket not found at {socket_path_str}",
                            "details": {
                                "agent_id": instance_id,
                                "socket_path": socket_path_str,
                            },
                        }
                    }
                )
                + "\n"
            )
        else:
            console.print(
                STYLE_ERROR.format(
                    f"Error: Agent MCP socket not found at {socket_path_str}"
                )
            )
            console.print(
                STYLE_DIM.format(
                    f"Is the agent running? Check with 'anima status {instance_id[:8]}'"
                )
            )
        sys.exit(1)

    socket_path = Path(socket_path_str)

    # 5. Start interactive session
    if json_output:
        # Emit session_start event on attach (JSON mode)
        console.file.write(
            json.dumps(
                {
                    "event": "session_start",
                    "agent_id": instance_id,
                    "model": state.model,
                    "persona_name": persona_name,
                    "current_turn": mutable_state.current_turn,
                    "max_turns": mutable_state.max_turns,
                }
            )
            + "\n"
        )
    else:
        console.print(STYLE_SUCCESS.format(f"Attaching to agent {instance_id[:8]}..."))
        # format_session_header returns plain text; apply styling here
        console.print(
            f"[dim cyan]{
                format_session_header(
                    model=state.model,
                    persona_name=persona_name,
                    current_turn=mutable_state.current_turn,
                    max_turns=mutable_state.max_turns,
                )
            }[/dim cyan]"
        )

    # 5.5. Replay history from log.jsonl (before live prompt)
    # Spec source: vectl step talk-history-replay
    replay_talk_history(
        agent_id=instance_id,
        json_output=json_output,
        console=console,
        deps=deps,
        persona_name=persona_name,
    )

    if not json_output:
        console.print(
            STYLE_DIM.format(
                "Type your message and press Enter. "
                "/help for commands, /stop to cancel current turn, /quit to exit. "
                "Ctrl-C to detach."
            )
        )

    # Run the async interactive loop
    # Spec source: vectl step talk-ux-fixes.talk-ctrl-c-clean
    # Regression-structure reference kept for production-coupled source tests.
    # Wrap asyncio.run() in try/except KeyboardInterrupt because
    # asyncio.run() catches KeyboardInterrupt before the inner except block
    # in run_talk_session(), then re-raises to Click which shows "Aborted!".
    # The inner handler already printed the detach message, so we suppress here.
    try:
        asyncio.run(
            run_talk_session_with_deps(
                socket_path=socket_path,
                agent_id=instance_id,
                json_output=json_output,
                console=console,
                deps=deps,
                persona_name=persona_name,
            )
        )
    except KeyboardInterrupt:
        # KeyboardInterrupt already handled in run_talk_session()
        # (detach message printed there). Suppress re-raise to Click
        # to avoid showing "Aborted!" message.
        return
