"""Public CLI for pyro-mcp."""

from __future__ import annotations

import argparse
import json
import sys
from textwrap import dedent
from typing import Any

from pyro_mcp import __version__
from pyro_mcp.api import Pyro
from pyro_mcp.demo import run_demo
from pyro_mcp.ollama_demo import DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, run_ollama_tool_demo
from pyro_mcp.runtime import DEFAULT_PLATFORM, doctor_report
from pyro_mcp.vm_environments import DEFAULT_CATALOG_VERSION
from pyro_mcp.vm_manager import (
    DEFAULT_MEM_MIB,
    DEFAULT_VCPU_COUNT,
)


def _print_json(payload: dict[str, Any]) -> None:
    print(json.dumps(payload, indent=2, sort_keys=True))


def _write_stream(text: str, *, stream: Any) -> None:
    if text == "":
        return
    stream.write(text)
    stream.flush()


def _print_run_human(payload: dict[str, Any]) -> None:
    stdout = str(payload.get("stdout", ""))
    stderr = str(payload.get("stderr", ""))
    _write_stream(stdout, stream=sys.stdout)
    _write_stream(stderr, stream=sys.stderr)
    print(
        "[run] "
        f"environment={str(payload.get('environment', 'unknown'))} "
        f"execution_mode={str(payload.get('execution_mode', 'unknown'))} "
        f"exit_code={int(payload.get('exit_code', 1))} "
        f"duration_ms={int(payload.get('duration_ms', 0))}",
        file=sys.stderr,
        flush=True,
    )


def _print_env_list_human(payload: dict[str, Any]) -> None:
    print(f"Catalog version: {payload.get('catalog_version', 'unknown')}")
    environments = payload.get("environments")
    if not isinstance(environments, list) or not environments:
        print("No environments found.")
        return
    for entry in environments:
        if not isinstance(entry, dict):
            continue
        status = "installed" if bool(entry.get("installed")) else "not installed"
        print(
            f"{str(entry.get('name', 'unknown'))} [{status}] "
            f"{str(entry.get('description', '')).strip()}".rstrip()
        )


def _print_env_detail_human(payload: dict[str, Any], *, action: str) -> None:
    print(f"{action}: {str(payload.get('name', 'unknown'))}")
    print(f"Version: {str(payload.get('version', 'unknown'))}")
    print(
        f"Distribution: {str(payload.get('distribution', 'unknown'))} "
        f"{str(payload.get('distribution_version', 'unknown'))}"
    )
    print(f"Installed: {'yes' if bool(payload.get('installed')) else 'no'}")
    print(f"Cache dir: {str(payload.get('cache_dir', 'unknown'))}")
    packages = payload.get("default_packages")
    if isinstance(packages, list) and packages:
        print("Default packages: " + ", ".join(str(item) for item in packages))
    description = str(payload.get("description", "")).strip()
    if description != "":
        print(f"Description: {description}")
    if payload.get("installed"):
        print(f"Install dir: {str(payload.get('install_dir', 'unknown'))}")
        install_manifest = payload.get("install_manifest")
        if install_manifest is not None:
            print(f"Install manifest: {str(install_manifest)}")
        kernel_image = payload.get("kernel_image")
        if kernel_image is not None:
            print(f"Kernel image: {str(kernel_image)}")
        rootfs_image = payload.get("rootfs_image")
        if rootfs_image is not None:
            print(f"Rootfs image: {str(rootfs_image)}")
    registry = payload.get("oci_registry")
    repository = payload.get("oci_repository")
    reference = payload.get("oci_reference")
    if isinstance(registry, str) and isinstance(repository, str) and isinstance(reference, str):
        print(f"OCI source: {registry}/{repository}:{reference}")


def _print_prune_human(payload: dict[str, Any]) -> None:
    count = int(payload.get("count", 0))
    print(f"Deleted {count} cached environment entr{'y' if count == 1 else 'ies'}.")
    deleted = payload.get("deleted_environment_dirs")
    if isinstance(deleted, list):
        for entry in deleted:
            print(f"- {entry}")


