import argparse
import contextlib
import json
import logging
import os
import re
import shlex
import subprocess
import sys
import uuid
from pathlib import Path

import requests

from synapse.a2a_client import A2AClient
from synapse.history import HistoryManager
from synapse.paths import get_history_db_path
from synapse.registry import (
    AgentRegistry,
    get_valid_uds_path,
    is_port_open,
    is_process_running,
)
from synapse.reply_stack import SenderInfo
from synapse.reply_target import clear_reply_target, load_reply_target
from synapse.settings import get_settings

logger = logging.getLogger(__name__)


def _artifact_display_text(artifact: dict) -> str:
    """Extract display text from an artifact payload."""
    data = artifact.get("data")
    if isinstance(data, dict) and "content" in data:
        return str(data["content"])
    if data is not None:
        return str(data)
    return str(artifact.get("text", ""))


def _get_history_manager() -> HistoryManager:
    """Get history manager instance from environment."""
    db_path = get_history_db_path()
    return HistoryManager.from_env(db_path)


def get_parent_pid(pid: int) -> int:
    """Get parent PID of a process (cross-platform)."""
    try:
        # Try /proc first (Linux)
        with open(f"/proc/{pid}/stat") as f:
            stat = f.read().split()
            return int(stat[3])
    except (FileNotFoundError, PermissionError, IndexError):
        pass

    # Fallback: use ps command (macOS/BSD)
    try:
        result = subprocess.run(
            ["ps", "-o", "ppid=", "-p", str(pid)],
            capture_output=True,
            text=True,
            timeout=2,
        )
        if result.returncode == 0:
            return int(result.stdout.strip())
    except (subprocess.TimeoutExpired, ValueError, FileNotFoundError, PermissionError):
        pass

    return 0


def is_descendant_of(child_pid: int, ancestor_pid: int, max_depth: int = 15) -> bool:
    """Check if child_pid is a descendant of ancestor_pid."""
    current = child_pid
    for _ in range(max_depth):
        if current == ancestor_pid:
            return True
        if current <= 1:
            return False
        parent = get_parent_pid(current)
        if parent == 0 or parent == current:
            return False
        current = parent
    return False


def _extract_sender_info_from_agent(agent_id: str, info: dict) -> dict[str, str]:
    """Extract sender info fields from agent registry entry.

    Args:
        agent_id: The agent's unique identifier
        info: Agent info dict from registry

    Returns:
        Dict with sender_id and optional sender_type, sender_endpoint, sender_uds_path
    """
    sender_info: dict[str, str] = {"sender_id": agent_id}
    field_mapping = {
        "agent_type": "sender_type",
        "endpoint": "sender_endpoint",
        "uds_path": "sender_uds_path",
        "name": "sender_name",
    }
    for registry_key, sender_key in field_mapping.items():
        value = info.get(registry_key)
        if value:
            sender_info[sender_key] = value
    return sender_info


def _validate_explicit_sender(sender: str) -> str | None:
    """Validate explicit sender format and return helpful error if invalid.

    Returns:
        Error message string if invalid, None if valid.
    """
    # Pattern allows hyphens in type segment (e.g., synapse-gpt-4-8120)
    if re.match(r"^synapse-[\w-]+-\d+$", sender):
        return None

    # Check if it's a type or custom name and provide helpful error
    try:
        reg = AgentRegistry()
        agents = reg.list_agents()
        for agent_id, info in agents.items():
            if info.get("name") == sender:
                return (
                    f"Error: --from requires agent ID, not custom name.\n"
                    f"Use: --from {agent_id}"
                )
            if info.get("agent_type", "").lower() == sender.lower():
                return (
                    f"Error: --from requires agent ID, not agent type.\n"
                    f"Use: --from {agent_id}"
                )
    except Exception:
        pass

    return (
        "Error: --from requires agent ID format (synapse-<type>-<port>).\n"
        "Example: --from $SYNAPSE_AGENT_ID (or synapse-claude-8100)"
    )


def _lookup_sender_in_registry(agent_id: str) -> dict[str, str]:
    """Look up sender info from the agent registry.

    Returns a dict with sender_id and optional sender_type, sender_endpoint, etc.
    Falls back to a minimal dict with just sender_id if not found in registry.
    """
    try:
        reg = AgentRegistry()
        agents = reg.list_agents()
        if agent_id in agents:
            return _extract_sender_info_from_agent(agent_id, agents[agent_id])
    except Exception:
        pass
    return {"sender_id": agent_id}


