"""Gaol MCP Server — expose container management to AI agents.

Run with:  uv run gaol-mcp
"""

from __future__ import annotations

import json
import logging
import subprocess
import sys
from datetime import datetime
from pathlib import Path

from mcp.server.fastmcp import FastMCP

# All logging to stderr (stdout is reserved for JSON-RPC on stdio transport)
logging.basicConfig(
    stream=sys.stderr,
    level=logging.INFO,
    format="%(name)s: %(message)s",
)
logger = logging.getLogger("gaol-mcp")

GAOL_ROOT = Path(__file__).resolve().parents[3]  # src/gaol/mcp -> repo root
FEATURE_REQUESTS_FILE = Path.home() / ".config" / "gaol" / "mcp-feature-requests.jsonl"

mcp = FastMCP(
    "gaol",
    instructions=(
        "Gaol is a cross-platform container and VM management tool supporting "
        "Docker, systemd-nspawn, and libvirt/QEMU. Use these tools to create, "
        "manage, and inspect containers. Use the gaol_guide resource for comprehensive "
        "CLI documentation and workflow patterns. "
        "For nspawn/libvirt operations, sudo is required — the tools handle this automatically."
    ),
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _run_gaol(*args: str, sudo: bool = False, timeout: int = 300) -> tuple[int, str, str]:
    """Run a gaol CLI command and return (exit_code, stdout, stderr)."""
    cmd = ["sudo", "-n", "gaol", *args] if sudo else ["gaol", *args]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.returncode, result.stdout, result.stderr
    except subprocess.TimeoutExpired:
        return 1, "", f"Command timed out after {timeout}s"
    except FileNotFoundError:
        return 1, "", "gaol CLI not found. Install with: uv tool install gaol"


def _needs_sudo(runtime: str) -> bool:
    return runtime in {"nspawn", "libvirt"}


def _get_store():
    """Lazy import to avoid loading heavy deps at MCP startup."""
    from gaol.store import get_container_store

    return get_container_store()


def _get_profile_loader():
    from gaol.profiles.loader import get_profile_loader

    return get_profile_loader()


# ---------------------------------------------------------------------------
# Resources
# ---------------------------------------------------------------------------


@mcp.resource("gaol://docs/guide")
def guide_resource() -> str:
    """Comprehensive gaol agent guide with CLI reference and workflows."""
    agent_md = GAOL_ROOT / "AGENT.md"
    if agent_md.exists():
        return agent_md.read_text()
    return "AGENT.md not found. See gaol --help for CLI reference."


@mcp.resource("gaol://docs/architecture")
def architecture_resource() -> str:
    """Gaol architecture overview, key directories, and conventions."""
    claude_md = GAOL_ROOT / "CLAUDE.md"
    if claude_md.exists():
        return claude_md.read_text()
    return "CLAUDE.md not found."


@mcp.resource("gaol://profiles/builtin")
def builtin_profiles_resource() -> str:
    """List of all built-in profiles with descriptions."""
    from gaol.profiles.loader import BUILTIN_PROFILES

    lines = ["# Built-in Gaol Profiles\n"]
    for name, profile_dict in sorted(BUILTIN_PROFILES.items()):
        desc = profile_dict.get("description", "")
        pkgs = profile_dict.get("packages", [])
        lines.append(f"## {name}")
        if desc:
            lines.append(f"{desc}\n")
        if pkgs:
            lines.append(f"Packages: {', '.join(pkgs)}\n")
    return "\n".join(lines)


@mcp.resource("gaol://specs/examples")
def spec_examples_resource() -> str:
    """Example spec files from the gaol repository."""
    specs_dir = GAOL_ROOT / "specs"
    if not specs_dir.exists():
        return "No specs directory found."

    lines = ["# Example Spec Files\n"]
    for spec_file in sorted(specs_dir.glob("*.yaml")):
        lines.append(f"## {spec_file.name}")
        content = spec_file.read_text()
        # Show first 30 lines as preview
        preview = "\n".join(content.splitlines()[:30])
        lines.append(f"```yaml\n{preview}\n```\n")
    return "\n".join(lines)


@mcp.resource("gaol://feature-requests")
def feature_requests_resource() -> str:
    """Pending feature requests from consuming projects."""
    if not FEATURE_REQUESTS_FILE.exists():
        return "No feature requests yet."
    entries = []
    for line in FEATURE_REQUESTS_FILE.read_text().splitlines():
        if line.strip():
            entries.append(json.loads(line))
    if not entries:
        return "No feature requests yet."
    lines = [f"# Feature Requests ({len(entries)} total)\n"]
    for entry in entries:
        lines.append(f"- [{entry.get('category', '?')}] {entry.get('description', '')}")
        if entry.get("context"):
            lines.append(f"  Context: {entry['context']}")
        lines.append(
            f"  From: {entry.get('requesting_project', '?')} @ {entry.get('timestamp', '?')}"
        )
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Tools — Read operations (direct Python imports)
# ---------------------------------------------------------------------------


@mcp.tool()
def gaol_list_containers(runtime: str | None = None) -> str:
    """List all containers managed by gaol.

    Args:
        runtime: Filter by runtime (docker, nspawn, libvirt). None for all.

    Returns:
        JSON list of containers with name, runtime, distro, profiles, created_at.
    """
    store = _get_store()
    containers = store.list()
    if runtime:
        containers = [c for c in containers if c.runtime == runtime]

    results = []
    for c in containers:
        results.append(
            {
                "name": c.config.name,
                "runtime": c.runtime,
                "distro": c.config.distro,
                "profiles": c.config.profiles,
                "created_at": c.created_at.isoformat() if c.created_at else None,
                "setup_done": c.setup_done,
            }
        )
    return json.dumps(results, indent=2)


@mcp.tool()
def gaol_container_status(name: str) -> str:
    """Get detailed status of a container including stored config and live state.

    Args:
        name: Container name.

    Returns:
        JSON with stored config and live runtime status.
    """
    store = _get_store()
    stored = store.get(name)
    if not stored:
        return json.dumps({"error": f"Container '{name}' not found in store"})

    result: dict = {
        "name": stored.config.name,
        "runtime": stored.runtime,
        "distro": stored.config.distro,
        "profiles": stored.config.profiles,
        "network_mode": stored.config.network.mode if stored.config.network else None,
        "volumes": stored.config.volumes,
        "environment": stored.config.environment,
        "packages": stored.config.packages,
        "setup_done": stored.setup_done,
        "created_at": stored.created_at.isoformat() if stored.created_at else None,
        "last_started_at": stored.last_started_at.isoformat() if stored.last_started_at else None,
    }

    # Try to get live status via CLI
    sudo = _needs_sudo(stored.runtime)
    rc, stdout, stderr = _run_gaol("status", name, sudo=sudo, timeout=10)
    if rc == 0:
        result["live_status"] = stdout.strip()
    else:
        result["live_status"] = f"Could not query: {stderr.strip()}"

    return json.dumps(result, indent=2)


@mcp.tool()
def gaol_container_inspect(name: str) -> str:
    """Show the full stored configuration for a container.

    Args:
        name: Container name.

    Returns:
        Full container config as YAML-like JSON.
    """
    store = _get_store()
    stored = store.get(name)
    if not stored:
        return json.dumps({"error": f"Container '{name}' not found in store"})
    return json.dumps(stored.model_dump(mode="json"), indent=2, default=str)


@mcp.tool()
def gaol_list_profiles() -> str:
    """List all available profiles with descriptions.

    Returns:
        JSON list of profiles with name, description, and package count.
    """
    loader = _get_profile_loader()
    names = loader.list_available()
    results = []
    for name in sorted(names):
        try:
            profile = loader.get(name)
            results.append(
                {
                    "name": profile.name,
                    "description": profile.description,
                    "packages": len(profile.packages),
                    "depends_on": [d.name for d in profile.depends_on]
                    if profile.depends_on
                    else [],
                }
            )
        except Exception as e:
            results.append({"name": name, "error": str(e)})
    return json.dumps(results, indent=2)


@mcp.tool()
def gaol_show_profile(name: str) -> str:
    """Show full details of a profile.

    Args:
        name: Profile name.

    Returns:
        Full profile details as JSON.
    """
    loader = _get_profile_loader()
    try:
        profile = loader.get(name)
        return json.dumps(profile.model_dump(mode="json"), indent=2, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def gaol_list_runtimes() -> str:
    """List available container runtimes and their status.

    Returns:
        JSON list of runtimes with availability status.
    """
    rc, stdout, stderr = _run_gaol("runtimes", timeout=10)
    if rc == 0:
        return stdout.strip()
    return json.dumps({"error": stderr.strip()})


@mcp.tool()
def gaol_validate_spec(spec_path: str) -> str:
    """Validate a container spec file without creating anything.

    Args:
        spec_path: Absolute path to the spec YAML file.

    Returns:
        Validation result with any errors found.
    """
    from gaol.specs.loader import SpecLoader

    loader = SpecLoader()
    try:
        errors = loader.validate(spec_path)
        if errors:
            return json.dumps({"valid": False, "errors": errors})
        return json.dumps({"valid": True, "errors": []})
    except Exception as e:
        return json.dumps({"valid": False, "errors": [str(e)]})


# ---------------------------------------------------------------------------
# Tools — Write operations (shell out to gaol CLI)
# ---------------------------------------------------------------------------


@mcp.tool()
def gaol_create_container(
    name: str,
    runtime: str = "docker",
    distro: str = "trixie",
    profiles: list[str] | None = None,
    ports: list[str] | None = None,
    volumes: list[str] | None = None,
    environment: dict[str, str] | None = None,
    network: str | None = None,
    gpu: bool = False,
    start: bool = False,
) -> str:
    """Create a new container.

    Args:
        name: Container name (alphanumeric, _, ., -).
        runtime: Runtime to use: docker, nspawn, or libvirt.
        distro: Base distro (e.g., trixie, bookworm, noble).
        profiles: List of profiles to apply (e.g., ["base", "dev-python"]).
        ports: Port mappings (e.g., ["8080:80", "3000:3000"]).
        volumes: Volume mounts (e.g., ["~/projects:/workspace"]).
        environment: Environment variables.
        network: Network mode: shared, bridged, host, none.
        gpu: Enable GPU passthrough.
        start: Start the container after creation.

    Returns:
        Result of the create operation.
    """
    args = ["create", name, "--runtime", runtime, "--distro", distro]
    for p in profiles or []:
        args.extend(["--profile", p])
    for port in ports or []:
        args.extend(["--port", port])
    for vol in volumes or []:
        args.extend(["--volume", vol])
    for key, val in (environment or {}).items():
        args.extend(["--env", f"{key}={val}"])
    if network:
        args.extend(["--network", network])
    if gpu:
        args.append("--gpu")
    if start:
        args.append("--start")

    sudo = _needs_sudo(runtime)
    rc, stdout, stderr = _run_gaol(*args, sudo=sudo, timeout=600)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip(), "output": stdout.strip()})