def _print_doctor_human(payload: dict[str, Any]) -> None:
    issues = payload.get("issues")
    runtime_ok = bool(payload.get("runtime_ok"))
    print(f"Platform: {str(payload.get('platform', 'unknown'))}")
    print(f"Runtime: {'PASS' if runtime_ok else 'FAIL'}")
    kvm = payload.get("kvm")
    if isinstance(kvm, dict):
        print(
            "KVM: "
            f"exists={'yes' if bool(kvm.get('exists')) else 'no'} "
            f"readable={'yes' if bool(kvm.get('readable')) else 'no'} "
            f"writable={'yes' if bool(kvm.get('writable')) else 'no'}"
        )
    runtime = payload.get("runtime")
    if isinstance(runtime, dict):
        print(f"Environment cache: {str(runtime.get('cache_dir', 'unknown'))}")
        capabilities = runtime.get("capabilities")
        if isinstance(capabilities, dict):
            print(
                "Capabilities: "
                f"vm_boot={'yes' if bool(capabilities.get('supports_vm_boot')) else 'no'} "
                f"guest_exec={'yes' if bool(capabilities.get('supports_guest_exec')) else 'no'} "
                "guest_network="
                f"{'yes' if bool(capabilities.get('supports_guest_network')) else 'no'}"
            )
    networking = payload.get("networking")
    if isinstance(networking, dict):
        print(
            "Networking: "
            f"tun={'yes' if bool(networking.get('tun_available')) else 'no'} "
            f"ip_forward={'yes' if bool(networking.get('ip_forward_enabled')) else 'no'}"
        )
    if isinstance(issues, list) and issues:
        print("Issues:")
        for issue in issues:
            print(f"- {issue}")