def _get_current_tty() -> str | None:
    """Return the TTY device name for the current process, or None."""
    for fd in (0, 1, 2):
        try:
            tty = os.ttyname(fd)
            if tty:
                return tty
        except (OSError, ValueError):
            continue
    return None


def _find_sender_by_pid() -> dict[str, str]:
    """Find sender info by matching current process ancestry to registered agents.

    Also uses TTY matching as fallback for tmux/spawned agents where process
    ancestry might be broken.
    """
    try:
        reg = AgentRegistry()
        agents = reg.list_agents()
        current_pid = os.getpid()

        # Priority 1: PID Ancestry match
        for agent_id, info in agents.items():
            agent_pid = info.get("pid")
            if agent_pid and is_descendant_of(current_pid, agent_pid):
                return _extract_sender_info_from_agent(agent_id, info)

        # Priority 2: TTY match (fallback for tmux/spawned shells)
        current_tty = _get_current_tty()
        if current_tty:
            for agent_id, info in agents.items():
                if info.get("tty_device") == current_tty:
                    return _extract_sender_info_from_agent(agent_id, info)

    except Exception as e:
        logger.error("Error in _find_sender_by_pid: %s", e, exc_info=True)
    return {}


def build_sender_info(explicit_sender: str | None = None) -> dict | str:
    """
    Build sender info using explicit sender, environment variable, or PID matching.

    Resolution order:
    1. explicit_sender (--from)
    2. SYNAPSE_AGENT_ID environment variable
    3. Registry PID ancestry/TTY matching

    Args:
        explicit_sender: Must be an agent ID (e.g., synapse-claude-8100).
                        TYPE (claude) and custom names are not allowed.

    Returns:
        dict with sender_id, sender_type, sender_endpoint, sender_uds_path.
        Returns error string if explicit_sender is invalid format.
    """
    if not explicit_sender:
        env_sender = os.environ.get("SYNAPSE_AGENT_ID")
        if env_sender:
            # If SYNAPSE_AGENT_ID is set, use it even if not in registry
            # instead of falling back to PID detection which might be wrong
            return _lookup_sender_in_registry(env_sender)
        return _find_sender_by_pid()

    # Validate explicit sender format
    error = _validate_explicit_sender(explicit_sender)
    if error:
        return error

    return _lookup_sender_in_registry(explicit_sender)


def cmd_list(args: argparse.Namespace) -> None:
    """List all available agents."""
    reg = AgentRegistry()
    agents = reg.get_live_agents() if args.live else reg.list_agents()
    print(json.dumps(agents, indent=2))


def cmd_cleanup(args: argparse.Namespace) -> None:
    """Remove stale registry entries for dead agents."""
    reg = AgentRegistry()
    removed = reg.cleanup_stale_entries()
    if removed:
        print(f"Removed {len(removed)} stale registry entries:")
        for agent_id in removed:
            print(f"  - {agent_id}")
    else:
        print("No stale entries found.")


def _format_task_error(task_error: object) -> tuple[str, str]:
    """Extract an error code/message pair from a task error payload."""
    if isinstance(task_error, dict):
        return (
            str(task_error.get("code", "UNKNOWN_ERROR")),
            str(task_error.get("message", "Task failed")),
        )

    return (
        str(getattr(task_error, "code", "UNKNOWN_ERROR")),
        str(getattr(task_error, "message", "Task failed")),
    )


def _record_sent_message(
    task_id: str,
    target_agent: dict[str, object],
    message: str,
    priority: int,
    sender_info: dict[str, str] | None,
) -> None:
    """Record sent message to history database.

    Args:
        task_id: The task ID returned from the target agent
        target_agent: Target agent information dict
        message: The message that was sent
        priority: Priority level (1-5)
        sender_info: Sender information dict (optional)
    """
    try:
        history = _get_history_manager()
        if not history.enabled:
            return

        # Determine sender agent name
        sender_name = "user"
        if sender_info:
            # Prefer explicit sender_type if available
            sender_name = sender_info.get("sender_type") or sender_name
            # Fallback: extract from sender_id (e.g., "synapse-claude-8100" -> "claude")
            if sender_name == "user":
                sender_id = sender_info.get("sender_id", "")
                parts = sender_id.split("-") if sender_id else []
                if len(parts) >= 3 and parts[0] == "synapse":
                    sender_name = parts[1]

        # Build metadata
        metadata = {
            "direction": "sent",
            "target_agent_id": target_agent.get("agent_id"),
            "target_agent_type": target_agent.get("agent_type"),
            "priority": priority,
        }
        if sender_info:
            metadata["sender"] = sender_info

        # Save to history
        # Use task_id as-is; direction is already recorded in metadata
        history.save_observation(
            task_id=task_id,
            agent_name=sender_name,
            session_id="a2a-send",
            input_text=f"@{target_agent.get('agent_type')} {message}",
            output_text=f"Task sent to {target_agent.get('agent_id')}",
            status="sent",
            metadata=metadata,
        )
    except Exception:
        # Non-critical error - log at debug level for troubleshooting
        logger.debug("Failed to record sent message to history", exc_info=True)