@mcp.tool()
def gaol_apply_spec(spec_path: str, start: bool = False) -> str:
    """Apply a container spec file to create or update a container.

    Args:
        spec_path: Absolute path to the spec YAML/TOML file.
        start: Start the container after applying.

    Returns:
        Result of the apply operation.
    """
    # Peek at the spec to determine runtime for sudo decision
    sudo = False
    try:
        from gaol.specs.loader import load_spec

        spec = load_spec(spec_path)
        sudo = _needs_sudo(spec.runtime)
    except Exception:
        pass

    args = ["apply", spec_path]
    if start:
        args.append("--start")

    rc, stdout, stderr = _run_gaol(*args, sudo=sudo, timeout=600)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip(), "output": stdout.strip()})


@mcp.tool()
def gaol_start_container(name: str) -> str:
    """Start a stopped container.

    Args:
        name: Container name.

    Returns:
        Result of the start operation.
    """
    store = _get_store()
    stored = store.get(name)
    sudo = _needs_sudo(stored.runtime) if stored else False

    rc, stdout, stderr = _run_gaol("start", name, sudo=sudo, timeout=60)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip()})


@mcp.tool()
def gaol_stop_container(name: str, timeout: int = 10) -> str:
    """Stop a running container.

    Args:
        name: Container name.
        timeout: Seconds to wait before force stopping.

    Returns:
        Result of the stop operation.
    """
    store = _get_store()
    stored = store.get(name)
    sudo = _needs_sudo(stored.runtime) if stored else False

    rc, stdout, stderr = _run_gaol("stop", name, "--timeout", str(timeout), sudo=sudo, timeout=30)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip()})


