"""Core logic for the ``mld doctor`` command.

Validates plugin project structure, entry points, SDK dependencies,
and source files without executing any plugin code.
"""

from __future__ import annotations

import argparse
import json

from mld_sdk.info_command import _supports_color
import re
import sys
from dataclasses import dataclass
from pathlib import Path

try:
    import tomllib
except ModuleNotFoundError:  # pragma: no cover - Python < 3.11 fallback
    import tomli as tomllib  # type: ignore[no-redef]


@dataclass(slots=True)
class CheckResult:
    """Result of a single diagnostic check."""

    ok: bool
    label: str
    detail: str = ""


def _check_pyproject(project_dir: Path) -> tuple[CheckResult, dict | None]:
    """Check that pyproject.toml exists and has a [project] table."""
    path = project_dir / "pyproject.toml"
    if not path.exists():
        return CheckResult(False, "pyproject.toml", "not found"), None

    try:
        data = tomllib.loads(path.read_text())
    except Exception as exc:
        return CheckResult(False, "pyproject.toml", f"parse error: {exc}"), None

    if "project" not in data:
        return CheckResult(False, "pyproject.toml", "missing [project] table"), None

    name = data["project"].get("name", "")
    return CheckResult(True, "pyproject.toml", name), data


def _check_entry_point(data: dict | None) -> tuple[CheckResult, str | None, str | None, str | None]:
    """Check that an mld.plugins entry point is defined with correct format."""
    if data is None:
        return CheckResult(False, "Entry point", "skipped (no pyproject.toml)"), None, None, None

    eps = (
        data.get("project", {})
        .get("entry-points", {})
        .get("mld.plugins", {})
    )
    if not eps:
        return (
            CheckResult(False, "Entry point", 'no [project.entry-points."mld.plugins"] section'),
            None, None, None,
        )

    if len(eps) > 1:
        names = ", ".join(eps.keys())
        return (
            CheckResult(False, "Entry point", f"multiple entry points found: {names} (expected exactly one)"),
            None, None, None,
        )

    ep_name, ep_value = next(iter(eps.items()))

    # Validate format: "module.path:ClassName"
    if ":" not in ep_value:
        return (
            CheckResult(False, "Entry point", f'"{ep_value}" missing colon — expected "module.path:ClassName"'),
            None, None, None,
        )

    module_path, class_name = ep_value.rsplit(":", 1)
    return (
        CheckResult(True, "Entry point", f'{ep_name} = "{ep_value}"'),
        ep_name, module_path, class_name,
    )


def _check_source_module(project_dir: Path, module_path: str | None) -> tuple[CheckResult, Path | None]:
    """Check that the source module file exists."""
    if module_path is None:
        return CheckResult(False, "Source module", "skipped (no entry point)"), None

    # Convert module.path to file path: a.b.c -> src/a/b/c.py
    parts = module_path.split(".")
    candidates = [
        project_dir / "src" / "/".join(parts[:-1]) / f"{parts[-1]}.py",
        project_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py",
    ]

    for candidate in candidates:
        if candidate.exists():
            rel = candidate.relative_to(project_dir)
            return CheckResult(True, "Source module", str(rel)), candidate

    expected = " or ".join(str(c.relative_to(project_dir)) for c in candidates)
    return CheckResult(False, "Source module", f"not found at {expected}"), None


def _check_plugin_class(
    source_file: Path | None, class_name: str | None, source_content: str | None = None,
) -> tuple[CheckResult, str | None]:
    """Check that the plugin class exists. Returns (result, source_content) for reuse."""
    if source_file is None or class_name is None:
        return CheckResult(False, "Plugin class", "skipped (no source module)"), None

    if source_content is None:
        source_content = source_file.read_text()
    pattern = rf"class\s+{re.escape(class_name)}\s*\("
    if re.search(pattern, source_content):
        inherit_pattern = rf"class\s+{re.escape(class_name)}\s*\([^)]*AnalysisPlugin[^)]*\)"
        if re.search(inherit_pattern, source_content):
            return CheckResult(True, "Plugin class", f"{class_name}(AnalysisPlugin)"), source_content
        return CheckResult(True, "Plugin class", f"{class_name} (warning: does not inherit AnalysisPlugin)"), source_content

    return CheckResult(False, "Plugin class", f"class {class_name} not found in source"), source_content