def _resolve_target_agent(
    target: str, agents: dict[str, dict]
) -> tuple[dict | None, str | None]:
    """Resolve target agent from name/id.

    Matching priority:
    1. Custom name (exact match, case-sensitive)
    2. Exact match on agent_id (e.g., synapse-claude-8100)
    3. Match on type-port shorthand (e.g., claude-8100)
    4. Match on agent_type if only one exists (e.g., claude)

    Returns:
        Tuple of (agent_info, error_message). If successful, error_message is None.
    """
    # Priority 1: Custom name (exact match, case-sensitive)
    for info in agents.values():
        if info.get("name") == target:
            return info, None

    # Priority 2: Exact match by ID
    if target in agents:
        return agents[target], None

    target_lower = target.lower()

    # Priority 3: Type-port shorthand (e.g., claude-8100, gpt-4-8120)
    type_port_match = re.match(r"^([\w-]+)-(\d+)$", target_lower)
    if type_port_match:
        target_type, port_str = type_port_match.groups()
        target_port = int(port_str)
        for info in agents.values():
            if (
                info.get("agent_type", "").lower() == target_type
                and info.get("port") == target_port
            ):
                return info, None

    # Priority 4: Fuzzy match by agent_type
    matches = [
        a for a in agents.values() if target_lower in a.get("agent_type", "").lower()
    ]

    if len(matches) == 1:
        return matches[0], None

    if len(matches) > 1:
        return None, _format_ambiguous_target_error(target, matches)

    return None, f"No agent found matching '{target}'"


def _format_ambiguous_target_error(target: str, matches: list[dict]) -> str:
    """Format error message for ambiguous target resolution.

    Args:
        target: The original target string.
        matches: List of matching agent info dicts.

    Returns:
        Formatted error message with concrete command examples.
    """
    agent_ids = [m.get("agent_id", "unknown") for m in matches]
    error = f"Ambiguous target '{target}'. Found {len(matches)} agents: {agent_ids}"

    # Build concrete command examples for each match
    lines = ["  Use a specific identifier to target the right agent:"]
    for m in matches:
        agent_id = m.get("agent_id", "unknown")
        name = m.get("name")
        identifier = shlex.quote(name) if name else agent_id
        hint = f"  # {agent_id}" if name else ""
        lines.append(f'    synapse send {identifier} "<message>"{hint}')

    return error + "\n" + "\n".join(lines)


def _get_response_mode(response_mode_arg: str | None) -> str:
    """Resolve response mode from settings and CLI flags.

    Returns "wait", "notify", or "silent".
    """
    if response_mode_arg:
        return response_mode_arg
    settings = get_settings()
    flow = settings.get_a2a_flow()
    flow_defaults = {"oneway": "silent", "roundtrip": "wait"}
    return flow_defaults.get(flow, "notify")


def _normalize_working_dir(path_str: str | None) -> str | None:
    """Normalize a working directory path for stable comparisons."""
    if not path_str:
        return None
    try:
        return str(Path(path_str).expanduser().resolve())
    except OSError:
        return os.path.abspath(os.path.expanduser(path_str))


def _agents_in_current_working_dir(
    agents: dict[str, dict],
    cwd: str,
    exclude_id: str | None = None,
) -> list[dict]:
    """Return agents whose working_dir matches current working directory."""
    normalized_cwd = _normalize_working_dir(cwd)
    if not normalized_cwd:
        return []

    return [
        agent
        for agent in agents.values()
        if agent.get("agent_id") != exclude_id
        and _normalize_working_dir(agent.get("working_dir")) == normalized_cwd
    ]


def _extract_agent_type_from_id(agent_id: str) -> str | None:
    """Extract agent type from an agent ID like 'synapse-claude-8100'."""
    parts = agent_id.split("-")
    if len(parts) >= 2:
        return parts[1]
    return None


def _suggest_spawn_type(target_type: str, sender_type: str | None) -> str:
    """Pick an agent type to suggest for spawning, avoiding the sender's own type."""
    from synapse.port_manager import PORT_RANGES

    if not sender_type or target_type != sender_type:
        return target_type

    for agent_type in PORT_RANGES:
        if agent_type != sender_type and agent_type != "dummy":
            return agent_type

    return target_type