@mcp.tool()
def gaol_destroy_container(name: str, force: bool = False) -> str:
    """Destroy a container permanently. This is irreversible.

    Args:
        name: Container name.
        force: Force destruction even if running.

    Returns:
        Result of the destroy operation.
    """
    store = _get_store()
    stored = store.get(name)
    sudo = _needs_sudo(stored.runtime) if stored else False

    args = ["destroy", name]
    if force:
        args.append("--force")

    rc, stdout, stderr = _run_gaol(*args, sudo=sudo, timeout=60)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip()})


@mcp.tool()
def gaol_exec_command(
    name: str,
    command: list[str],
    user: str | None = None,
    workdir: str | None = None,
) -> str:
    """Execute a command inside a running container.

    Args:
        name: Container name.
        command: Command and arguments to run.
        user: User to run as inside the container.
        workdir: Working directory inside the container.

    Returns:
        JSON with exit_code, stdout, and stderr.
    """
    store = _get_store()
    stored = store.get(name)
    sudo = _needs_sudo(stored.runtime) if stored else False

    args = ["exec", name]
    if user:
        args.extend(["--user", user])
    if workdir:
        args.extend(["--workdir", workdir])
    args.append("--")
    args.extend(command)

    rc, stdout, stderr = _run_gaol(*args, sudo=sudo, timeout=120)
    return json.dumps(
        {
            "exit_code": rc,
            "stdout": stdout,
            "stderr": stderr,
        }
    )


