#!/usr/bin/env python3
"""Preflight checks for the Python CLI MCP template."""

from __future__ import annotations

import argparse
import json
import os
import shutil
import sys
from pathlib import Path


DEFAULT_BINARY = "project-cli"


def _result(ok: bool, label: str, detail: str) -> dict[str, object]:
    return {"ok": ok, "label": label, "detail": detail}


def _load_dotenv(path: Path) -> None:
    if not path.exists():
        return
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()
        if key and key not in os.environ:
            os.environ[key] = value


_load_dotenv(Path(__file__).with_name(".env"))


def _split_roots(raw: str, default: Path) -> list[Path]:
    if not raw.strip():
        return [default.resolve()]
    roots: list[Path] = []
    for item in raw.split(","):
        item = item.strip()
        if item:
            roots.append(Path(item).expanduser().resolve())
    return roots


def _print_result(ok: bool, label: str, detail: str) -> None:
    status = "OK" if ok else "FAIL"
    print(f"[{status}] {label}: {detail}")


def _check_python() -> dict[str, object]:
    detail = f"{sys.executable} (Python {sys.version.split()[0]})"
    return _result(True, "python", detail)


def _check_mcp_import() -> dict[str, object]:
    try:
        import mcp  # noqa: F401
    except ImportError as exc:
        return _result(False, "mcp import", str(exc))
    return _result(True, "mcp import", "package is importable")


def _check_server(server_path: Path) -> dict[str, object]:
    if not server_path.exists():
        return _result(False, "server path", f"file does not exist: {server_path}")
    try:
        source = server_path.read_text(encoding="utf-8")
        compile(source, str(server_path), "exec")
    except OSError as exc:
        return _result(False, "server path", str(exc))
    except SyntaxError as exc:
        return _result(False, "server syntax", f"{server_path}:{exc.lineno}: {exc.msg}")
    return _result(True, "server path", str(server_path))


def _check_binary() -> dict[str, object]:
    configured = os.getenv("PROJECT_BIN", "").strip()
    if configured:
        binary_path = Path(configured).expanduser().resolve()
        if not binary_path.exists():
            return _result(False, "PROJECT_BIN", f"does not exist: {binary_path}")
        if not os.access(binary_path, os.X_OK):
            return _result(False, "PROJECT_BIN", f"not executable: {binary_path}")
        return _result(True, "PROJECT_BIN", str(binary_path))

    located = shutil.which(DEFAULT_BINARY)
    if not located:
        return _result(
            False,
            "PROJECT_BIN",
            f"unset and fallback binary '{DEFAULT_BINARY}' was not found on PATH",
        )
    return _result(True, "PROJECT_BIN", f"unset; using PATH fallback {located}")


def _check_roots(env_key: str, default: Path) -> list[dict[str, object]]:
    raw = os.getenv(env_key, "")
    roots = _split_roots(raw, default)
    results: list[dict[str, object]] = []
    for root in roots:
        if not root.exists():
            results.append(_result(False, env_key, f"missing path: {root}"))
            continue
        if not root.is_dir():
            results.append(_result(False, env_key, f"not a directory: {root}"))
            continue
        results.append(_result(True, env_key, str(root)))
    return results


def _check_timeout() -> dict[str, object]:
    raw = os.getenv("PROJECT_CMD_TIMEOUT_SEC", "180")
    try:
        timeout = int(raw)
    except ValueError:
        return _result(False, "PROJECT_CMD_TIMEOUT_SEC", f"not an integer: {raw}")
    if timeout <= 0:
        return _result(False, "PROJECT_CMD_TIMEOUT_SEC", f"must be > 0, got {timeout}")
    return _result(True, "PROJECT_CMD_TIMEOUT_SEC", str(timeout))


def _validate_server_entry(name: str, entry: object) -> str | None:
    if not isinstance(entry, dict):
        return f"server '{name}' must be a JSON object"

    command = entry.get("command")
    if not isinstance(command, str) or not command.strip():
        return f"server '{name}' is missing a non-empty 'command' string"

    args = entry.get("args", [])
    if not isinstance(args, list) or any(not isinstance(item, str) for item in args):
        return f"server '{name}' must use a string-only 'args' array when provided"

    if not args:
        return f"server '{name}' should include an 'args' entry pointing to the server path"

    env = entry.get("env", {})
    if not isinstance(env, dict):
        return f"server '{name}' must use an object for 'env' when provided"
    if any(not isinstance(key, str) or not isinstance(value, str) for key, value in env.items()):
        return f"server '{name}' must use string keys and values in 'env'"

    server_type = entry.get("type")
    if server_type is not None and (not isinstance(server_type, str) or not server_type.strip()):
        return f"server '{name}' must use a non-empty string for 'type' when provided"

    return None


def _check_config(config_path: Path) -> dict[str, object]:
    if not config_path.exists():
        return _result(False, "config", f"file does not exist: {config_path}")
    try:
        data = json.loads(config_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        return _result(False, "config", f"{config_path}: {exc}")

    if not isinstance(data, dict):
        return _result(False, "config", f"{config_path}: top-level JSON must be an object")

    servers = data.get("mcpServers")
    if not isinstance(servers, dict) or not servers:
        return _result(False, "config", f"{config_path}: missing non-empty 'mcpServers' object")

    for name, entry in servers.items():
        if not isinstance(name, str) or not name.strip():
            return _result(False, "config", f"{config_path}: mcpServers keys must be non-empty strings")
        error = _validate_server_entry(name, entry)
        if error:
            return _result(False, "config", f"{config_path}: {error}")

    return _result(True, "config", f"{config_path}: found {len(servers)} server entry(s)")


def _run_checks(server_path: Path, config_paths: list[Path]) -> list[dict[str, object]]:
    results = [
        _check_python(),
        _check_mcp_import(),
        _check_server(server_path),
        _check_binary(),
        *_check_roots("PROJECT_ALLOWED_READ_DIRS", Path.cwd()),
        *_check_roots("PROJECT_ALLOWED_WRITE_DIRS", Path.cwd()),
        _check_timeout(),
    ]

    for config_path in config_paths:
        results.append(_check_config(config_path))
    return results


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Validate Python CLI MCP template prerequisites before client setup."
    )
    parser.add_argument(
        "--server",
        default=Path(__file__).with_name("server.py"),
        type=Path,
        help="Path to the MCP server Python file.",
    )
    parser.add_argument(
        "--config",
        action="append",
        default=[],
        type=Path,
        help="Optional MCP client config JSON file to validate. Repeat for multiple files.",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Print machine-readable JSON results instead of line-oriented text.",
    )
    args = parser.parse_args()

    results = _run_checks(
        args.server.expanduser().resolve(),
        [config_path.expanduser().resolve() for config_path in args.config],
    )
    ok = all(bool(result["ok"]) for result in results)

    if args.json:
        print(
            json.dumps(
                {
                    "ok": ok,
                    "results": results,
                    "summary": (
                        "doctor result: all checks passed"
                        if ok
                        else "doctor result: fix failing checks before configuring clients or deploying"
                    ),
                },
                ensure_ascii=True,
            )
        )
        return 0 if ok else 1

    for result in results:
        _print_result(bool(result["ok"]), str(result["label"]), str(result["detail"]))

    if ok:
        print("doctor result: all checks passed")
        return 0

    print("doctor result: fix failing checks before configuring clients or deploying")
    return 1


if __name__ == "__main__":
    raise SystemExit(main())
