"""Process-signal utilities for killing agent trees.

Extracted from state.py to separate process-signal concerns from state
management. Provides cascade kill: terminates all agents in a subtree
(children before parent) and sends OS signals.

Key functions:
- kill_agent_tree: public entry point used by kill_cmd.py and mcp_serve.py
- _terminate_pid_with_fallback: SIGTERM → SIGKILL escalation
- _collect_kill_postorder: depth-first postorder traversal of agent children
- _is_pid_alive: probe process liveness via kill(pid, 0)
"""

from __future__ import annotations

import errno
import os
import signal
import time

from returns.result import Failure, Result, Success

from anima.core.lifecycle import LifecycleEvent, TERMINAL_STATES
from anima.core.schemas import AgentNotFoundError, AgentStateData, AnimaError, InternalError


def _is_pid_alive(pid: int) -> Result[bool, InternalError]:
    """Check whether a process exists without sending a signal."""
    try:
        os.kill(pid, 0)
        return Success(True)
    except ProcessLookupError:
        return Success(False)
    except OSError as e:
        if e.errno == errno.EPERM:
            return Success(True)
        return Failure(InternalError(f"Failed to probe pid {pid}: {e}"))


# @shell_complexity: Process lifecycle management - SIGTERM→SIGKILL escalation requires signal-specific error handling
def _terminate_pid_with_fallback(
    pid: int,
    grace_seconds: float,
) -> Result[None, InternalError]:
    """Send SIGTERM and escalate to SIGKILL after grace window."""
    try:
        os.kill(pid, signal.SIGTERM)
    except ProcessLookupError:
        return Success(None)
    except OSError as e:
        return Failure(InternalError(f"Failed to send SIGTERM to pid {pid}: {e}"))

    if grace_seconds > 0:
        time.sleep(grace_seconds)

    alive_result = _is_pid_alive(pid)
    if isinstance(alive_result, Failure):
        return alive_result

    if not alive_result.unwrap():
        return Success(None)

    try:
        os.kill(pid, signal.SIGKILL)
    except ProcessLookupError:
        return Success(None)
    except OSError as e:
        return Failure(InternalError(f"Failed to send SIGKILL to pid {pid}: {e}"))

    return Success(None)


# @shell_complexity: Graph traversal with cycle detection requires early termination branches
def _collect_kill_postorder(
    instance_id: str,
    visiting: set[str],
    visited: set[str],
) -> Result[list[AgentStateData], AnimaError]:
    """Collect subtree nodes in depth-first postorder (children before parent)."""
    from anima.shell.state import read_state

    if instance_id in visited:
        return Success([])

    if instance_id in visiting:
        return Failure(
            InternalError(
                f"Cycle detected in agent children graph at {instance_id}",
            )
        )

    read_result = read_state(instance_id)
    if isinstance(read_result, Failure):
        # Skip missing agents (state never written or externally deleted)
        # Only propagate I/O errors, not "not found"
        if isinstance(read_result.failure(), AgentNotFoundError):
            visited.add(instance_id)
            return Success([])
        return read_result

    state = read_result.unwrap()
    visiting.add(instance_id)
    ordered: list[AgentStateData] = []

    for child_id in state.children:
        child_result = _collect_kill_postorder(child_id, visiting, visited)
        if isinstance(child_result, Failure):
            return child_result
        ordered.extend(child_result.unwrap())

    visiting.remove(instance_id)
    visited.add(instance_id)
    ordered.append(state)
    return Success(ordered)


# @shell_complexity: Cascade kill orchestration with state transitions requires sequential validation with early exits
def kill_agent_tree(
    instance_id: str,
    error: str,
    grace_seconds: float = 1.0,
) -> Result[list[AgentStateData], AnimaError]:
    """Kill an agent and all descendants depth-first.

    Spec source: vectl step
    ``spawn-correctness.fix-kill-propagate-to-child-agents-cascade-kill``.
    """
    from anima.shell.state import transition_state

    order_result = _collect_kill_postorder(instance_id, set(), set())
    if isinstance(order_result, Failure):
        return order_result

    killed_states: list[AgentStateData] = []

    for state in order_result.unwrap():
        if state.state in TERMINAL_STATES:
            continue

        transition_result = transition_state(
            state.instance_id, LifecycleEvent.kill, error
        )
        if isinstance(transition_result, Failure):
            return transition_result

        killed_state = transition_result.unwrap()
        if killed_state.pid is not None:
            signal_result = _terminate_pid_with_fallback(
                killed_state.pid,
                grace_seconds=grace_seconds,
            )
            if isinstance(signal_result, Failure):
                return signal_result

        killed_states.append(killed_state)

    return Success(killed_states)