def _warn_working_dir_mismatch(
    target_agent: dict,
    agents: dict[str, dict],
) -> bool:
    """Check and warn if sender CWD differs from target's working_dir.

    Returns True if mismatch detected, False otherwise.
    """
    target_dir = target_agent.get("working_dir")
    if not target_dir:
        return False

    cwd = os.getcwd()
    normalized_cwd = _normalize_working_dir(cwd)
    normalized_target = _normalize_working_dir(target_dir)

    if normalized_cwd == normalized_target:
        return False

    display_name = target_agent.get("name") or target_agent.get("agent_id", "unknown")

    print(
        f'Warning: Target agent "{display_name}" is in a different directory:',
        file=sys.stderr,
    )
    print(f"  Sender:  {cwd}", file=sys.stderr)
    print(f"  Target:  {target_dir}", file=sys.stderr)

    target_id = target_agent.get("agent_id")
    same_dir = _agents_in_current_working_dir(agents, cwd, exclude_id=target_id)

    if same_dir:
        print("Agents in current directory:", file=sys.stderr)
        for a in same_dir:
            name = a.get("name") or a.get("agent_id", "?")
            atype = a.get("agent_type", "?")
            status = a.get("status", "?")
            print(f"  {name} ({atype}) - {status}", file=sys.stderr)
    else:
        sender_id = os.environ.get("SYNAPSE_AGENT_ID", "")
        sender_type = _extract_agent_type_from_id(sender_id) if sender_id else None
        suggest_type = _suggest_spawn_type(
            target_agent.get("agent_type", "claude"), sender_type
        )
        print("No agents in current directory. Spawn one with:", file=sys.stderr)
        print(f"  synapse spawn {suggest_type} --name <name>", file=sys.stderr)

    print("Use --force to send anyway.", file=sys.stderr)
    return True


def _resolve_message(args: argparse.Namespace) -> str:
    """Resolve message content from positional arg, --message-file, or --stdin.

    Exactly one source must be specified. Multiple or zero sources cause exit.
    """
    sources: list[str] = []
    if getattr(args, "message", None):
        sources.append("positional")
    if getattr(args, "message_file", None):
        sources.append("--message-file")
    if getattr(args, "stdin", False):
        sources.append("--stdin")

    if len(sources) > 1:
        print(
            f"Error: Multiple message sources specified: {', '.join(sources)}. "
            "Use exactly one of: positional argument, --message-file, or --stdin.",
            file=sys.stderr,
        )
        sys.exit(1)

    if len(sources) == 0:
        print(
            "Error: No message provided. Use a positional argument, "
            "--message-file PATH, or --stdin.",
            file=sys.stderr,
        )
        sys.exit(1)

    if getattr(args, "stdin", False):
        return sys.stdin.read()

    message_file = getattr(args, "message_file", None)
    if message_file:
        if message_file == "-":
            return sys.stdin.read()
        path = Path(message_file)
        if not path.exists():
            print(f"Error: Message file not found: {message_file}", file=sys.stderr)
            sys.exit(1)
        return path.read_text(encoding="utf-8")

    return str(args.message)


def _process_attachments(file_paths: list[str]) -> list[dict]:
    """Stage attachment files into a temp dir and return FilePart dicts.

    Each file is copied to:
      {tempdir}/synapse-a2a/attachments/{uuid}-{name}

    Returns:
      [{"type": "file", "file": {"name": <filename>, "uri": "file://<staged_path>"}}]
    """
    import shutil
    import tempfile

    if not file_paths:
        return []

    staging_dir = Path(tempfile.gettempdir()) / "synapse-a2a" / "attachments"
    staging_dir.mkdir(parents=True, exist_ok=True)

    parts: list[dict] = []
    for file_path in file_paths:
        src = Path(file_path)
        if not src.exists():
            print(f"Error: Attachment file not found: {file_path}", file=sys.stderr)
            sys.exit(1)
        if not src.is_file():
            print(f"Error: Attachment path is not a file: {file_path}", file=sys.stderr)
            sys.exit(1)

        staged_path = staging_dir / f"{uuid.uuid4()}-{src.name}"
        try:
            shutil.copy2(src, staged_path)
        except OSError as e:
            print(
                f"Error: Failed to stage attachment '{file_path}': {e}",
                file=sys.stderr,
            )
            sys.exit(1)

        parts.append(
            {
                "type": "file",
                "file": {"name": src.name, "uri": f"file://{staged_path}"},
            }
        )

    return parts