def _check_routes_prefix(
    source_file: Path | None, ep_name: str | None, source_content: str | None = None,
) -> CheckResult:
    """Check that a routes prefix is defined or derivable."""
    prefix = None

    if source_file is not None:
        if source_content is None:
            source_content = source_file.read_text()
        match = re.search(r'routes_prefix\s*[=:]\s*["\'](/[^"\']+)["\']', source_content)
        if match:
            prefix = match.group(1)

    if prefix is None and ep_name is not None:
        prefix = f"/{ep_name}"

    if prefix:
        return CheckResult(True, "Routes prefix", prefix)
    return CheckResult(False, "Routes prefix", "could not determine routes prefix")


def _check_sdk_python(data: dict | None) -> CheckResult:
    """Check that mld-sdk is listed as a Python dependency."""
    if data is None:
        return CheckResult(False, "Python SDK dep", "skipped (no pyproject.toml)")

    deps = data.get("project", {}).get("dependencies", [])
    for dep in deps:
        if dep.strip().startswith("mld-sdk"):
            return CheckResult(True, "Python SDK dep", dep.strip())

    return CheckResult(False, "Python SDK dep", "mld-sdk not in [project.dependencies]")


def _check_sdk_frontend(project_dir: Path) -> CheckResult:
    """Check frontend SDK dependency if frontend exists."""
    pkg_json = project_dir / "frontend" / "package.json"
    if not pkg_json.exists():
        return CheckResult(True, "Frontend SDK dep", "no frontend (ok)")

    try:
        pkg = json.loads(pkg_json.read_text())
    except Exception as exc:
        return CheckResult(False, "Frontend SDK dep", f"package.json parse error: {exc}")

    deps = pkg.get("dependencies", {})
    version = deps.get("@morscherlab/mld-sdk")
    if version:
        return CheckResult(True, "Frontend SDK dep", f"@morscherlab/mld-sdk {version}")

    return CheckResult(False, "Frontend SDK dep", "@morscherlab/mld-sdk not in dependencies")


def _check_tests(project_dir: Path) -> CheckResult:
    """Check that a tests directory exists with test files."""
    tests_dir = project_dir / "tests"
    if not tests_dir.exists():
        return CheckResult(False, "Tests", "tests/ directory not found")

    test_files = list(tests_dir.glob("test_*.py"))
    if not test_files:
        return CheckResult(False, "Tests", "tests/ exists but no test_*.py files")

    return CheckResult(True, "Tests", f"{len(test_files)} test file(s)")


_AI_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules"]


def _check_ai_instructions(project_dir: Path) -> CheckResult:
    """Check that an AI instructions file exists."""
    for name in _AI_FILES:
        if (project_dir / name).exists():
            return CheckResult(True, "AI instructions", f"{name} found")
    return CheckResult(
        False,
        "AI instructions",
        "not found (run mld init to generate CLAUDE.md / AGENTS.md / .cursorrules)",
    )


def run_checks(project_dir: Path) -> list[CheckResult]:
    """Run all diagnostic checks and return results."""
    results: list[CheckResult] = []

    pyproject_result, data = _check_pyproject(project_dir)
    results.append(pyproject_result)

    ep_result, ep_name, module_path, class_name = _check_entry_point(data)
    results.append(ep_result)

    source_result, source_file = _check_source_module(project_dir, module_path)
    results.append(source_result)

    class_result, source_content = _check_plugin_class(source_file, class_name)
    results.append(class_result)
    results.append(_check_routes_prefix(source_file, ep_name, source_content))
    results.append(_check_sdk_python(data))
    results.append(_check_sdk_frontend(project_dir))
    results.append(_check_tests(project_dir))
    results.append(_check_ai_instructions(project_dir))

    return results


def cmd_doctor(args: argparse.Namespace) -> None:
    """Execute the ``mld doctor`` command."""
    project_dir = Path(getattr(args, "path", ".")).resolve()

    color = _supports_color()
    green = "\033[32m" if color else ""
    red = "\033[31m" if color else ""
    bold = "\033[1m" if color else ""
    reset = "\033[0m" if color else ""

    print(f"\n{bold}Checking plugin health...{reset}\n")

    results = run_checks(project_dir)

    passed = 0
    failed = 0
    for r in results:
        if r.ok:
            icon = f"{green}ok{reset}"
            passed += 1
        else:
            icon = f"{red}!!{reset}"
            failed += 1
        detail = f"  {r.detail}" if r.detail else ""
        print(f"  [{icon}]  {r.label}{detail}")

    total = passed + failed
    print()
    if failed == 0:
        print(f"{green}{passed}/{total} checks passed{reset}")
    else:
        print(f"{red}{failed}/{total} checks failed{reset}")
        sys.exit(1)
