"""Delete and prune commands: rm, prune.

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

# @shell_orchestration: CLI commands orchestrate sys.exit for error handling
# @invar:allow shell_result: CLI commands use sys.exit() and tuple returns
# @invar:allow file_size_warning: rm/prune share filtering and reporting helpers that are reused across both command paths

from __future__ import annotations

import signal
import sys
import time
from datetime import datetime

from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from anima.core.lifecycle import LifecycleEvent, killable_states
from anima.core.schemas import AgentState, AgentStateData
from anima.shell.cli_commands._shared import (
    resolve_agent,
    shortest_unique_agent_id_prefixes as _shortest_unique_agent_id_prefixes,
)
from anima.shell.state import delete_agent_state, list_agents, transition_state

EXIT_AGENT_NOT_FOUND = 2


# @invar:allow shell_result: CLI helper uses sys.exit() for error handling
# @shell_complexity: CLI filter parsing with validation
def _parse_prune_filters(states: tuple[str, ...], console: Console) -> list[AgentState]:
    """Parse --state filters for prune command."""
    terminal_states = {AgentState.completed, AgentState.failed, AgentState.killed}
    filter_states: list[AgentState] = []

    if states:
        for state_str in states:
            try:
                filter_states.append(AgentState(state_str))
            except ValueError:
                console.print(f"[red]Error: Invalid state '{state_str}'[/red]")
                sys.exit(1)
    else:
        filter_states = list(terminal_states)

    for fs in filter_states:
        if fs not in terminal_states:
            console.print(
                f"[red]Error: Cannot prune non-terminal state '{fs.value}'[/red]"
            )
            console.print("[dim]Valid states: completed, failed, killed[/dim]")
            sys.exit(1)

    return filter_states


# @invar:allow shell_result: CLI utility returns plain list (no I/O errors possible)
# @invar:allow shell_complexity: Age filtering with 4 branches for parse/error/threshold logic
# @shell_orchestration: Pure filter retained in shell for prune_impl locality; defensive CLI boundary logic
# @shell_complexity: Spec-source help-groups.gate-review; keep explicit parse/error/threshold branches for CLI filtering clarity
def _filter_by_age(
    agents: list[AgentStateData], older_than: int
) -> list[AgentStateData]:
    """Filter agents by age (days)."""
    cutoff_seconds = older_than * 86400
    current_time = time.time()
    filtered: list[AgentStateData] = []

    for agent in agents:
        if not agent.created_at:
            continue
        try:
            created = datetime.fromisoformat(agent.created_at.replace("Z", "+00:00"))
            age_seconds = current_time - created.timestamp()
            if age_seconds >= cutoff_seconds:
                filtered.append(agent)
        except (ValueError, TypeError):
            filtered.append(agent)  # Defensive: include on parse error

    return filtered


def _print_empty_result(
    console: Console,
    json_output: bool,
    filter_states: list[AgentState],
    older_than: int | None,
) -> None:
    """Print empty prune result and exit."""
    if json_output:
        console.print_json(data={"pruned": 0, "agents": []})
    else:
        state_names = ", ".join(s.value for s in filter_states)
        if older_than is not None:
            msg = f"[dim]No agents found in states: {state_names} older than {older_than} day(s)[/dim]"
        else:
            msg = f"[dim]No agents found in states: {state_names}[/dim]"
        console.print(msg)
    sys.exit(0)


def _print_prune_table(console: Console, agents: list[AgentStateData]) -> None:
    """Print dry-run table for prune command."""
    state_colors = {
        AgentState.completed: "blue",
        AgentState.failed: "red",
        AgentState.killed: "yellow",
    }

    table = Table(title=f"Agents to prune ({len(agents)} total)")
    table.add_column("INSTANCE_ID", style="cyan")
    table.add_column("STATE", style="white")
    table.add_column("CREATED", style="white")

    display_id_map = _shortest_unique_agent_id_prefixes(
        [agent.instance_id for agent in agents]
    )

    for agent in agents:
        instance_id_short = display_id_map[agent.instance_id]
        state_color = state_colors.get(agent.state, "white")
        state_str = f"[{state_color}]{agent.state.value}[/{state_color}]"
        created = agent.created_at[:19] if agent.created_at else "N/A"
        table.add_row(instance_id_short, state_str, created)

    console.print(table)
    console.print("[dim]Use --yes to actually delete (dry-run mode is default)[/dim]")


def _print_prune_json_dry_run(console: Console, agents: list[AgentStateData]) -> None:
    """Print JSON dry-run output for prune command."""
    console.print_json(
        data={
            "dry_run": True,
            "would_delete": len(agents),
            "agents": [
                {"instance_id": a.instance_id, "state": a.state.value} for a in agents
            ],
        }
    )


def _print_rm_success(
    console: Console, json_output: bool, instance_id: str, killed_first: bool
) -> None:
    """Print success message for rm command."""
    if json_output:
        output = {"agent_id": instance_id, "deleted": True}
        if killed_first:
            output["killed_first"] = True
        console.print_json(data=output)
    else:
        message = (
            f"[bold]Agent ID:[/] {instance_id}\n[bold]Deleted:[/] [green]yes[/green]"
        )
        if killed_first:
            message += "\n[dim](killed first)[/dim]"
        console.print(Panel(message, title="anima rm", border_style="green"))


def _print_error(
    console: Console,
    json_output: bool,
    code: str,
    num_code: int,
    msg: str,
    details: dict,
) -> None:
    """Print error message and exit."""
    if json_output:
        console.print_json(
            data={
                "error": {
                    "code": code,
                    "numeric_code": num_code,
                    "message": msg,
                    "details": details,
                }
            }
        )
    else:
        console.print(f"[red]Error: {msg}[/red]")
    sys.exit(1)


def _send_sigterm(console: Console, json_output: bool, pid: int) -> None:
    """Send SIGTERM to process, warn on failure."""
    import os

    try:
        os.kill(pid, signal.SIGTERM)
    except ProcessLookupError:
        pass
    except OSError as e:
        if not json_output:
            console.print(
                f"[yellow]Warning: Failed to send SIGTERM to PID {pid}: {e}[/yellow]"
            )


def _print_non_terminal_error(
    console: Console, json_output: bool, state: AgentStateData
) -> None:
    """Print non-terminal agent error and exit."""
    msg = f"Agent is in non-terminal state: {state.state.value}. Use --force to kill first."
    if json_output:
        console.print_json(
            data={
                "error": {
                    "code": "AGENT_NOT_TERMINAL",
                    "numeric_code": 315,
                    "message": msg,
                    "details": {
                        "agent_id": state.instance_id,
                        "state": state.state.value,
                    },
                }
            }
        )
    else:
        console.print(f"[red]Error: {msg}[/red]")
        console.print("[dim]Use --force to kill the agent first, then delete.[/dim]")
    sys.exit(1)


# @shell_complexity: 4 branches for non-terminal agent handling (force flag, killable check, transition result)
# @shell_orchestration: State check for CLI cleanup
# @invar:allow shell_result: Returns bool sentinel so callers can stop flow when sys.exit is intercepted
def _handle_non_terminal_agent(
    console: Console,
    json_output: bool,
    state: AgentStateData,
    force: bool,
    killable: frozenset[AgentState] | None = None,
) -> bool:
    """Handle non-terminal agent deletion attempt.

    Returns:
        ``True`` when caller can continue deletion flow.
        ``False`` when an error path was hit and caller must return.

    Spec source: user step ``post-review-fixes.fix-rm-bulk-exit-behavior-test-order-shared-state-should-fix``
    requires callers to stop flow if ``sys.exit`` is intercepted by tests/runtime.

    Args:
        console: Rich console used for error output.
        json_output: Whether CLI is in JSON mode.
        state: Agent lifecycle state under deletion handling.
        force: Whether caller requested forced deletion.
        killable: Optional killable-state snapshot supplied by caller.
            Spec source: same step requires deterministic behavior under
            order-dependent test environments by reusing one snapshot.
    """
    from returns.result import Failure

    if not force:
        _print_non_terminal_error(console, json_output, state)
        return False

    killable_states_snapshot = killable if killable is not None else killable_states()
    if state.state not in killable_states_snapshot:
        return True  # pending agents don't need kill

    trans_result = transition_state(
        state.instance_id, LifecycleEvent.kill, error="Killed by user (rm --force)"
    )
    if isinstance(trans_result, Failure):
        # Spec source: vectl step ``fix-unsafe-unwrap-after-sys-exit`` requires
        # explicit control-flow termination after _print_error in case sys.exit
        # is intercepted by the runtime/test harness.
        _print_error(
            console,
            json_output,
            "INTERNAL",
            10,
            f"Failed to transition state: {trans_result.failure()}",
            {"agent_id": state.instance_id},
        )
        return False

    updated_state = trans_result.unwrap()
    if updated_state.pid is not None:
        _send_sigterm(console, json_output, updated_state.pid)
    return True


# @shell_orchestration: CLI command with file deletion
def rm_impl(agent_id: str, force: bool, json_output: bool, console: Console) -> None:
    """Implementation for anima rm command.

    By default, refuses to delete non-terminal agents (pending, running,
    suspended, awaiting_approval). Use --force to kill first then delete.

    Exit codes: 0 success, 1 error (not terminal without --force), 2 not found.
    """
    from returns.result import Failure

    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )

    terminal_states = {AgentState.completed, AgentState.failed, AgentState.killed}
    non_terminal_states = {
        AgentState.pending,
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }
    needs_kill = state.state in non_terminal_states

    if state.state not in terminal_states and state.state in non_terminal_states:
        should_continue = _handle_non_terminal_agent(console, json_output, state, force)
        if not should_continue:
            return

    delete_result = delete_agent_state(state.instance_id)
    if isinstance(delete_result, Failure):
        _print_error(
            console,
            json_output,
            "INTERNAL",
            10,
            f"Failed to delete agent state: {delete_result.failure()}",
            {"agent_id": state.instance_id},
        )
        # Spec source: vectl step
        # ``post-review-fixes.retest-gate-review-fixes-retest-shared-helper-location-fix-control-flow-guard``
        # requires explicit post-error control-flow guard so rm cannot fall
        # through to success if sys.exit() is intercepted by tests/runtime.
        return

    _print_rm_success(console, json_output, state.instance_id, needs_kill and force)
    sys.exit(0)


# @invar:allow function_size: Bulk rm includes planning, confirmation, and execution
# @shell_complexity: Bulk rm supports force policy and mixed state actions
def _rm_bulk(force: bool, yes: bool, json_output: bool, console: Console) -> None:
    """Implementation for ``anima rm --all``.

    Spec source: step ``rm-batch.implement-rm-all``.
    """
    from returns.result import Failure

    terminal_states = {AgentState.completed, AgentState.failed, AgentState.killed}
    non_terminal_states = {
        AgentState.pending,
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }
    killable = killable_states()

    all_agents_result = list_agents()
    if isinstance(all_agents_result, Failure):
        # Spec source: vectl step ``fix-unsafe-unwrap-after-sys-exit`` requires
        # explicit control-flow termination after _print_error in case sys.exit
        # is intercepted by the runtime/test harness.
        _print_error(
            console,
            json_output,
            "INTERNAL",
            10,
            f"Failed to list agents: {all_agents_result.failure()}",
            {},
        )
        return

    all_agents = sorted(all_agents_result.unwrap(), key=lambda a: a.instance_id)

    if not all_agents:
        if json_output:
            console.print_json(data={"deleted": 0, "killed_first": 0, "agents": []})
        else:
            console.print("[dim]No agents found.[/dim]")
        sys.exit(0)
        return

    planned_actions: list[tuple[AgentStateData, str, bool]] = []
    for agent in all_agents:
        if agent.state in terminal_states:
            planned_actions.append((agent, "deleted", False))
            continue

        if agent.state in non_terminal_states and force:
            if agent.state in killable and agent.pid is not None:
                planned_actions.append((agent, "killed_then_deleted", True))
            else:
                planned_actions.append((agent, "deleted", False))
            continue

        planned_actions.append((agent, "skip", False))

    actionable = [entry for entry in planned_actions if entry[1] != "skip"]
    if not actionable:
        if json_output:
            console.print_json(data={"deleted": 0, "killed_first": 0, "agents": []})
        else:
            console.print(
                "[dim]No agents eligible for deletion. Use --force for non-terminal agents.[/dim]"
            )
        sys.exit(0)
        return

    if not yes:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "CONFIRMATION_REQUIRED",
                        "numeric_code": 1,
                        "message": "Confirmation required for --all. Use --yes to skip.",
                        "details": {"count": len(actionable)},
                    }
                }
            )
            sys.exit(1)
            return

        table = Table(title=f"Agents for rm --all ({len(planned_actions)} total)")
        table.add_column("INSTANCE_ID", style="cyan")
        table.add_column("STATE", style="white")
        table.add_column("PERSONA", style="white")
        table.add_column("ACTION", style="white")

        display_id_map = _shortest_unique_agent_id_prefixes(
            [agent.instance_id for agent, _, _ in planned_actions]
        )

        for agent, action, _ in planned_actions:
            persona = agent.persona_name[:20] if agent.persona_name else "N/A"
            table.add_row(
                display_id_map[agent.instance_id],
                agent.state.value,
                persona,
                action,
            )

        console.print(table)
        console.print(f"\n[yellow]About to delete {len(actionable)} agent(s).[/yellow]")
        try:
            response = console.input("[bold]Confirm? (y/N):[/] ").strip().lower()
            if response != "y":
                console.print("[dim]Cancelled.[/dim]")
                sys.exit(0)
                return
        except (EOFError, KeyboardInterrupt):
            console.print("\n[dim]Cancelled.[/dim]")
            sys.exit(1)
            return

    deleted_count = 0
    killed_first_count = 0
    deleted_agents: list[dict[str, str | bool]] = []
    errors: list[tuple[str, str]] = []

    for agent, action, requires_kill in planned_actions:
        if action == "skip":
            continue

        if requires_kill:
            should_continue = _handle_non_terminal_agent(
                console=console,
                json_output=json_output,
                state=agent,
                force=True,
                killable=killable,
            )
            if not should_continue:
                return

        delete_result = delete_agent_state(agent.instance_id)
        if isinstance(delete_result, Failure):
            errors.append((agent.instance_id, str(delete_result.failure())))
            continue

        deleted_count += 1
        if requires_kill:
            killed_first_count += 1

        deleted_agents.append(
            {
                "instance_id": agent.instance_id,
                "state": agent.state.value,
                "action": action,
                "killed_first": requires_kill,
            }
        )

    if json_output:
        result: dict[str, object] = {
            "deleted": deleted_count,
            "killed_first": killed_first_count,
            "agents": deleted_agents,
        }
        if errors:
            result["errors"] = [{"agent_id": aid, "error": err} for aid, err in errors]
        console.print_json(data=result)
        sys.exit(0)
        return

    result_table = Table(title=f"Deleted agents ({deleted_count} total)")
    result_table.add_column("INSTANCE_ID", style="cyan")
    result_table.add_column("STATE", style="white")
    result_table.add_column("ACTION", style="white")

    deleted_agent_ids = [str(agent["instance_id"]) for agent in deleted_agents]
    display_id_map = _shortest_unique_agent_id_prefixes(deleted_agent_ids)

    for agent in deleted_agents:
        instance_id = str(agent["instance_id"])
        instance_id_short = display_id_map[instance_id]
        result_table.add_row(
            instance_id_short,
            str(agent["state"]),
            str(agent["action"]),
        )
    console.print(result_table)
    if errors:
        console.print(f"[red]Failed to delete {len(errors)} agent(s).[/red]")
    console.print(
        f"[green]Deleted {deleted_count} agent(s); killed first {killed_first_count}.[/green]"
    )
    sys.exit(0)
    return


# @invar:allow shell_result: CLI deletion coordinator returns tuple for caller to format
# @shell_orchestration: CLI deletion coordinator
def _perform_deletions(
    agents_to_prune: list[AgentStateData],
) -> tuple[int, list[AgentStateData], list[tuple[str, str]]]:
    """Perform deletion of agents. Returns (count, pruned_agents, errors)."""
    from returns.result import Failure
    from anima.shell.state import delete_agent_state

    pruned_count = 0
    pruned_agents: list[AgentStateData] = []
    errors: list[tuple[str, str]] = []

    for agent in agents_to_prune:
        delete_result = delete_agent_state(agent.instance_id)
        if isinstance(delete_result, Failure):
            errors.append((agent.instance_id, str(delete_result.failure())))
        else:
            pruned_count += 1
            pruned_agents.append(agent)

    return pruned_count, pruned_agents, errors


# @shell_complexity: 6 branches for prune output formatting (json_output, agents list, errors list, summary branches)
def _print_prune_results(
    console: Console,
    json_output: bool,
    pruned_count: int,
    pruned_agents: list[AgentStateData],
    errors: list[tuple[str, str]],
) -> None:
    """Print prune command results."""
    if json_output:
        output = {
            "pruned": pruned_count,
            "failed": len(errors),
            "agents": [
                {"instance_id": a.instance_id, "state": a.state.value}
                for a in pruned_agents
            ],
        }
        if errors:
            output["errors"] = [
                {"instance_id": aid, "error": err} for aid, err in errors
            ]
        console.print_json(data=output)
    else:
        if pruned_count > 0:
            console.print(f"[green]Pruned {pruned_count} agent(s)[/green]")
        if errors:
            console.print(f"[red]Failed to prune {len(errors)} agent(s):[/red]")
            display_id_map = _shortest_unique_agent_id_prefixes(
                [instance_id for instance_id, _ in errors]
            )
            for instance_id, err in errors[:5]:
                console.print(f"  [dim]{display_id_map[instance_id]}:[/] {err}")
            if len(errors) > 5:
                console.print(f"  [dim]... and {len(errors) - 5} more[/dim]")


# @invar:allow shell_result: CLI filter helper uses sys.exit() for error handling
# @shell_complexity: CLI agent filtering with state and age predicates
def _get_filtered_agents(
    states: tuple[str, ...], older_than: int | None, console: Console, json_output: bool
) -> list[AgentStateData]:
    """Get agents filtered by state and age. Exits on error."""
    from returns.result import Failure

    filter_states = _parse_prune_filters(states, console)
    all_agents_result = list_agents()

    if isinstance(all_agents_result, Failure):
        msg = f"Failed to list agents: {all_agents_result.failure()}"
        if json_output:
            console.print_json(
                data={"error": {"code": "INTERNAL", "numeric_code": 10, "message": msg}}
            )
        else:
            console.print(f"[red]Error: {msg}[/red]")
        sys.exit(1)

    agents = [a for a in all_agents_result.unwrap() if a.state in filter_states]
    if older_than is not None:
        agents = _filter_by_age(agents, older_than)
    agents.sort(key=lambda a: a.instance_id)
    return agents


# @shell_orchestration: CLI command with age filtering
def prune_impl(
    states: tuple[str, ...],
    older_than: int | None,
    dry_run: bool,
    yes: bool,
    json_output: bool,
    console: Console,
) -> None:
    """Implementation for anima prune command.

    Prune agents in terminal states (completed, failed, killed).

    By default, lists what would be deleted (dry-run mode).
    Use --yes to actually delete.

    Examples:
        anima prune                          # List all terminal agents
        anima prune --state failed           # List only failed agents
        anima prune --state completed --state failed  # List completed and failed
        anima prune --older-than 7           # List agents older than 7 days
        anima prune --yes                    # Delete all terminal agents
        anima prune --state failed --yes      # Delete all failed agents

    Exit codes: 0 success, 1 error, 2 not found.
    """
    agents_to_prune = _get_filtered_agents(states, older_than, console, json_output)
    filter_states = _parse_prune_filters(states, console)  # reuse for display

    if not agents_to_prune:
        _print_empty_result(console, json_output, filter_states, older_than)

    if dry_run or not yes:
        if json_output:
            _print_prune_json_dry_run(console, agents_to_prune)
        else:
            _print_prune_table(console, agents_to_prune)
        sys.exit(0)

    pruned_count, pruned_agents, errors = _perform_deletions(agents_to_prune)
    _print_prune_results(console, json_output, pruned_count, pruned_agents, errors)
    sys.exit(0)
