"""Vagrant preflight checks for vm-blackbox plugin compatibility."""

from __future__ import annotations

import asyncio
import re
from pathlib import Path
from typing import Annotated

from fastmcp.tools import tool
from pydantic import Field

from mcp_vm_blackbox._helpers import effective_root
from mcp_vm_blackbox.models import OverallStatus, PreflightCheck, VagrantPreflightResult

# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

_WINDOWS_BOX_PATTERNS: re.Pattern[str] = re.compile(
    r"windows|win[^a-z]|server.?2022|server2022|gusztavvargadr", re.IGNORECASE
)
_VB_NAME_RE: re.Pattern[str] = re.compile(r'vb\.name\s*=\s*["\']([^"\']+)["\']')
_PRIVATE_NETWORK_IP_RE: re.Pattern[str] = re.compile(r"private_network.*?ip\s*:", re.DOTALL)
_SYNCED_FOLDER_RE: re.Pattern[str] = re.compile(r"synced_folder")
_PROVISIONER_PATH_RE: re.Pattern[str] = re.compile(r'path\s*:\s*["\']([^"\']+)["\']')
_COMMUNICATOR_RE: re.Pattern[str] = re.compile(r'communicator\s*[=,]\s*["\']?(winrm|winssh)["\']?', re.IGNORECASE)
_VM_DEFINE_RE: re.Pattern[str] = re.compile(r'config\.vm\.define\s+["\']?(\w+)')


async def _run_cmd(*args: str) -> tuple[int, str, str]:
    """Run an external command and return (returncode, stdout, stderr).

    Args:
        *args: Command and arguments to execute.

    Returns:
        Tuple of (returncode, stdout, stderr).
    """
    try:
        proc = await asyncio.create_subprocess_exec(
            *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )
        stdout_b, stderr_b = await proc.communicate()
        rc = proc.returncode or 0
        return (rc, stdout_b.decode("utf-8", errors="replace"), stderr_b.decode("utf-8", errors="replace"))
    except FileNotFoundError:
        return -1, "", f"command not found: {args[0]}"


def _overall(checks: list[PreflightCheck]) -> OverallStatus:
    """Compute overall status from individual check results.

    Args:
        checks: List of completed preflight checks.

    Returns:
        'fail' if any check failed, 'warn' if any warned and none failed, else 'pass'.
    """
    statuses = {c.status for c in checks}
    if "fail" in statuses:
        return "fail"
    if "warn" in statuses:
        return "warn"
    return "pass"


# ---------------------------------------------------------------------------
# Individual checks
# ---------------------------------------------------------------------------


def _check_vagrantfile_exists(vagrantfile_path: Path) -> PreflightCheck:
    """Check 1 — Vagrantfile exists at the given path.

    Args:
        vagrantfile_path: Expected path to the Vagrantfile.

    Returns:
        PreflightCheck result.
    """
    if vagrantfile_path.is_file():
        return PreflightCheck(
            name="vagrantfile_exists", status="pass", message=f"Vagrantfile found at {vagrantfile_path}"
        )
    return PreflightCheck(
        name="vagrantfile_exists", status="fail", message=f"Vagrantfile not found at {vagrantfile_path}"
    )


async def _check_vagrant_cli() -> tuple[PreflightCheck, str | None]:
    """Check 2 — vagrant CLI is available.

    Returns:
        Tuple of (PreflightCheck, version_string_or_None).
    """
    rc, stdout, stderr = await _run_cmd("vagrant", "--version")
    if rc == -1:
        return (
            PreflightCheck(
                name="vagrant_cli",
                status="warn",
                message="vagrant CLI not found — may be on a different machine",
                detail=stderr.strip() or None,
            ),
            None,
        )
    version = stdout.strip() or stderr.strip()
    return (
        PreflightCheck(name="vagrant_cli", status="pass", message=f"vagrant CLI available: {version}", detail=version),
        version,
    )


async def _check_vboxmanage() -> tuple[PreflightCheck, str | None]:
    """Check 10 — VBoxManage is installed.

    Returns:
        Tuple of (PreflightCheck, version_string_or_None).
    """
    rc, stdout, stderr = await _run_cmd("vboxmanage", "--version")
    if rc == -1:
        return (
            PreflightCheck(
                name="vboxmanage_installed",
                status="warn",
                message="VBoxManage not found — VMs cannot be started without it",
                detail=stderr.strip() or None,
            ),
            None,
        )
    version = stdout.strip() or stderr.strip()
    return (
        PreflightCheck(
            name="vboxmanage_installed", status="pass", message=f"VBoxManage available: {version}", detail=version
        ),
        version,
    )


