# Implements: REQ-p00005-E
"""
elspais.commands.doctor - Diagnose environment and installation health.

Checks the elspais setup on this machine:
- Configuration file exists and is valid
- Required settings are present
- Spec directories exist
- Worktree detection and canonical root
- Associate path resolution
- Local config overrides
"""
from __future__ import annotations

import argparse
from pathlib import Path
from typing import TYPE_CHECKING

from elspais.commands.health import HealthCheck, HealthReport

if TYPE_CHECKING:
    from elspais.config import ConfigLoader


# =============================================================================
# Config Checks (moved from health.py, messages rewritten for lay-persons)
# =============================================================================


def check_config_exists(config_path: Path | None, start_path: Path) -> HealthCheck:
    """Check if config file exists and is accessible."""
    from elspais.config import find_config_file

    if config_path and config_path.exists():
        return HealthCheck(
            name="config.exists",
            passed=True,
            message=f"Configuration file found at {config_path}",
            category="config",
            details={"path": str(config_path)},
        )

    found = find_config_file(start_path)
    if found:
        return HealthCheck(
            name="config.exists",
            passed=True,
            message=f"Configuration file found at {found}",
            category="config",
            details={"path": str(found)},
        )

    return HealthCheck(
        name="config.exists",
        passed=True,
        message="No configuration file found (using defaults). Run 'elspais init' to create one.",
        category="config",
        severity="info",
    )


def check_config_syntax(config_path: Path | None, start_path: Path) -> HealthCheck:
    """Check if config file has valid TOML syntax."""
    from elspais.config import find_config_file

    actual_path = (
        config_path if config_path and config_path.exists() else find_config_file(start_path)
    )

    if not actual_path:
        return HealthCheck(
            name="config.syntax",
            passed=True,
            message="No configuration file to check (using defaults)",
            category="config",
            severity="info",
        )

    try:
        content = actual_path.read_text(encoding="utf-8")
        from elspais.config import parse_toml

        parse_toml(content)
        return HealthCheck(
            name="config.syntax",
            passed=True,
            message="Configuration file syntax is valid",
            category="config",
        )
    except Exception as e:
        return HealthCheck(
            name="config.syntax",
            passed=False,
            message=f"Configuration file has a formatting error: {e}",
            category="config",
            details={"error": str(e), "path": str(actual_path)},
        )


def check_config_required_fields(config: ConfigLoader) -> HealthCheck:
    """Check that required configuration sections exist."""
    raw = config.get_raw()
    missing = []

    patterns = raw.get("patterns", {})
    if not patterns.get("types"):
        missing.append("patterns.types (requirement type definitions)")

    spec = raw.get("spec", {})
    if not spec.get("directories"):
        missing.append("spec.directories (where to find spec files)")

    rules = raw.get("rules", {})
    if not rules.get("hierarchy"):
        missing.append("rules.hierarchy (requirement hierarchy rules)")

    if missing:
        return HealthCheck(
            name="config.required_fields",
            passed=False,
            message=f"Configuration is missing required settings: {', '.join(missing)}",
            category="config",
            severity="warning",
            details={"missing": missing},
        )

    return HealthCheck(
        name="config.required_fields",
        passed=True,
        message="All required configuration settings are present",
        category="config",
    )


def check_config_pattern_tokens(config: ConfigLoader) -> HealthCheck:
    """Validate that the ID pattern template uses valid placeholders."""
    import re

    template = config.get("patterns.id_template", "")
    valid_tokens = {"{prefix}", "{type}", "{id}", "{associated}"}

    found_tokens = set(re.findall(r"\{[^}]+\}", template))

    invalid = found_tokens - valid_tokens
    if invalid:
        return HealthCheck(
            name="config.pattern_tokens",
            passed=False,
            message=(
                f"ID pattern has unrecognized placeholders: {', '.join(invalid)}. "
                f"Valid ones are: {', '.join(sorted(valid_tokens))}"
            ),
            category="config",
            details={"invalid_tokens": list(invalid), "valid_tokens": list(valid_tokens)},
        )

    required = {"{prefix}", "{id}"}
    missing = required - found_tokens
    if missing:
        return HealthCheck(
            name="config.pattern_tokens",
            passed=False,
            message=f"ID pattern is missing required placeholders: {', '.join(missing)}",
            category="config",
            severity="warning",
            details={"missing": list(missing)},
        )

    return HealthCheck(
        name="config.pattern_tokens",
        passed=True,
        message=f"ID pattern is valid: {template}",
        category="config",
    )


