"""Daemon and monitoring commands: monitor/top, serve.

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

# @invar:allow function_size: CLI command implementations with daemon management
# @invar:allow shell_result: monitor and serve commands report daemon failures via exit status

from __future__ import annotations

from contextlib import contextmanager
import os
import signal
import sys
import termios
import threading
import tty
from typing import Iterator

from deal import post, pre
from rich.console import Console
from rich.live import Live
from rich.table import Table

from anima.core.schemas import AgentState, AgentStateData
from anima.shell.cli_commands._shared import shortest_unique_agent_id_prefixes
from anima.shell.state import list_agents


STATE_COLORS: dict[str, str] = {
    AgentState.running.value: "green",
    AgentState.completed.value: "blue",
    AgentState.failed.value: "red",
    AgentState.killed.value: "yellow",
    AgentState.pending.value: "dim",
    AgentState.suspended.value: "cyan",
    AgentState.awaiting_approval.value: "magenta",
}
"""State-to-color mapping used by monitor table.

>>> STATE_COLORS["running"]
'green'
"""


# @invar:allow shell_result: Step testability.extract-monitor-pure-logic requires counting helper in shell module
# @shell_orchestration: Pure helper retained in shell module to keep monitor_impl-local behavior unchanged
@pre(lambda agents: isinstance(agents, list))
@post(lambda result: all(v >= 0 for v in result.values()))
def count_agent_states(agents: list[AgentStateData]) -> dict[str, int]:
    """Count agents by state value.

    >>> count_agent_states([])
    {}
    >>> agents = [
    ...     AgentStateData(
    ...         agent_id="ag_1",
    ...         instance_id="inst_1",
    ...         persona_name="p1",
    ...         trace_id="tr_1",
    ...         state=AgentState.running,
    ...     ),
    ...     AgentStateData(
    ...         agent_id="ag_2",
    ...         instance_id="inst_2",
    ...         persona_name="p2",
    ...         trace_id="tr_2",
    ...         state=AgentState.pending,
    ...     ),
    ...     AgentStateData(
    ...         agent_id="ag_3",
    ...         instance_id="inst_3",
    ...         persona_name="p3",
    ...         trace_id="tr_3",
    ...         state=AgentState.running,
    ...     ),
    ... ]
    >>> count_agent_states(agents) == {"running": 2, "pending": 1}
    True
    """
    counts: dict[str, int] = {}
    for agent in agents:
        state_name = agent.state.value
        counts[state_name] = counts.get(state_name, 0) + 1
    return counts


@pre(
    lambda agent, state_colors, display_agent_id=None: (
        isinstance(agent, AgentStateData) and isinstance(state_colors, dict)
    )
)
@post(lambda result: len(result) > 0)
# @invar:allow shell_result: Pure helper for table row formatting (no I/O, returns list)
# @invar:allow shell_complexity: Agent row formatting has 5 branches for optional fields and state coloring
# @shell_orchestration: Spec-source testability.extract-monitor-pure-logic; display helpers kept in shell for CLI test compatibility
# @shell_complexity: Row formatter preserves explicit branches for optional persona, timestamp, pid, and token fields
def format_agent_row(
    agent: AgentStateData,
    state_colors: dict[str, str],
    *,
    display_agent_id: str | None = None,
) -> list[str]:
    """Format a table row for one agent.

    >>> row = format_agent_row(
    ...     AgentStateData(
    ...         instance_id="inst_12345678",
    ...         persona_name="demo",
    ...         trace_id="tr_1",
    ...         state=AgentState.running,
    ...     ),
    ...     {"running": "green"},
    ... )
    >>> row[:3]
    ['inst_12345678', '[green]running[/green]', 'demo']
    """
    agent_id_short = (
        display_agent_id if display_agent_id is not None else agent.agent_id
    )
    state_color = state_colors.get(agent.state.value, "white")
    state_str = f"[{state_color}]{agent.state.value}[/{state_color}]"
    model = agent.persona_name[:12] if agent.persona_name else "N/A"
    created = agent.created_at[:19] if agent.created_at else "N/A"
    pid = str(agent.pid) if agent.pid else "N/A"
    token_str = (
        str(agent.token_usage.total_tokens)
        if agent.token_usage and agent.token_usage.total_tokens > 0
        else "N/A"
    )
    return [agent_id_short, state_str, model, created, pid, token_str]


# @invar:allow shell_result: Step testability.extract-monitor-pure-logic requires summary formatter in shell module
# @shell_orchestration: Pure helper retained in shell module to keep monitor_impl-local behavior unchanged
@pre(lambda counts, total: total >= 0)
@post(lambda result: len(result) > 0)
def format_summary_row(counts: dict[str, int], total: int) -> str:
    """Format summary state counts row text.

    >>> format_summary_row({"running": 2, "pending": 1}, total=3)
    '[green]running:2[/green] | [dim]pending:1[/dim]'
    """
    summary_parts: list[str] = []
    for state_name in ["running", "pending", "completed", "failed", "killed"]:
        count = counts.get(state_name, 0)
        if count > 0:
            color = STATE_COLORS.get(state_name, "white")
            summary_parts.append(f"[{color}]{state_name}:{count}[/{color}]")

    summary = " | ".join(summary_parts)
    if summary:
        return summary
    return "[white][/white]"


# @shell_complexity: Live dashboard with threading, signal handling, and Rich Live updates
# @invar:allow function_size: CLI daemon with table output
def monitor_impl(refresh: float, once: bool, console: Console) -> None:
    """Shared implementation for monitor/top command names.

    TUI dashboard showing all agents with live updates.

    Displays a Rich Live dashboard with:
    - Auto-refreshing agent table (ID, State, Model, Created, PID, Tokens)
    - Color-coded states
    - Summary line with total counts
    - Press 'q' to quit, or Ctrl-C for graceful exit

    Exit codes: 0 normal exit, 1 internal error.
    """
    from returns.result import Failure

    # State color mapping per docs/INTERFACES.md
    state_colors = STATE_COLORS

    stop_event = threading.Event()
    resize_event = threading.Event()

    def handle_signal(signum: int, frame: object) -> None:
        """Signal handler for graceful shutdown."""
        stop_event.set()

    def handle_resize(signum: int, frame: object) -> None:
        """Signal handler for terminal resize refresh."""
        # Spec source: user step ``fix-monitor-resize`` requires forcing Rich
        # Console terminal size re-detection when SIGWINCH arrives.
        if hasattr(console, "_size"):
            setattr(console, "_size", None)
        resize_event.set()

    @contextmanager
    def raw_stdin_mode() -> Iterator[None]:
        """Enable raw stdin for single-keystroke quit and always restore mode.

        Spec source: vectl step ``ux-polish.fix-monitor-quit-key`` requires
        immediate quit on ``q`` without Enter and robust terminal restoration.
        """
        try:
            stdin_fd = sys.stdin.fileno()
        except (AttributeError, OSError, ValueError):
            yield
            return

        if not sys.stdin.isatty():
            yield
            return

        original_attrs = termios.tcgetattr(stdin_fd)
        tty.setcbreak(stdin_fd)
        try:
            yield
        finally:
            termios.tcsetattr(stdin_fd, termios.TCSADRAIN, original_attrs)

    def input_thread() -> None:
        """Background thread to read single-key ``q`` for quit."""
        try:
            stdin_fd = sys.stdin.fileno()
            while not stop_event.is_set():
                chunk = os.read(stdin_fd, 1)
                if not chunk:
                    continue
                char = chunk.decode("utf-8", errors="ignore")
                if char and char.lower() == "q":
                    stop_event.set()
                    break
        except (OSError, ValueError):
            return

    # Setup signal handlers
    signal.signal(signal.SIGINT, handle_signal)
    signal.signal(signal.SIGTERM, handle_signal)
    if hasattr(signal, "SIGWINCH"):
        signal.signal(signal.SIGWINCH, handle_resize)

    def build_table() -> Table:
        """Build the agents table."""
        result = list_agents()

        if isinstance(result, Failure):
            # Return error table
            error_table = Table(title="Agents - Error")
            error_table.add_column("Error")
            error_table.add_row(f"[red]Failed to list agents: {result.failure()}[/red]")
            return error_table

        agents = result.unwrap()

        table = Table(title=f"Agents (refresh: {refresh}s | press 'q' to quit)")
        table.add_column("ID", style="cyan")
        table.add_column("State", style="white")
        table.add_column("Model", style="white")
        table.add_column("Created", style="white")
        table.add_column("PID", style="white")
        table.add_column("Tokens", style="white")

        # Summary counts
        state_counts = count_agent_states(agents)
        display_id_map = shortest_unique_agent_id_prefixes([a.agent_id for a in agents])

        for agent in agents:
            table.add_row(
                *format_agent_row(
                    agent,
                    state_colors,
                    display_agent_id=display_id_map.get(agent.agent_id, agent.agent_id),
                )
            )

        # Add summary row if agents exist
        if agents:
            table.add_section()
            table.add_row(
                "Summary",
                format_summary_row(state_counts, len(agents)),
                "",
                "",
                "",
                "",
            )

        return table

    if once:
        console.print(build_table())
        return

    try:
        with raw_stdin_mode():
            # Start input thread for 'q' key
            reader_thread = threading.Thread(target=input_thread, daemon=True)
            reader_thread.start()

            with Live(
                build_table(),
                console=console,
                refresh_per_second=1.0 / refresh,
                screen=False,
            ) as live:
                while not stop_event.is_set():
                    if resize_event.wait(timeout=refresh):
                        resize_event.clear()
                    if not stop_event.is_set():
                        live.update(build_table())

        console.print("[dim]Monitor stopped[/dim]")
        sys.exit(0)

    except KeyboardInterrupt:
        console.print("\n[dim]Monitor stopped[/dim]")
        sys.exit(0)


# @shell_complexity: Async MCP server with signal handlers and graceful shutdown
def serve_impl(port: int, host: str, json_output: bool, console: Console) -> None:
    """Implementation for anima serve command.

    Starts anima as a remote node exposing MCP server.

    Starts ServeMCPServer on the specified host:port, exposing:
    - anima.spawn() - spawn new agents
    - anima.list() - list agents
    - anima.status() - get agent status
    - anima.interact() - send messages to agents
    - anima.collect() - collect results
    - anima.kill() - kill agents

    Use Ctrl-C to stop the server.
    """
    import asyncio

    from returns.result import Failure

    from anima.shell.mcp_serve import ServeMCPServer

    async def run_server() -> None:
        server = ServeMCPServer(host=host, port=port)

        # Start server
        result = await server.start()
        if isinstance(result, Failure):
            if json_output:
                console.print_json(
                    data={
                        "error": {
                            "code": "SERVE_START_FAILED",
                            "numeric_code": 10,  # INTERNAL per contracts/errors.yaml
                            "message": result.failure().message,
                        }
                    }
                )
            else:
                console.print(
                    f"[red]Failed to start server: {result.failure().message}[/red]"
                )
            sys.exit(1)  # Server start failed

        # Print status
        if json_output:
            console.print_json(data={"host": host, "port": port, "status": "listening"})
        else:
            console.print(f"[green]ServeMCPServer listening on {host}:{port}[/green]")

        # Setup signal handler for graceful shutdown
        loop = asyncio.get_event_loop()
        stop_event = asyncio.Event()

        def handle_signal() -> None:
            stop_event.set()

        try:
            loop.add_signal_handler(signal.SIGTERM, handle_signal)
            loop.add_signal_handler(signal.SIGINT, handle_signal)
        except NotImplementedError:
            # Windows doesn't support add_signal_handler
            signal.signal(signal.SIGTERM, lambda s, f: stop_event.set())
            signal.signal(signal.SIGINT, lambda s, f: stop_event.set())

        # Wait for shutdown signal
        await stop_event.wait()

        # Stop server
        if not json_output:
            console.print("\n[yellow]Shutting down...[/yellow]")

        stop_result = await server.stop()
        if isinstance(stop_result, Failure):
            if not json_output:
                console.print(
                    f"[red]Warning: Error during shutdown: {stop_result.failure().message}[/red]"
                )

        if not json_output:
            console.print("[green]Server stopped[/green]")

    try:
        asyncio.run(run_server())
    except KeyboardInterrupt:
        # Already handled by signal handler
        pass
