"""CLI registry orchestration for MCP registration across all supported clients."""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

from .claude_internal import remove_claude_internal_config
from .codex_internal import remove_codex_internal_config
from .gemini_internal import remove_gemini_internal_config
from .openclaw import (
    _is_openclaw_likely_installed,
    get_openclaw_config_path,
    remove_openclaw_config,
    write_openclaw_config,
)

MCP_REGISTER_TIMEOUT_MS = 5000


class RegistrationFailure(RuntimeError):
    """Raised when required MCP registration does not produce a usable client config."""


def _canonical_command_text(command: str, args: list[str]) -> str:
    return " ".join([command, *args]).strip()


def _manual_registration_hint(client: str, command: str, args: list[str]) -> str:
    command_text = _canonical_command_text(command, args)
    hint = f"target_client={client} manual_command='{command_text}'"
    if client == "openclaw":
        hint = f"{hint} config_path={get_openclaw_config_path()}"
    return hint


def _cli_candidates(server_name: str, command: str, args: list[str]) -> list[dict[str, object]]:
    return [
        {
            "client": "claude-internal",
            "label": "Claude Code (internal)",
            "remove": ["claude-internal", "mcp", "remove", "-s", "user", server_name],
            "command": ["claude-internal", "mcp", "add", "-s", "user", server_name, "--", command, *args],
        },
        {
            "client": "claude",
            "label": "Claude Code",
            "remove": ["claude", "mcp", "remove", "-s", "user", server_name],
            "command": ["claude", "mcp", "add", "-s", "user", server_name, "--", command, *args],
        },
        {
            "client": "codex",
            "label": "Codex CLI / Codex App",
            "remove": ["codex", "mcp", "remove", server_name],
            "command": ["codex", "mcp", "add", server_name, "--", command, *args],
        },
        {
            "client": "gemini-internal",
            "label": "Gemini CLI (internal)",
            "remove": ["gemini-internal", "mcp", "remove", "-s", "user", server_name],
            "command": ["gemini-internal", "mcp", "add", "-s", "user", server_name, command, *args],
        },
        {
            "client": "gemini",
            "label": "Gemini CLI",
            "remove": ["gemini", "mcp", "remove", "-s", "user", server_name],
            "command": ["gemini", "mcp", "add", "-s", "user", server_name, command, *args],
        },
        {
            "client": "opencode",
            "label": "OpenCode",
            "remove": ["opencode", "mcp", "remove", server_name],
            "command": ["opencode", "mcp", "add", server_name, "--", command, *args],
        },
    ]


def _detected_clients() -> list[str]:
    detected = [
        str(candidate["client"])
        for candidate in _cli_candidates("reddit-mcp", "reddit-mcp", ["serve"])
        if shutil.which(str(candidate["client"]))
    ]
    if _is_openclaw_likely_installed(get_openclaw_config_path()):
        detected.append("openclaw")
    return detected


def _classify_cli_failure(exc_or_result) -> tuple[str, str]:
    if isinstance(exc_or_result, subprocess.TimeoutExpired):
        output = f"{exc_or_result.stdout or ''}{exc_or_result.stderr or ''}"
        return "timeout", output

    stdout = getattr(exc_or_result, "stdout", "") or ""
    stderr = getattr(exc_or_result, "stderr", "") or ""
    output = f"{stderr}{stdout}"
    lower = output.lower()
    if "already exists" in lower:
        return "already_exists", output
    if "waiting for authentication" in lower:
        return "authentication", output
    return "other", output


def _run_cli_registration(command: list[str], label: str) -> None:
    try:
        result = subprocess.run(
            command,
            capture_output=True,
            text=True,
            timeout=MCP_REGISTER_TIMEOUT_MS,
        )
    except subprocess.TimeoutExpired as exc:
        kind, output = _classify_cli_failure(exc)
        raise RuntimeError(f"{kind}: {output.strip()}") from exc

    if result.returncode == 0:
        print(f"\nRegistering with {label}...")
        print(f"  OK Registered with {label}")
        return

    kind, output = _classify_cli_failure(result)
    if kind == "already_exists":
        print(f"\nRegistering with {label}...")
        print(f"  OK Already registered with {label}")
        return

    trimmed = " ".join(output.split()).strip()
    if kind == "authentication":
        raise RuntimeError(f"authentication: {trimmed}")
    if kind == "timeout":
        raise RuntimeError(f"timeout after {MCP_REGISTER_TIMEOUT_MS}ms: {trimmed}")
    raise RuntimeError(trimmed or f"{label} CLI returned exit code {result.returncode}")