class _HelpFormatter(
    argparse.RawDescriptionHelpFormatter,
    argparse.ArgumentDefaultsHelpFormatter,
):
    """Help formatter with examples and default values."""

    def _get_help_string(self, action: argparse.Action) -> str:
        if action.default is None and action.help is not None:
            return action.help
        help_string = super()._get_help_string(action)
        if help_string is None:
            return ""
        return help_string


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=(
            "Run ephemeral Firecracker microVM workflows from the CLI on supported "
            "Linux x86_64 KVM hosts."
        ),
        epilog=dedent(
            """
            Suggested first run:
              pyro doctor
              pyro env pull debian:12
              pyro run debian:12 -- git --version

            Use `pyro mcp serve` only after the CLI validation path works.
            """
        ),
        formatter_class=_HelpFormatter,
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
    subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")

    env_parser = subparsers.add_parser(
        "env",
        help="Inspect and manage curated environments.",
        description="Inspect, install, and prune curated Linux environments.",
        epilog=dedent(
            """
            Examples:
              pyro env list
              pyro env pull debian:12
              pyro env inspect debian:12
            """
        ),
        formatter_class=_HelpFormatter,
    )
    env_subparsers = env_parser.add_subparsers(dest="env_command", required=True, metavar="ENV")
    list_parser = env_subparsers.add_parser(
        "list",
        help="List official environments.",
        description="List the shipped environment catalog and show local install status.",
        epilog="Example:\n  pyro env list",
        formatter_class=_HelpFormatter,
    )
    list_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )
    pull_parser = env_subparsers.add_parser(
        "pull",
        help="Install an environment into the local cache.",
        description="Download and install one official environment into the local cache.",
        epilog="Example:\n  pyro env pull debian:12",
        formatter_class=_HelpFormatter,
    )
    pull_parser.add_argument(
        "environment",
        metavar="ENVIRONMENT",
        help="Environment name from `pyro env list`, for example `debian:12`.",
    )
    pull_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )
    inspect_parser = env_subparsers.add_parser(
        "inspect",
        help="Inspect one environment.",
        description="Show catalog and local cache details for one environment.",
        epilog="Example:\n  pyro env inspect debian:12",
        formatter_class=_HelpFormatter,
    )
    inspect_parser.add_argument(
        "environment",
        metavar="ENVIRONMENT",
        help="Environment name from `pyro env list`, for example `debian:12`.",
    )
    inspect_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )
    prune_parser = env_subparsers.add_parser(
        "prune",
        help="Delete stale cached environments.",
        description="Remove cached environment installs that are no longer referenced.",
        epilog="Example:\n  pyro env prune",
        formatter_class=_HelpFormatter,
    )
    prune_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )

    mcp_parser = subparsers.add_parser(
        "mcp",
        help="Run the MCP server.",
        description=(
            "Run the MCP server after you have already validated the host and "
            "guest execution with `pyro doctor` and `pyro run`."
        ),
        epilog="Example:\n  pyro mcp serve",
        formatter_class=_HelpFormatter,
    )
    mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True, metavar="MCP")
    mcp_subparsers.add_parser(
        "serve",
        help="Run the MCP server over stdio.",
        description="Expose pyro tools over stdio for an MCP client.",
        epilog=dedent(
            """
            Example:
              pyro mcp serve

            Use this from an MCP client config after the CLI evaluation path works.
            """
        ),
        formatter_class=_HelpFormatter,
    )

    run_parser = subparsers.add_parser(
        "run",
        help="Run one command inside an ephemeral VM.",
        description="Run one non-interactive command in a temporary Firecracker microVM.",
        epilog=dedent(
            """
            Examples:
              pyro run debian:12 -- git --version
              pyro run debian:12 --network -- git ls-remote https://github.com/octocat/Hello-World.git
            """
        ),
        formatter_class=_HelpFormatter,
    )
    run_parser.add_argument(
        "environment",
        metavar="ENVIRONMENT",
        help="Curated environment to boot, for example `debian:12`.",
    )
    run_parser.add_argument(
        "--vcpu-count",
        type=int,
        default=DEFAULT_VCPU_COUNT,
        help="Number of virtual CPUs to allocate to the guest.",
    )
    run_parser.add_argument(
        "--mem-mib",
        type=int,
        default=DEFAULT_MEM_MIB,
        help="Guest memory allocation in MiB.",
    )
    run_parser.add_argument(
        "--timeout-seconds",
        type=int,
        default=30,
        help="Maximum time allowed for the guest command.",
    )
    run_parser.add_argument(
        "--ttl-seconds",
        type=int,
        default=600,
        help="Time-to-live for temporary VM artifacts before cleanup.",
    )
    run_parser.add_argument(
        "--network",
        action="store_true",
        help="Enable outbound guest networking. Requires TAP/NAT privileges on the host.",
    )
    run_parser.add_argument(
        "--allow-host-compat",
        action="store_true",
        help=(
            "Opt into host-side compatibility execution if guest boot or guest exec "
            "is unavailable."
        ),
    )
    run_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )
    run_parser.add_argument(
        "command_args",
        nargs="*",
        metavar="ARG",
        help=(
            "Command and arguments to run inside the guest. Prefix them with `--`, "
            "for example `pyro run debian:12 -- git --version`."
        ),
    )

    doctor_parser = subparsers.add_parser(
        "doctor",
        help="Inspect runtime and host diagnostics.",
        description="Check host prerequisites and embedded runtime health before your first run.",
        epilog=dedent(
            """
            Examples:
              pyro doctor
              pyro doctor --json
            """
        ),
        formatter_class=_HelpFormatter,
    )
    doctor_parser.add_argument(
        "--platform",
        default=DEFAULT_PLATFORM,
        help="Runtime platform to inspect.",
    )
    doctor_parser.add_argument(
        "--json",
        action="store_true",
        help="Print structured JSON instead of human-readable output.",
    )

    demo_parser = subparsers.add_parser(
        "demo",
        help="Run built-in demos.",
        description="Run built-in demos after the basic CLI validation path works.",
        epilog=dedent(
            """
            Examples:
              pyro demo
              pyro demo --network
              pyro demo ollama --verbose
            """
        ),
        formatter_class=_HelpFormatter,
    )
    demo_subparsers = demo_parser.add_subparsers(dest="demo_command", metavar="DEMO")
    demo_parser.add_argument(
        "--network",
        action="store_true",
        help="Enable outbound guest networking for the deterministic demo.",
    )
    ollama_parser = demo_subparsers.add_parser(
        "ollama",
        help="Run the Ollama MCP demo.",
        description="Run the Ollama tool-calling demo against the `vm_run` and lifecycle tools.",
        epilog=dedent(
            """
            Example:
              pyro demo ollama --model llama3.2:3b --verbose
            """
        ),
        formatter_class=_HelpFormatter,
    )
    ollama_parser.add_argument(
        "--base-url",
        default=DEFAULT_OLLAMA_BASE_URL,
        help="OpenAI-compatible base URL for the Ollama server.",
    )
    ollama_parser.add_argument(
        "--model",
        default=DEFAULT_OLLAMA_MODEL,
        help="Ollama model name to use for tool calling.",
    )
    ollama_parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Print full tool loop output instead of only the summary.",
    )

    return parser


