"""Spawn a single agent in a new terminal pane.

Provides the core ``spawn_agent()`` function used by both
the ``synapse spawn`` CLI command and the ``POST /spawn`` API endpoint.
"""

from __future__ import annotations

import contextlib
import os
import shlex
import subprocess
from dataclasses import dataclass

from synapse.port_manager import PortManager, is_port_available
from synapse.registry import AgentRegistry
from synapse.server import load_profile
from synapse.terminal_jump import (
    _get_tmux_spawn_panes,
    create_panes,
    detect_terminal_app,
)


def _get_tmux_pane_ids() -> set[str]:
    """Return the set of current tmux pane IDs."""
    try:
        result = subprocess.run(
            ["tmux", "list-panes", "-F", "#{pane_id}"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0 and result.stdout.strip():
            return set(result.stdout.strip().splitlines())
    except Exception:
        pass
    return set()


def _get_new_tmux_pane_id(before: set[str]) -> str | None:
    """Return the pane ID that appeared after a spawn, or None."""
    after = _get_tmux_pane_ids()
    new_panes = after - before
    return new_panes.pop() if new_panes else None


def _set_tmux_spawn_panes(value: str) -> None:
    """Write SYNAPSE_SPAWN_PANES to the tmux session environment."""
    with contextlib.suppress(Exception):
        subprocess.run(
            ["tmux", "set-environment", "SYNAPSE_SPAWN_PANES", value],
            capture_output=True,
            timeout=5,
        )


@dataclass
class SpawnResult:
    """Result of a spawn_agent() call."""

    agent_id: str
    port: int
    terminal_used: str
    status: str  # "submitted" | "failed"
    worktree_path: str | None = None
    worktree_branch: str | None = None


def spawn_agent(
    profile: str,
    port: int | None = None,
    name: str | None = None,
    role: str | None = None,
    skill_set: str | None = None,
    terminal: str | None = None,
    tool_args: list[str] | None = None,
    worktree: str | bool | None = None,
    fallback_tool_args: list[str] | None = None,
) -> SpawnResult:
    """Spawn a single agent in a new terminal pane.

    Args:
        profile: Agent profile name (claude, gemini, codex, etc.).
        port: Explicit port number. Auto-assigned if None.
        name: Custom agent name.
        role: Agent role description.
        skill_set: Skill set to activate.
        terminal: Terminal app to use. Auto-detected if None.
        tool_args: Extra arguments passed through to the underlying CLI tool
            (e.g., ``["--dangerously-skip-permissions"]``).
        worktree: If True, create a worktree with auto-generated name.
            If a string, use it as the worktree name. If None/False, no worktree.
        fallback_tool_args: If not None, produces a shell-level fallback
            command when the primary command fails (e.g., resume session
            not found).

    Returns:
        SpawnResult with agent_id, port, terminal_used, status.

    Raises:
        FileNotFoundError: If profile is invalid.
        RuntimeError: If port unavailable, ports exhausted, or no terminal.
    """
    # 1. Validate profile
    load_profile(profile)

    # 2. Resolve port
    if port is not None:
        if not is_port_available(port):
            raise RuntimeError(f"Port {port} is already in use")
    else:
        registry = AgentRegistry()
        pm = PortManager(registry)
        port = pm.get_available_port(profile)
        if port is None:
            raise RuntimeError(pm.format_exhaustion_error(profile))

    # 3. Detect terminal
    terminal_used = terminal or detect_terminal_app()
    if terminal_used is None:
        raise RuntimeError(
            "No supported terminal detected. "
            "Supported: tmux, iTerm2, Terminal.app, Ghostty, zellij"
        )

    # 4. Create worktree if requested
    worktree_info = None
    cwd = os.getcwd()
    extra_env: dict[str, str] | None = None

    if worktree:
        from synapse.worktree import create_worktree

        wt_name = worktree if isinstance(worktree, str) else None
        worktree_info = create_worktree(name=wt_name)
        cwd = str(worktree_info.path)
        extra_env = {
            "SYNAPSE_WORKTREE_PATH": str(worktree_info.path),
            "SYNAPSE_WORKTREE_BRANCH": worktree_info.branch,
            "SYNAPSE_WORKTREE_BASE_BRANCH": worktree_info.base_branch,
        }

    # 5. Build agent spec: profile:name:role:skill_set:port
    agent_spec = ":".join(
        [
            profile,
            name or "",
            role or "",
            skill_set or "",
            str(port),
        ]
    )

    # 6. Create pane commands via existing infrastructure
    commands = create_panes(
        [agent_spec],
        layout="auto",
        terminal_app=terminal_used,
        all_new=True,
        tool_args=tool_args,
        cwd=cwd,
        extra_env=extra_env,
        fallback_tool_args=fallback_tool_args,
    )

    if not commands:
        raise RuntimeError(
            f"No commands generated by create_panes for terminal '{terminal_used}'"
        )

    # 7. Execute commands and track spawn zone panes (tmux only)
    panes_before: set[str] = set()
    if terminal_used == "tmux":
        panes_before = _get_tmux_pane_ids()

    try:
        for cmd in commands:
            subprocess.run(shlex.split(cmd), check=True)
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"Failed to spawn agent: {e}") from e

    # 8. Track new pane in SYNAPSE_SPAWN_PANES for spawn zone tiling
    #    Uses tmux session environment so value persists across CLI calls.
    if terminal_used == "tmux" and panes_before:
        new_pane = _get_new_tmux_pane_id(panes_before)
        if new_pane:
            existing = _get_tmux_spawn_panes()
            if existing:
                _set_tmux_spawn_panes(f"{existing},{new_pane}")
            else:
                _set_tmux_spawn_panes(new_pane)

    agent_id = f"synapse-{profile}-{port}"
    wt_path = str(worktree_info.path) if worktree_info else None
    wt_branch = worktree_info.branch if worktree_info else None

    return SpawnResult(
        agent_id=agent_id,
        port=port,
        terminal_used=terminal_used,
        status="submitted",
        worktree_path=wt_path,
        worktree_branch=wt_branch,
    )


def wait_for_agent(
    agent_id: str,
    timeout: float = 30.0,
    poll_interval: float = 0.5,
) -> dict | None:
    """Wait for a spawned agent to register and become alive.

    Uses fixed-interval polling against the file-based registry.
    Fixed interval is preferred over exponential backoff here because
    registry reads are cheap (single file read) and agent boot times
    are unpredictable — we want consistent responsiveness.

    Args:
        agent_id: The agent ID to wait for (e.g. "synapse-claude-8100").
        timeout: Maximum seconds to wait (default 30s).
        poll_interval: Seconds between polls (default 0.5s).

    Returns:
        Agent info dict if found and alive, or None if timed out.
    """
    import time

    from synapse.port_manager import is_process_alive

    registry = AgentRegistry()
    deadline = time.monotonic() + timeout

    while time.monotonic() < deadline:
        agents = registry.list_agents()
        if agent_id in agents:
            info = agents[agent_id]
            pid = info.get("pid")
            if pid and is_process_alive(pid):
                return info
        time.sleep(poll_interval)

    return None