def _run_best_effort_cli_remove(command: list[str]) -> None:
    try:
        result = subprocess.run(
            command,
            capture_output=True,
            text=True,
            timeout=MCP_REGISTER_TIMEOUT_MS,
        )
    except (OSError, subprocess.SubprocessError):
        return

    if result.returncode == 0:
        return

    output = f"{result.stderr or ''}{result.stdout or ''}".lower()
    if any(text in output for text in ("not found", "no server", "unknown", "not registered")):
        return


def register_all(
    *,
    server_name: str,
    command: str,
    args: list[str],
) -> None:
    """Register MCP server with all detected CLIs."""
    detected_clients = _detected_clients()
    command_text = _canonical_command_text(command, args)
    if not detected_clients:
        raise RegistrationFailure(
            "No supported AI client detected for MCP registration. "
            f"Configure a supported client manually with `{command_text}`."
        )

    failures: list[str] = []
    candidate_map = {str(candidate["client"]): candidate for candidate in _cli_candidates(server_name, command, args)}
    for client in detected_clients:
        try:
            if client == "openclaw":
                status = write_openclaw_config(server_name=server_name, command=command, args=args)
                print("\nRegistering with OpenClaw...")
                print(f"  OK Registered with OpenClaw ({status} mcporter.json)")
                continue

            candidate = candidate_map[client]
            # Why: client CLIs own their live config paths; writing guessed internal files drifts from real behavior.
            resolved_binary = shutil.which(client)
            if not resolved_binary:
                raise RuntimeError(f"{client} binary disappeared from PATH during registration")
            resolved_remove = [resolved_binary, *list(candidate["remove"])[1:]]
            resolved_command = [resolved_binary, *list(candidate["command"])[1:]]
            # Why: Claude/Codex CLIs do not overwrite existing same-name MCP entries; remove+add keeps registration canonical.
            _run_best_effort_cli_remove(resolved_remove)
            _run_cli_registration(resolved_command, str(candidate["label"]))
        except Exception as exc:
            failures.append(
                f"{client}: registration failed. {_manual_registration_hint(client, command, args)}. Details: {exc}"
            )

    if failures:
        raise RegistrationFailure("\n".join(failures))


def unregister_all(*, server_name: str) -> None:
    """Remove MCP server registration from all detected CLIs."""
    print("\nUninstalling MCP registrations\n")
    print("Removing MCP client registrations...")

    cli_candidates = [
        ("claude-internal", "Claude Code (internal)", ["claude-internal", "mcp", "remove", "-s", "user", server_name]),
        ("claude", "Claude Code", ["claude", "mcp", "remove", "-s", "user", server_name]),
        ("codex", "Codex CLI / Codex App", ["codex", "mcp", "remove", server_name]),
        ("gemini-internal", "Gemini CLI (internal)", ["gemini-internal", "mcp", "remove", "-s", "user", server_name]),
        ("gemini", "Gemini CLI", ["gemini", "mcp", "remove", "-s", "user", server_name]),
        ("opencode", "OpenCode", ["opencode", "mcp", "remove", server_name]),
    ]

    for binary, label, command in cli_candidates:
        if shutil.which(binary):
            try:
                result = subprocess.run(command, capture_output=True, text=True, timeout=15)
                if result.returncode == 0:
                    print(f"  OK Removed MCP registration from {label}")
                else:
                    stderr = (result.stderr or "").lower()
                    if any(text in stderr for text in ("not found", "no server", "unknown")):
                        print(f"  INFO {label} did not have {server_name} registered")
                    else:
                        print(f"  WARN Failed to remove MCP registration from {label}")
            except (OSError, subprocess.SubprocessError):
                print(f"  WARN Failed to remove MCP registration from {label}")

    # Why: clean up the broken direct-write internal config entries created by older releases.
    status = remove_claude_internal_config(server_name=server_name)
    if status == "removed":
        print("  OK Removed legacy Claude Code (internal) direct config")

    try:
        remove_codex_internal_config(server_name=server_name)
        print("  OK Removed legacy Codex CLI (internal) direct config")
    except Exception:
        pass

    status = remove_gemini_internal_config(server_name=server_name)
    if status == "removed":
        print("  OK Removed legacy Gemini CLI (internal) direct config")

    status = remove_openclaw_config(server_name=server_name)
    if status == "removed":
        print("  OK Removed MCP registration from OpenClaw")
    else:
        print(f"  INFO OpenClaw did not have {server_name} registered")