def check_config_hierarchy_rules(config: ConfigLoader) -> HealthCheck:
    """Validate hierarchy rules are consistent."""
    hierarchy = config.get("rules.hierarchy", {})
    types = config.get("patterns.types", {})

    if not isinstance(hierarchy, dict):
        return HealthCheck(
            name="config.hierarchy_rules",
            passed=False,
            message=f"Hierarchy rules should be a table, but found {type(hierarchy).__name__}",
            category="config",
            severity="warning",
        )

    if not isinstance(types, dict):
        return HealthCheck(
            name="config.hierarchy_rules",
            passed=False,
            message=f"Requirement types should be a table, but found {type(types).__name__}",
            category="config",
            severity="warning",
        )

    issues = []
    non_level_keys = {"allowed_implements", "allow_circular", "allow_orphans", "allowed"}

    for level, allowed_parents in hierarchy.items():
        if level in non_level_keys:
            continue
        if level not in types:
            issues.append(f"Rule references unknown level '{level}'")
            continue
        if not isinstance(allowed_parents, list):
            issues.append(
                f"Rule for '{level}' should be a list, found {type(allowed_parents).__name__}"
            )
            continue
        for parent in allowed_parents:
            if parent not in types:
                issues.append(f"Level '{level}' references unknown parent level '{parent}'")

    if issues:
        return HealthCheck(
            name="config.hierarchy_rules",
            passed=False,
            message=f"Hierarchy rule issues: {'; '.join(issues)}",
            category="config",
            severity="warning",
            details={"issues": issues},
        )

    return HealthCheck(
        name="config.hierarchy_rules",
        passed=True,
        message=f"Hierarchy rules are valid ({len(hierarchy)} levels configured)",
        category="config",
    )


def check_config_paths_exist(config: ConfigLoader, start_path: Path) -> HealthCheck:
    """Check that configured spec directories exist on disk."""
    spec_dirs = config.get("spec.directories", ["spec"])

    if not isinstance(spec_dirs, list):
        return HealthCheck(
            name="config.paths_exist",
            passed=False,
            message=f"Spec directories setting should be a list, found {type(spec_dirs).__name__}",
            category="config",
            severity="warning",
        )

    missing = []
    found = []

    for spec_dir in spec_dirs:
        full_path = start_path / spec_dir
        if full_path.exists():
            found.append(str(spec_dir))
        else:
            missing.append(str(spec_dir))

    if missing:
        return HealthCheck(
            name="config.paths_exist",
            passed=False,
            message=f"Spec directories not found on disk: {', '.join(missing)}",
            category="config",
            details={"missing": missing, "found": found},
        )

    return HealthCheck(
        name="config.paths_exist",
        passed=True,
        message=f"All {len(found)} spec directories exist",
        category="config",
        details={"directories": found},
    )


def check_config_project_type(config: ConfigLoader) -> HealthCheck:
    """Check project type configuration is consistent."""
    from elspais.config import validate_project_config

    raw = config.get_raw()
    errors = validate_project_config(raw)

    if errors:
        return HealthCheck(
            name="config.project_type",
            passed=False,
            message=errors[0],
            category="config",
            severity="warning",
            details={"errors": errors},
        )

    project_type = raw.get("project", {}).get("type")
    if project_type:
        return HealthCheck(
            name="config.project_type",
            passed=True,
            message=f"Project type '{project_type}' is properly configured",
            category="config",
            details={"type": project_type},
        )

    return HealthCheck(
        name="config.project_type",
        passed=True,
        message="Project type not set (using defaults)",
        category="config",
        severity="info",
    )


def run_config_checks(
    config_path: Path | None, config: ConfigLoader, start_path: Path
) -> list[HealthCheck]:
    """Run all configuration checks."""
    return [
        check_config_exists(config_path, start_path),
        check_config_syntax(config_path, start_path),
        check_config_required_fields(config),
        check_config_project_type(config),
        check_config_pattern_tokens(config),
        check_config_hierarchy_rules(config),
        check_config_paths_exist(config, start_path),
    ]


# =============================================================================
# Environment Checks
# =============================================================================