def _require_command(command_args: list[str]) -> str:
    if command_args and command_args[0] == "--":
        command_args = command_args[1:]
    if not command_args:
        raise ValueError("command is required after `pyro run --`")
    return " ".join(command_args)


def main() -> None:
    args = _build_parser().parse_args()
    pyro = Pyro()
    if args.command == "env":
        if args.env_command == "list":
            list_payload: dict[str, Any] = {
                "catalog_version": DEFAULT_CATALOG_VERSION,
                "environments": pyro.list_environments(),
            }
            if bool(args.json):
                _print_json(list_payload)
            else:
                _print_env_list_human(list_payload)
            return
        if args.env_command == "pull":
            pull_payload = pyro.pull_environment(args.environment)
            if bool(args.json):
                _print_json(pull_payload)
            else:
                _print_env_detail_human(pull_payload, action="Pulled")
            return
        if args.env_command == "inspect":
            inspect_payload = pyro.inspect_environment(args.environment)
            if bool(args.json):
                _print_json(inspect_payload)
            else:
                _print_env_detail_human(inspect_payload, action="Environment")
            return
        if args.env_command == "prune":
            prune_payload = pyro.prune_environments()
            if bool(args.json):
                _print_json(prune_payload)
            else:
                _print_prune_human(prune_payload)
            return
    if args.command == "mcp":
        pyro.create_server().run(transport="stdio")
        return
    if args.command == "run":
        command = _require_command(args.command_args)
        try:
            result = pyro.run_in_vm(
                environment=args.environment,
                command=command,
                vcpu_count=args.vcpu_count,
                mem_mib=args.mem_mib,
                timeout_seconds=args.timeout_seconds,
                ttl_seconds=args.ttl_seconds,
                network=args.network,
                allow_host_compat=args.allow_host_compat,
            )
        except Exception as exc:  # noqa: BLE001
            if bool(args.json):
                _print_json({"ok": False, "error": str(exc)})
            else:
                print(f"[error] {exc}", file=sys.stderr, flush=True)
            raise SystemExit(1) from exc
        if bool(args.json):
            _print_json(result)
        else:
            _print_run_human(result)
        exit_code = int(result.get("exit_code", 1))
        if exit_code != 0:
            raise SystemExit(exit_code)
        return
    if args.command == "doctor":
        payload = doctor_report(platform=args.platform)
        if bool(args.json):
            _print_json(payload)
        else:
            _print_doctor_human(payload)
        return
    if args.command == "demo" and args.demo_command == "ollama":
        try:
            result = run_ollama_tool_demo(
                base_url=args.base_url,
                model=args.model,
                verbose=args.verbose,
                log=lambda message: print(message, flush=True),
            )
        except Exception as exc:  # noqa: BLE001
            print(f"[error] {exc}", flush=True)
            raise SystemExit(1) from exc
        exec_result = result["exec_result"]
        if not isinstance(exec_result, dict):
            raise RuntimeError("demo produced invalid execution result")
        print(
            f"[summary] exit_code={int(exec_result.get('exit_code', -1))} "
            f"fallback_used={bool(result.get('fallback_used'))} "
            f"execution_mode={str(exec_result.get('execution_mode', 'unknown'))}",
            flush=True,
        )
        if args.verbose:
            print(f"[summary] stdout={str(exec_result.get('stdout', '')).strip()}", flush=True)
        return
    result = run_demo(network=bool(args.network))
    _print_json(result)