def _check_virtualbox_provider(text: str) -> PreflightCheck:
    """Check 3 — Vagrantfile references virtualbox provider.

    Args:
        text: Full Vagrantfile content.

    Returns:
        PreflightCheck result.
    """
    if re.search(r"virtualbox", text, re.IGNORECASE):
        return PreflightCheck(name="virtualbox_provider", status="pass", message="VirtualBox provider reference found")
    return PreflightCheck(
        name="virtualbox_provider",
        status="warn",
        message="No 'virtualbox' provider reference found — vm-blackbox requires VirtualBox",
    )


def _check_vm_names(text: str) -> tuple[PreflightCheck, list[str]]:
    """Check 4 — Explicit vb.name assignments exist.

    Args:
        text: Full Vagrantfile content.

    Returns:
        Tuple of (PreflightCheck, list_of_vm_names_found).
    """
    names = _VB_NAME_RE.findall(text)
    if not names:
        return (
            PreflightCheck(
                name="explicit_vm_names",
                status="fail",
                message=(
                    "No vb.name assignments found. VirtualBox will generate unpredictable "
                    'names that will not match vm_list output. Add vb.name = "..." inside '
                    "each config.vm.provider 'virtualbox' block."
                ),
            ),
            [],
        )
    return (
        PreflightCheck(
            name="explicit_vm_names",
            status="pass",
            message=f"Found {len(names)} explicit vb.name assignment(s): {', '.join(names)}",
            detail=", ".join(names),
        ),
        names,
    )


def _check_winrm_communicators(text: str) -> PreflightCheck:
    """Check 5 — Windows VMs have WinRM communicator set.

    Args:
        text: Full Vagrantfile content.

    Returns:
        PreflightCheck result.
    """
    # Find box name declarations
    box_matches = re.findall(r'(?:config|vm)\.vm\.box\s*=\s*["\']([^"\']+)["\']', text)
    # Also look inside define blocks: config.vm.define "name" do |vm| vm.vm.box = "..."
    # We check globally — any box name that looks Windows-ish
    windows_boxes = [b for b in box_matches if _WINDOWS_BOX_PATTERNS.search(b)]

    if not windows_boxes:
        return PreflightCheck(
            name="winrm_communicator",
            status="pass",
            message="No Windows-looking box names detected — WinRM check skipped",
        )

    if _COMMUNICATOR_RE.search(text):
        return PreflightCheck(
            name="winrm_communicator",
            status="pass",
            message=f"Windows VM(s) detected ({', '.join(windows_boxes)}) and WinRM/WinSSH communicator is set",
            detail=", ".join(windows_boxes),
        )

    return PreflightCheck(
        name="winrm_communicator",
        status="fail",
        message=(
            f"Windows VM(s) detected ({', '.join(windows_boxes)}) but no WinRM/WinSSH communicator found. "
            "Add: config.vm.communicator = 'winrm' — vm_powershell will not work without it."
        ),
        detail=", ".join(windows_boxes),
    )


def _check_private_network_ips(text: str) -> PreflightCheck:
    """Check 6 — At least one private_network IP assignment exists.

    Args:
        text: Full Vagrantfile content.

    Returns:
        PreflightCheck result.
    """
    matches = _PRIVATE_NETWORK_IP_RE.findall(text)
    count = len(matches)
    if count == 0:
        return PreflightCheck(
            name="private_network_ips",
            status="warn",
            message=(
                "No private_network ip: entries found — cross-VM communication and known IP addressing will not work"
            ),
        )
    return PreflightCheck(
        name="private_network_ips", status="pass", message=f"Found {count} private_network IP assignment(s)"
    )


def _check_synced_folders(text: str) -> PreflightCheck:
    """Check 7 — Synced folder entries are present.

    Args:
        text: Full Vagrantfile content.

    Returns:
        PreflightCheck result.
    """
    count = len(_SYNCED_FOLDER_RE.findall(text))
    if count == 0:
        return PreflightCheck(
            name="synced_folders",
            status="warn",
            message="No synced_folder entries found — file transfer to/from VM may not work",
        )
    return PreflightCheck(name="synced_folders", status="pass", message=f"Found {count} synced_folder entry/entries")


def _check_provisioner_scripts(text: str, vagrantfile_dir: Path) -> PreflightCheck:
    """Check 8 — Shell provisioner path: references exist on disk.

    Args:
        text: Full Vagrantfile content.
        vagrantfile_dir: Directory containing the Vagrantfile.

    Returns:
        PreflightCheck result.
    """
    paths = _PROVISIONER_PATH_RE.findall(text)
    if not paths:
        return PreflightCheck(
            name="provisioner_scripts", status="pass", message="No shell provisioner path: references found"
        )

    missing = [p for p in paths if not (vagrantfile_dir / p).is_file()]
    if missing:
        return PreflightCheck(
            name="provisioner_scripts",
            status="fail",
            message=f"Missing provisioner script(s): {', '.join(missing)}",
            detail=", ".join(missing),
        )
    return PreflightCheck(
        name="provisioner_scripts", status="pass", message=f"All {len(paths)} provisioner script(s) exist on disk"
    )