def check_worktree_status(git_root: Path | None, canonical_root: Path | None) -> HealthCheck:
    """Detect if running in a git worktree."""
    if git_root is None:
        return HealthCheck(
            name="worktree.status",
            passed=True,
            message="Not in a git repository",
            category="environment",
            severity="info",
        )

    if canonical_root and canonical_root != git_root:
        return HealthCheck(
            name="worktree.status",
            passed=True,
            message=f"Running in a git worktree. Main repository: {canonical_root}",
            category="environment",
            severity="info",
            details={"git_root": str(git_root), "canonical_root": str(canonical_root)},
        )

    return HealthCheck(
        name="worktree.status",
        passed=True,
        message="Running in the main repository (not a worktree)",
        category="environment",
        severity="info",
    )


def check_associate_paths(config: dict, canonical_root: Path | None) -> HealthCheck:
    """Check that each configured associate path exists on disk."""
    associate_paths = config.get("associates", {}).get("paths", [])

    if not associate_paths:
        return HealthCheck(
            name="associate.paths_resolvable",
            passed=True,
            message="No associated projects configured",
            category="environment",
            severity="info",
        )

    missing = []
    found = []
    for path_str in associate_paths:
        p = Path(path_str)
        if not p.is_absolute() and canonical_root:
            p = canonical_root / p
        if p.exists():
            found.append(str(path_str))
        else:
            missing.append(f"{path_str} (expected at {p})")

    if missing:
        return HealthCheck(
            name="associate.paths_resolvable",
            passed=False,
            message=f"Associated project paths not found: {'; '.join(missing)}",
            category="environment",
            severity="warning",
            details={"missing": missing, "found": found},
        )

    return HealthCheck(
        name="associate.paths_resolvable",
        passed=True,
        message=f"All {len(found)} associated project paths exist",
        category="environment",
        details={"found": found},
    )


def check_associate_configs(config: dict, canonical_root: Path | None) -> HealthCheck:
    """Check that each discovered associate has valid configuration."""
    from elspais.associates import discover_associate_from_path

    associate_paths = config.get("associates", {}).get("paths", [])

    if not associate_paths:
        return HealthCheck(
            name="associate.configs_valid",
            passed=True,
            message="No associated projects to check",
            category="environment",
            severity="info",
        )

    invalid = []
    valid = []
    for path_str in associate_paths:
        p = Path(path_str)
        if not p.is_absolute() and canonical_root:
            p = canonical_root / p
        if not p.exists():
            continue  # Already reported by check_associate_paths
        result = discover_associate_from_path(p)
        if isinstance(result, str):
            invalid.append(f"{path_str}: {result}")
        else:
            valid.append(f"{path_str} ({result.code})")

    if invalid:
        return HealthCheck(
            name="associate.configs_valid",
            passed=False,
            message=f"Associated project configuration issues: {'; '.join(invalid)}",
            category="environment",
            severity="warning",
            details={"invalid": invalid, "valid": valid},
        )

    return HealthCheck(
        name="associate.configs_valid",
        passed=True,
        message=f"All {len(valid)} associated projects have valid configuration",
        category="environment",
        details={"valid": valid},
    )


def check_local_toml_exists(start_path: Path) -> HealthCheck:
    """Check if local config override file exists."""
    local_path = start_path / ".elspais.local.toml"

    if local_path.exists():
        return HealthCheck(
            name="local_toml.exists",
            passed=True,
            message="Local configuration file found",
            category="environment",
            details={"path": str(local_path)},
        )

    return HealthCheck(
        name="local_toml.exists",
        passed=True,
        message=(
            "No .elspais.local.toml found. "
            "Create one for developer-specific settings (like associate paths)."
        ),
        category="environment",
        severity="info",
    )