def _warn_shell_expansion(message: str) -> None:
    """Warn if message contains shell expansion characters."""
    patterns = [
        (r"`[^`]+`", "backtick command substitution"),
        (r"\$\([^)]+\)", "$() command substitution"),
        (r"\$\{[^}]+\}", "${} variable expansion"),
    ]
    for pattern, desc in patterns:
        if re.search(pattern, message):
            print(
                f"WARNING: Message contains {desc} which may be expanded by the shell.\n"
                "  Consider using --message-file or --stdin to avoid shell expansion:\n"
                '    echo "your message" | synapse send <target> --stdin\n'
                "    synapse send <target> --message-file /path/to/message.txt",
                file=sys.stderr,
            )
            return


def cmd_send(args: argparse.Namespace) -> None:
    """Send a message to a target agent using Google A2A protocol."""
    message = _resolve_message(args)
    _warn_shell_expansion(message)
    file_parts = None
    if getattr(args, "attach", None):
        file_parts = _process_attachments(args.attach)

    reg = AgentRegistry()
    agents = reg.list_agents()

    # Resolve target agent
    target_agent, error = _resolve_target_agent(args.target, agents)
    if error or target_agent is None:
        print(f"Error: {error}", file=sys.stderr)
        sys.exit(1)

    # Validate agent is actually running
    pid = target_agent.get("pid")
    port = target_agent.get("port")
    agent_id = target_agent["agent_id"]
    uds_path = get_valid_uds_path(target_agent.get("uds_path"))

    # Check if process is still alive
    if pid and not is_process_running(pid):
        print(
            f"Error: Agent '{agent_id}' process (PID {pid}) is no longer running.",
            file=sys.stderr,
        )
        print(
            f"  Hint: Remove stale registry with: rm ~/.a2a/registry/{agent_id}.json",
            file=sys.stderr,
        )
        reg.unregister(agent_id)  # Auto-cleanup
        print("  (Registry entry has been automatically removed)", file=sys.stderr)
        sys.exit(1)

    # Check if port is reachable (fast 1-second check)
    if not uds_path and port and not is_port_open("localhost", port, timeout=1.0):
        print(
            f"Error: Agent '{agent_id}' server on port {port} is not responding.",
            file=sys.stderr,
        )
        print(
            "  The process may be running but the A2A server is not started.",
            file=sys.stderr,
        )
        agent_type = target_agent["agent_type"]
        print(
            f"  Hint: Start the server with: synapse start {agent_type} --port {port}",
            file=sys.stderr,
        )
        sys.exit(1)

    # Check working_dir mismatch (skip with --force)
    if not getattr(args, "force", False) and _warn_working_dir_mismatch(
        target_agent, agents
    ):
        sys.exit(1)

    # Build sender metadata
    sender_info = build_sender_info(getattr(args, "sender", None))

    # Check if build_sender_info returned an error
    if isinstance(sender_info, str):
        print(sender_info, file=sys.stderr)
        sys.exit(1)

    # Determine response_mode based on a2a.flow setting and flags
    response_mode = _get_response_mode(getattr(args, "response_mode", None))

    # Add metadata (sender info and response_mode)
    client = A2AClient()
    task = client.send_to_local(
        endpoint=str(target_agent["endpoint"]),
        message=message,
        file_parts=file_parts,
        priority=args.priority,
        wait_for_completion=(response_mode == "wait"),
        timeout=60,
        sender_info=sender_info or None,
        response_mode=response_mode,
        uds_path=uds_path,
        registry=reg,
        sender_agent_id=sender_info.get("sender_id") if sender_info else None,
        target_agent_id=agent_id,
        board_task_id=getattr(args, "board_task_id", None),
    )

    if not task:
        print("Error sending message: local send failed", file=sys.stderr)
        sys.exit(1)

    task_id = task.id or str(uuid.uuid4())
    agent_type = target_agent["agent_type"]
    agent_short = target_agent["agent_id"][:8]

    # Link board task to A2A transport task
    board_task_id = getattr(args, "board_task_id", None)
    if board_task_id:
        try:
            from synapse.task_board import TaskBoard

            board = TaskBoard.from_env()
            board.link_a2a_task(board_task_id, task_id)
        except Exception as e:
            logger.warning("Failed to link board task %s: %s", board_task_id, e)

    print(f"Success: Task created for {agent_type} ({agent_short}...)")
    print(f"  Task ID: {task_id}")
    print(f"  Status: {task.status}")

    task_error = getattr(task, "error", None)
    if task.status == "failed" and task_error:
        code, message = _format_task_error(task_error)
        print(f"  Error: {code} - {message}")
    elif task.artifacts:
        print("  Response:")
        for artifact in task.artifacts:
            artifact_type = artifact.get("type", "unknown")
            content = _artifact_display_text(artifact)
            if content:
                indented = str(content).replace("\n", "\n    ")
                print(f"    [{artifact_type}] {indented}")

    _record_sent_message(
        task_id=task_id,
        target_agent=target_agent,
        message=message,
        priority=args.priority,
        sender_info=sender_info,
    )