@mcp.tool()
def gaol_container_logs(name: str, tail: int = 100) -> str:
    """Get recent logs from a container.

    Args:
        name: Container name.
        tail: Number of log lines to return.

    Returns:
        Container log output.
    """
    store = _get_store()
    stored = store.get(name)
    sudo = _needs_sudo(stored.runtime) if stored else False

    rc, stdout, stderr = _run_gaol("logs", name, "--tail", str(tail), sudo=sudo, timeout=15)
    if rc == 0:
        return stdout
    return f"Error: {stderr.strip()}"


# ---------------------------------------------------------------------------
# Tools — Remote host operations
# ---------------------------------------------------------------------------


@mcp.tool()
def gaol_remote_list_hosts() -> str:
    """List all registered remote hosts.

    Returns:
        JSON list of remote hosts with connection status.
    """
    rc, stdout, stderr = _run_gaol("remote", "list", timeout=10)
    if rc == 0:
        return stdout.strip() or "No remote hosts registered."
    return f"Error: {stderr.strip()}"


@mcp.tool()
def gaol_remote_exec(
    host: str,
    command: list[str],
    timeout: int = 120,
) -> str:
    """Execute a command on a remote host via SSH.

    The command runs on the remote host directly (not inside a container).
    Use gaol_remote_container_exec to run commands inside remote containers.

    Args:
        host: Registered remote host name.
        command: Command and arguments to run.
        timeout: Command timeout in seconds.

    Returns:
        JSON with exit_code, stdout, stderr.
    """
    args = ["remote", "exec", host, "--timeout", str(timeout), "--"] + command
    rc, stdout, stderr = _run_gaol(*args, timeout=timeout + 10)
    return json.dumps({"exit_code": rc, "stdout": stdout, "stderr": stderr})