def check_cross_repo_in_committed_config(config_path: Path | None) -> HealthCheck:
    """Warn if cross-repo paths are in the committed config file."""
    if not config_path or not config_path.exists():
        return HealthCheck(
            name="cross_repo.in_committed",
            passed=True,
            message="No committed configuration file to check",
            category="environment",
            severity="info",
        )

    try:
        content = config_path.read_text(encoding="utf-8")
        from elspais.config import parse_toml

        data = parse_toml(content)
    except Exception:
        return HealthCheck(
            name="cross_repo.in_committed",
            passed=True,
            message="Could not parse configuration file (reported by config.syntax check)",
            category="environment",
            severity="info",
        )

    cross_repo_paths = []
    spec_dirs = data.get("spec", {}).get("directories", [])
    if isinstance(spec_dirs, list):
        for d in spec_dirs:
            if ".." in str(d):
                cross_repo_paths.append(f"spec.directories: {d}")

    assoc_paths = data.get("associates", {}).get("paths", [])
    if isinstance(assoc_paths, list):
        for p in assoc_paths:
            if ".." in str(p):
                cross_repo_paths.append(f"associates.paths: {p}")

    if cross_repo_paths:
        return HealthCheck(
            name="cross_repo.in_committed",
            passed=False,
            message=(
                f"Cross-project paths found in shared config ({', '.join(cross_repo_paths)}). "
                "Move these to .elspais.local.toml so they don't affect other developers."
            ),
            category="environment",
            severity="warning",
            details={"paths": cross_repo_paths},
        )

    return HealthCheck(
        name="cross_repo.in_committed",
        passed=True,
        message="No cross-project paths in shared configuration",
        category="environment",
    )


def run_environment_checks(
    config: dict,
    git_root: Path | None,
    canonical_root: Path | None,
    config_path: Path | None,
    start_path: Path,
) -> list[HealthCheck]:
    """Run all environment checks."""
    return [
        check_worktree_status(git_root, canonical_root),
        check_associate_paths(config, canonical_root),
        check_associate_configs(config, canonical_root),
        check_local_toml_exists(start_path),
        check_cross_repo_in_committed_config(config_path),
    ]


def _print_text_report(report: HealthReport, verbose: bool = False) -> None:
    """Print human-readable doctor report."""
    categories = ["config", "environment"]

    for category in categories:
        checks = list(report.iter_by_category(category))
        if not checks:
            continue

        passed = sum(1 for c in checks if c.passed)
        total = len(checks)
        status = "OK" if passed == total else "ISSUES FOUND"
        label = category.upper()
        print(f"\n{label} ({passed}/{total} checks passed) - {status}")
        print("-" * 50)

        for check in checks:
            if check.passed:
                icon = "ok"
            elif check.severity == "warning":
                icon = "!!"
            elif check.severity == "info":
                icon = "--"
            else:
                icon = "XX"

            print(f"  [{icon}] {check.message}")

            if verbose and check.details:
                for key, value in check.details.items():
                    if isinstance(value, list) and len(value) > 3:
                        print(f"        {key}: {value[:3]} ... ({len(value)} total)")
                    else:
                        print(f"        {key}: {value}")

    print()
    total = len(report.checks)
    failed = report.failed
    warnings = report.warnings
    if failed == 0 and warnings == 0:
        print(f"All {total} checks passed. Your elspais installation looks good.")
    elif failed == 0:
        print(f"{total - warnings} checks passed, {warnings} warnings. No critical issues.")
    else:
        print(f"{failed} issues found out of {total} checks. See above for details.")


def run(args: argparse.Namespace) -> int:
    """Run the doctor command."""
    from elspais.config import ConfigLoader, find_config_file, find_git_root, get_config

    config_path = getattr(args, "config", None)
    if config_path:
        config_path = Path(config_path)
    canonical_root = getattr(args, "canonical_root", None)
    start_path = Path.cwd()
    git_root = find_git_root(start_path)

    report = HealthReport()

    # Load config for checks
    config = None
    config_dict = {}
    try:
        config_dict = get_config(config_path, start_path=start_path)
        config = ConfigLoader.from_dict(config_dict)
        for check in run_config_checks(config_path, config, start_path):
            report.add(check)
    except Exception as e:
        report.add(
            HealthCheck(
                name="config.load",
                passed=False,
                message=f"Could not load configuration: {e}",
                category="config",
            )
        )

    # Find the actual config file path for cross-repo check
    actual_config_path = config_path
    if not actual_config_path:
        actual_config_path = find_config_file(start_path)

    # Environment checks
    for check in run_environment_checks(
        config_dict, git_root, canonical_root, actual_config_path, start_path
    ):
        report.add(check)

    # Output
    if getattr(args, "json", False):
        import json

        print(json.dumps(report.to_dict(), indent=2))
    else:
        _print_text_report(report, verbose=getattr(args, "verbose", False))

    return 0 if report.is_healthy else 1