def cmd_broadcast(args: argparse.Namespace) -> None:
    """Broadcast a message to all agents in current working directory."""
    message = _resolve_message(args)
    _warn_shell_expansion(message)
    file_parts = None
    if getattr(args, "attach", None):
        file_parts = _process_attachments(args.attach)

    reg = AgentRegistry()
    agents = reg.list_agents()

    sender_info = build_sender_info(getattr(args, "sender", None))
    if isinstance(sender_info, str):
        print(sender_info, file=sys.stderr)
        sys.exit(1)

    sender_id = sender_info.get("sender_id") if sender_info else None
    recipients = _agents_in_current_working_dir(
        agents=agents,
        cwd=str(Path.cwd()),
        exclude_id=sender_id,
    )

    if not recipients:
        print(
            "Error: No agents found in current working directory",
            file=sys.stderr,
        )
        sys.exit(1)

    response_mode = _get_response_mode(getattr(args, "response_mode", None))
    client = A2AClient()
    sent_count = 0
    failed: list[tuple[str, str]] = []

    for target_agent in recipients:
        agent_id = target_agent.get("agent_id", "unknown")
        pid = target_agent.get("pid")
        port = target_agent.get("port")
        endpoint = target_agent.get("endpoint")
        uds_path = get_valid_uds_path(target_agent.get("uds_path"))

        if not endpoint:
            failed.append((agent_id, "missing endpoint"))
            continue

        if pid and not is_process_running(pid):
            reg.unregister(agent_id)
            failed.append((agent_id, f"process {pid} is no longer running"))
            continue

        if not uds_path and port and not is_port_open("localhost", port, timeout=1.0):
            failed.append((agent_id, f"server on port {port} is not responding"))
            continue

        task = client.send_to_local(
            endpoint=str(endpoint),
            message=message,
            file_parts=file_parts,
            priority=args.priority,
            wait_for_completion=(response_mode == "wait"),
            timeout=60,
            sender_info=sender_info or None,
            response_mode=response_mode,
            uds_path=uds_path,
            local_only=False,
            registry=reg,
            sender_agent_id=sender_id,
            target_agent_id=agent_id,
        )

        if not task:
            failed.append((agent_id, "local send failed"))
            continue

        sent_count += 1
        task_id = task.id or str(uuid.uuid4())
        _record_sent_message(
            task_id=task_id,
            target_agent=target_agent,
            message=message,
            priority=args.priority,
            sender_info=sender_info or None,
        )

    print(f"Sent: {sent_count}")
    print(f"Failed: {len(failed)}")
    for agent_id, reason in failed:
        print(f"  - {agent_id}: {reason}")

    if failed:
        sys.exit(1)


def _get_target_display_name(endpoint: str | None, uds_path: str | None) -> str:
    """Get a short display name for the target agent.

    Extracts port from endpoint URL or socket name from UDS path.
    """
    if endpoint and ":" in endpoint:
        return endpoint.rsplit(":", 1)[-1]
    if endpoint:
        return endpoint
    if uds_path:
        return Path(uds_path).name
    return "unknown"