async def _check_vagrant_validate(vagrantfile_dir: Path) -> PreflightCheck:
    """Check 9 — vagrant validate passes in vagrantfile_dir.

    Args:
        vagrantfile_dir: Directory containing the Vagrantfile.

    Returns:
        PreflightCheck result.
    """
    rc, stdout, stderr = await _run_cmd("vagrant", "validate", "--vagrantfile", str(vagrantfile_dir / "Vagrantfile"))
    if rc == -1:
        return PreflightCheck(
            name="vagrant_validate", status="skip", message="vagrant CLI unavailable — skipping vagrant validate"
        )
    if rc == 0:
        return PreflightCheck(
            name="vagrant_validate", status="pass", message="vagrant validate passed", detail=stdout.strip() or None
        )
    detail = (stderr.strip() or stdout.strip()) or None
    return PreflightCheck(name="vagrant_validate", status="fail", message="vagrant validate failed", detail=detail)


# ---------------------------------------------------------------------------
# Static analysis bundle
# ---------------------------------------------------------------------------


def _run_static_checks(text: str, vagrantfile_dir: Path) -> tuple[list[PreflightCheck], list[str]]:
    """Run all static Vagrantfile checks (checks 3-8).

    Args:
        text: Full Vagrantfile content.
        vagrantfile_dir: Directory containing the Vagrantfile.

    Returns:
        Tuple of (list_of_checks, vm_names_found).
    """
    names_check, vm_names = _check_vm_names(text)
    return (
        [
            _check_virtualbox_provider(text),
            names_check,
            _check_winrm_communicators(text),
            _check_private_network_ips(text),
            _check_synced_folders(text),
            _check_provisioner_scripts(text, vagrantfile_dir),
        ],
        vm_names,
    )


# ---------------------------------------------------------------------------
# MCP tool
# ---------------------------------------------------------------------------


@tool
async def vagrant_preflight(
    vagrantfile_dir: Annotated[
        str, Field(description=("Path to directory containing the Vagrantfile. Defaults to project root when empty."))
    ] = "",
) -> VagrantPreflightResult:
    """Validate a Vagrantfile has everything vm-blackbox needs before vagrant_up.

    Runs ten checks covering: Vagrantfile presence, vagrant/VBoxManage CLI
    availability, VirtualBox provider, explicit VM names (vb.name), WinRM
    communicator for Windows VMs, private network IPs, synced folders,
    provisioner script existence, vagrant validate, and VBoxManage install.

    Args:
        vagrantfile_dir: Path to the directory containing the Vagrantfile.
            When empty, uses the project root derived from the module location.

    Returns:
        VagrantPreflightResult with per-check status and overall verdict.
    """
    root_str = effective_root(vagrantfile_dir or None)
    root = Path(root_str)
    vagrantfile_path = root / "Vagrantfile"

    # --- Check 1: Vagrantfile exists ---
    vf_check = _check_vagrantfile_exists(vagrantfile_path)
    if vf_check.status == "fail":
        return VagrantPreflightResult(
            vagrantfile_path=str(vagrantfile_path),
            overall="fail",
            checks=[vf_check],
            error=f"Vagrantfile not found at {vagrantfile_path}",
        )

    text = vagrantfile_path.read_text(encoding="utf-8", errors="replace")

    # --- Checks 2 and 10: CLI tools (run concurrently) ---
    (vagrant_check, vagrant_version), (vbox_check, vbox_version) = await asyncio.gather(
        _check_vagrant_cli(), _check_vboxmanage()
    )

    static_checks, vm_names = _run_static_checks(text, root)

    # --- Check 9: vagrant validate (only when vagrant CLI is available) ---
    if vagrant_check.status == "pass":
        validate_check = await _check_vagrant_validate(root)
    else:
        validate_check = PreflightCheck(
            name="vagrant_validate", status="skip", message="vagrant CLI unavailable -- skipping vagrant validate"
        )

    checks: list[PreflightCheck] = [vf_check, vagrant_check, *static_checks, validate_check, vbox_check]

    return VagrantPreflightResult(
        vagrantfile_path=str(vagrantfile_path),
        overall=_overall(checks),
        checks=checks,
        vm_names=vm_names,
        vagrant_version=vagrant_version,
        vboxmanage_version=vbox_version,
    )