@mcp.tool()
def gaol_remote_container_exec(
    host: str,
    container: str,
    command: list[str],
    runtime: str = "nspawn",
    user: str | None = None,
    workdir: str | None = None,
    timeout: int = 120,
) -> str:
    """Execute a command inside a container on a remote host.

    This handles sudo and gaol exec on the remote — no need for manual
    SSH + nsenter. Equivalent to: gaol remote exec <host> -- gaol exec <container> --runtime <rt> -- <cmd>

    Args:
        host: Registered remote host name.
        container: Container name on the remote host.
        command: Command and arguments to run inside the container.
        runtime: Container runtime on the remote (docker, nspawn, libvirt). Default: nspawn.
        user: User to run as inside the container.
        workdir: Working directory inside the container.
        timeout: Command timeout in seconds.

    Returns:
        JSON with exit_code, stdout, stderr.
    """
    # Use gaol remote exec which goes through the remote manager (handles sudo)
    inner_args = ["exec", container, "--runtime", runtime]
    if user:
        inner_args.extend(["--user", user])
    if workdir:
        inner_args.extend(["--workdir", workdir])
    inner_args.append("--")
    inner_args.extend(command)
    args = ["remote", "exec", host, "--timeout", str(timeout), "--", "gaol"] + inner_args
    rc, stdout, stderr = _run_gaol(*args, timeout=timeout + 10)
    return json.dumps({"exit_code": rc, "stdout": stdout, "stderr": stderr})


@mcp.tool()
def gaol_remote_install(
    host: str,
    runtimes: list[str] | None = None,
) -> str:
    """Install gaol and prerequisites on a remote host.

    Bootstraps the remote with uv, gaol, and runtime-specific packages
    (debootstrap, docker, etc). The host must already be registered.

    Args:
        host: Registered remote host name.
        runtimes: Runtimes to set up (default: ["nspawn"]).

    Returns:
        Result of the installation.
    """
    args = ["remote", "install", host]
    for rt in runtimes or ["nspawn"]:
        args.extend(["--runtime", rt])

    rc, stdout, stderr = _run_gaol(*args, timeout=600)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip(), "output": stdout.strip()})


@mcp.tool()
def gaol_remote_sync(
    container: str,
    to_host: str | None = None,
    from_host: str | None = None,
) -> str:
    """Sync a container configuration between local and a remote host.

    Args:
        container: Container name to sync.
        to_host: Sync config TO this remote host.
        from_host: Sync config FROM this remote host.

    Returns:
        Result of the sync operation.
    """
    if not to_host and not from_host:
        return json.dumps({"error": "Specify either to_host or from_host"})

    args = ["remote", "sync", container]
    if to_host:
        args.extend(["--to", to_host])
    else:
        args.extend(["--from", from_host])

    rc, stdout, stderr = _run_gaol(*args, timeout=30)
    if rc == 0:
        return json.dumps({"success": True, "output": stdout.strip()})
    return json.dumps({"success": False, "error": stderr.strip()})


# ---------------------------------------------------------------------------
# Tools — Feature requests
# ---------------------------------------------------------------------------


@mcp.tool()
def gaol_feature_request(
    description: str,
    category: str = "tool",
    context: str = "",
    requesting_project: str = "",
) -> str:
    """Report a missing feature or capability gap in gaol.

    Use this when you need functionality that gaol doesn't currently support.
    Requests are logged for the gaol maintainer to review.

    Args:
        description: What functionality is needed and why.
        category: One of: tool, resource, workflow, bug.
        context: Additional context (e.g., what you were trying to do).
        requesting_project: The project directory requesting this feature.

    Returns:
        Confirmation that the request was logged.
    """
    entry = {
        "timestamp": datetime.now().isoformat(),
        "category": category,
        "description": description,
        "context": context,
        "requesting_project": requesting_project,
    }
    FEATURE_REQUESTS_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(FEATURE_REQUESTS_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")
    logger.info("Feature request logged: %s", description)
    return json.dumps({"logged": True, "entry": entry})


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


def main() -> None:
    """Run the gaol MCP server."""
    logger.info("Starting gaol MCP server")
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