def cmd_reply(args: argparse.Namespace) -> None:
    """Reply to the last message using the reply map.

    This command retrieves the reply target from the local agent's reply map
    and sends the reply message to the original sender.

    Supports --to to reply to a specific sender and --list-targets to show
    all available reply targets.
    """
    # Determine own endpoint from sender info
    explicit_sender = getattr(args, "sender", None)
    sender_info = build_sender_info(explicit_sender)

    # Handle error case from build_sender_info
    if isinstance(sender_info, str):
        print(sender_info, file=sys.stderr)
        sys.exit(1)

    my_endpoint = sender_info.get("sender_endpoint")

    if not my_endpoint:
        print(
            "Error: Cannot determine my endpoint. Are you running in a synapse agent?",
            file=sys.stderr,
        )
        sys.exit(1)

    # Handle --list-targets
    if getattr(args, "list_targets", False):
        try:
            resp = requests.get(f"{my_endpoint}/reply-stack/list", timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                sender_ids = data.get("sender_ids", [])
                targets = data.get("targets", [])
                if sender_ids:
                    print("Available reply targets:")
                    if targets:
                        for target_summary in targets:
                            sender_id = target_summary.get("sender_id", "unknown")
                            task_id = target_summary.get("sender_task_id")
                            preview = target_summary.get("message_preview")
                            received_at = target_summary.get("received_at")
                            parts = [f"  - {sender_id}"]
                            if task_id:
                                parts.append(f"task={task_id}")
                            if received_at:
                                parts.append(f"received_at={received_at}")
                            if preview:
                                parts.append(f"preview={preview}")
                            print(" | ".join(parts))
                    else:
                        for sid in sender_ids:
                            print(f"  - {sid}")
                else:
                    print("No reply targets available.")
            else:
                print(
                    f"Error: Failed to list reply targets: HTTP {resp.status_code}",
                    file=sys.stderr,
                )
                sys.exit(1)
        except requests.RequestException as e:
            print(f"Error: Failed to list reply targets: {e}", file=sys.stderr)
            sys.exit(1)
        return

    if not getattr(args, "message", "").strip():
        print(
            "Error: Reply message is required unless --list-targets is used.",
            file=sys.stderr,
        )
        sys.exit(1)

    def _load_target_summaries() -> list[dict[str, str]]:
        try:
            resp = requests.get(f"{my_endpoint}/reply-stack/list", timeout=5)
        except requests.RequestException:
            return []
        if resp.status_code != 200:
            return []
        data = resp.json()
        targets = data.get("targets", [])
        if isinstance(targets, list) and targets:
            return [target for target in targets if isinstance(target, dict)]
        sender_ids = data.get("sender_ids", [])
        if isinstance(sender_ids, list):
            return [{"sender_id": sid} for sid in sender_ids if isinstance(sid, str)]
        return []

    to_sender = getattr(args, "to", None)
    if not to_sender:
        summaries = _load_target_summaries()
        if len(summaries) > 1:
            print(
                "Error: Multiple reply targets available. Use "
                "'synapse reply --list-targets' or pass --to <sender_id>.",
                file=sys.stderr,
            )
            for summary in summaries:
                sender_id = summary.get("sender_id", "unknown")
                print(f"  - {sender_id}", file=sys.stderr)
            sys.exit(1)
        if len(summaries) == 1:
            to_sender = summaries[0].get("sender_id")

    # Build reply-stack/get URL with optional sender_id
    get_url = f"{my_endpoint}/reply-stack/get"
    if to_sender:
        get_url += f"?sender_id={to_sender}"

    # Get reply target from my agent's reply map (don't pop yet)
    try:
        resp = requests.get(get_url, timeout=5)
    except requests.RequestException as e:
        print(f"Error: Failed to get reply target: {e}", file=sys.stderr)
        sys.exit(1)

    target: dict[str, str | None] | SenderInfo | None = None
    got_target_from_stack = False
    # sender_info is guaranteed to be dict here (str case exits above)
    my_agent_id = sender_info.get("sender_id")

    if resp.status_code == 404:
        if my_agent_id:
            persisted = load_reply_target(my_agent_id)
            if persisted:
                target = persisted
        if not target:
            print(
                "Error: No reply target. No pending messages to reply to.",
                file=sys.stderr,
            )
            sys.exit(1)
    elif resp.status_code != 200:
        print(
            f"Error: Failed to get reply target: HTTP {resp.status_code}",
            file=sys.stderr,
        )
        sys.exit(1)
    else:
        target = resp.json()
        got_target_from_stack = True

    if not target:
        print(
            "Error: No reply target. No pending messages to reply to.", file=sys.stderr
        )
        sys.exit(1)
    target_endpoint = target.get("sender_endpoint")
    target_uds_path = target.get("sender_uds_path")
    task_id = target.get("sender_task_id")  # May be None

    if not target_endpoint and not target_uds_path:
        print("Error: Reply target has no endpoint", file=sys.stderr)
        sys.exit(1)

    # Send reply using A2AClient (prefer UDS if available)
    # sender_info is guaranteed to be dict here (str case exits above)
    client = A2AClient()
    result = client.send_to_local(
        endpoint=target_endpoint or "http://localhost",
        message=args.message,
        priority=3,  # Normal priority for replies
        sender_info=sender_info,
        response_mode="silent",  # Reply doesn't expect a reply back
        in_reply_to=task_id,
        uds_path=target_uds_path or None,
    )

    if not result:
        print("Error: Failed to send reply", file=sys.stderr)
        sys.exit(1)

    # Only pop from stack after successful send
    if got_target_from_stack:
        pop_url = f"{my_endpoint}/reply-stack/pop"
        if to_sender:
            pop_url += f"?sender_id={to_sender}"
        with contextlib.suppress(requests.RequestException):
            requests.get(pop_url, timeout=5)

    # Clear persisted fallback target after successful send
    if my_agent_id:
        clear_reply_target(my_agent_id)

    # Display target info (prefer UDS path if no HTTP endpoint)
    target_short = _get_target_display_name(target_endpoint, target_uds_path)
    print(f"Reply sent to {target_short}")
    if task_id:
        print(f"  In reply to task: {task_id[:8]}...")


def _add_response_mode_flags(parser: argparse.ArgumentParser) -> None:
    """Add --wait / --notify / --silent mutually exclusive response mode flags."""
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "--wait",
        dest="response_mode",
        action="store_const",
        const="wait",
        default=None,
        help="Wait synchronously for response (blocks until done)",
    )
    group.add_argument(
        "--notify",
        dest="response_mode",
        action="store_const",
        const="notify",
        help="Return immediately, receive async notification on completion (default)",
    )
    group.add_argument(
        "--silent",
        dest="response_mode",
        action="store_const",
        const="silent",
        help="Fire and forget — no response or notification",
    )


def _add_message_source_flags(parser: argparse.ArgumentParser) -> None:
    """Add --message-file, --stdin, and --attach flags to a parser."""
    parser.add_argument(
        "--message-file",
        "-F",
        dest="message_file",
        help="Read message from file (use '-' for stdin)",
    )
    parser.add_argument(
        "--stdin",
        action="store_true",
        default=False,
        help="Read message from stdin",
    )
    parser.add_argument(
        "--attach",
        "-a",
        action="append",
        dest="attach",
        help="Attach a file to the message (repeatable)",
    )


def main() -> None:
    """Parse command-line arguments and execute A2A client operations."""
    parser = argparse.ArgumentParser(description="Synapse A2A Client Tool")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # list command
    p_list = subparsers.add_parser("list", help="List active agents")
    p_list.add_argument(
        "--live", action="store_true", help="Only show live agents (auto-cleanup stale)"
    )

    # cleanup command
    subparsers.add_parser("cleanup", help="Remove stale registry entries")

    # send command
    p_send = subparsers.add_parser("send", help="Send message to an agent")
    p_send.add_argument(
        "--target", required=True, help="Target Agent ID or Type (e.g. 'claude')"
    )
    p_send.add_argument(
        "--priority", type=int, default=3, help="Priority (1-5, default: 3)"
    )
    p_send.add_argument(
        "--from",
        dest="sender",
        help="Sender Agent ID (auto-detected from env if not specified)",
    )
    _add_response_mode_flags(p_send)
    p_send.add_argument(
        "message", nargs="?", default=None, help="Content of the message"
    )
    _add_message_source_flags(p_send)
    p_send.add_argument(
        "--force",
        action="store_true",
        default=False,
        help="Send even if target is in a different working directory",
    )
    p_send.add_argument(
        "--board-task-id",
        dest="board_task_id",
        help="Board task ID to link with this message (set by --task flag)",
    )

    # broadcast command
    p_broadcast = subparsers.add_parser(
        "broadcast",
        help="Send message to all agents in current working directory",
    )
    p_broadcast.add_argument(
        "--priority", type=int, default=1, help="Priority (1-5, 5=Interrupt)"
    )
    p_broadcast.add_argument(
        "--from",
        dest="sender",
        help="Sender Agent ID (auto-detected from env if not specified)",
    )
    _add_response_mode_flags(p_broadcast)
    p_broadcast.add_argument(
        "message", nargs="?", default=None, help="Content of the message"
    )
    _add_message_source_flags(p_broadcast)

    # reply command - simplified reply to last message
    p_reply = subparsers.add_parser("reply", help="Reply to the last received message")
    p_reply.add_argument(
        "--from",
        dest="sender",
        help="Your agent ID (required in sandboxed environments like Codex)",
    )
    p_reply.add_argument(
        "--to",
        dest="to",
        help="Reply to a specific sender ID (default: last message)",
    )
    p_reply.add_argument(
        "--list-targets",
        action="store_true",
        default=False,
        help="List available reply targets and exit",
    )
    p_reply.add_argument("message", nargs="?", default="", help="Reply message content")

    args = parser.parse_args()

    commands = {
        "list": cmd_list,
        "cleanup": cmd_cleanup,
        "send": cmd_send,
        "broadcast": cmd_broadcast,
        "reply": cmd_reply,
    }
    commands[args.command](args)


if __name__ == "__main__":
    main()
