#!/usr/bin/env python3
"""
dependency_checker.py — Cross-Wizard Dependency Validator

Each skill may declare binary or environment-variable dependencies in its
SKILL.md frontmatter under a `dependencies` key:

    ---
    name: my-skill
    dependencies:
      binaries: [ffmpeg, imagemagick]
      env_vars: [MY_API_KEY, MY_SECRET]
    ---

This script scans all installed skills, extracts declared dependencies, and
checks whether each is satisfied in the current environment.

Usage:
    python wizard-bootstrap/dependency_checker.py
    python wizard-bootstrap/dependency_checker.py --json
    python wizard-bootstrap/dependency_checker.py --skill software-development/code-review
"""

import argparse
import json
import os
import shutil
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

try:
    import yaml
    HAS_YAML = True
except ImportError:
    HAS_YAML = False


# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class SkillDep:
    skill_path: str
    skill_name: str
    binary: Optional[str] = None
    env_var: Optional[str] = None
    satisfied: bool = False
    detail: str = ""


@dataclass
class DepReport:
    deps: list[SkillDep] = field(default_factory=list)

    @property
    def all_satisfied(self) -> bool:
        return all(d.satisfied for d in self.deps)

    @property
    def unsatisfied(self) -> list[SkillDep]:
        return [d for d in self.deps if not d.satisfied]


# ---------------------------------------------------------------------------
# Frontmatter parser
# ---------------------------------------------------------------------------

def _parse_frontmatter(text: str) -> dict:
    """Extract YAML frontmatter from a SKILL.md file."""
    if not text.startswith("---"):
        return {}
    end = text.find("\n---", 3)
    if end == -1:
        return {}
    fm_text = text[3:end].strip()
    if not HAS_YAML:
        return {}
    try:
        return yaml.safe_load(fm_text) or {}
    except Exception:
        return {}


def _load_skill_deps(skill_md: Path) -> tuple[str, list[str], list[str]]:
    """
    Returns (skill_name, binaries, env_vars) from a SKILL.md frontmatter.
    """
    text = skill_md.read_text(encoding="utf-8", errors="replace")
    fm = _parse_frontmatter(text)
    skill_name = fm.get("name", skill_md.parent.name)
    deps = fm.get("dependencies", {})
    if not isinstance(deps, dict):
        return skill_name, [], []
    binaries = deps.get("binaries") or []
    env_vars = deps.get("env_vars") or []
    if isinstance(binaries, str):
        binaries = [binaries]
    if isinstance(env_vars, str):
        env_vars = [env_vars]
    return skill_name, list(binaries), list(env_vars)


# ---------------------------------------------------------------------------
# Checks
# ---------------------------------------------------------------------------

def _check_binary(binary: str) -> tuple[bool, str]:
    path = shutil.which(binary)
    if path:
        return True, f"found at {path}"
    return False, f"not found in PATH"


def _check_env_var(var: str) -> tuple[bool, str]:
    val = os.environ.get(var)
    if val:
        return True, "set"
    return False, "not set"


# ---------------------------------------------------------------------------
# Scanner
# ---------------------------------------------------------------------------

def _find_skills_dir() -> Optional[Path]:
    """Resolve skills directory: prefer repo root, fall back to HERMES_HOME."""
    # Check if we're inside the repo
    repo_root = Path(__file__).parent.parent
    repo_skills = repo_root / "skills"
    if repo_skills.exists():
        return repo_skills

    hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
    for candidate in [hermes_home / "skills", hermes_home / "hermes-agent" / "skills"]:
        if candidate.exists():
            return candidate
    return None


def run_dep_check(skills_dir: Optional[Path] = None, skill_filter: Optional[str] = None) -> DepReport:
    resolved = skills_dir or _find_skills_dir()
    report = DepReport()

    if resolved is None or not resolved.exists():
        return report

    # Load ~/.hermes/.env so env var checks work
    hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
    env_path = hermes_home / ".env"
    if env_path.exists():
        try:
            from dotenv import load_dotenv  # noqa: PLC0415
            load_dotenv(env_path, override=False)
        except Exception:
            pass

    for skill_md in sorted(resolved.rglob("SKILL.md")):
        rel = str(skill_md.parent.relative_to(resolved))
        if skill_filter and skill_filter not in rel:
            continue

        skill_name, binaries, env_vars = _load_skill_deps(skill_md)

        for binary in binaries:
            ok, detail = _check_binary(binary)
            report.deps.append(SkillDep(
                skill_path=rel,
                skill_name=skill_name,
                binary=binary,
                satisfied=ok,
                detail=detail,
            ))

        for var in env_vars:
            ok, detail = _check_env_var(var)
            report.deps.append(SkillDep(
                skill_path=rel,
                skill_name=skill_name,
                env_var=var,
                satisfied=ok,
                detail=detail,
            ))

    return report


# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------

_GREEN = "\033[32m"
_RED = "\033[31m"
_YELLOW = "\033[33m"
_BOLD = "\033[1m"
_RESET = "\033[0m"


def _render_terminal(report: DepReport) -> None:
    print(f"\n{_BOLD}=== Cross-Wizard Dependency Check ==={_RESET}\n")

    if not report.deps:
        print("No skill dependencies declared. Skills use implicit deps only.\n")
        print(
            f"{_YELLOW}Tip:{_RESET} Declare binary/env_var deps in SKILL.md frontmatter "
            "under a 'dependencies' key to make them checkable.\n"
        )
        return

    for dep in report.deps:
        icon = f"{_GREEN}✓{_RESET}" if dep.satisfied else f"{_RED}✗{_RESET}"
        if dep.binary:
            dep_type = "binary"
            dep_name = dep.binary
        else:
            dep_type = "env_var"
            dep_name = dep.env_var

        print(f"  {icon}  [{dep.skill_path}] {dep_type}:{dep_name}  — {dep.detail}")

    total = len(report.deps)
    satisfied = sum(1 for d in report.deps if d.satisfied)
    print()
    if report.all_satisfied:
        print(f"{_GREEN}{_BOLD}All {total} dependencies satisfied.{_RESET}\n")
    else:
        failed = total - satisfied
        print(
            f"{_RED}{_BOLD}{failed}/{total} dependencies unsatisfied.{_RESET} "
            "Install missing binaries and set missing env vars.\n"
        )


def _render_json(report: DepReport) -> None:
    out = {
        "all_satisfied": report.all_satisfied,
        "summary": {
            "total": len(report.deps),
            "satisfied": sum(1 for d in report.deps if d.satisfied),
            "unsatisfied": len(report.unsatisfied),
        },
        "deps": [
            {
                "skill_path": d.skill_path,
                "skill_name": d.skill_name,
                "type": "binary" if d.binary else "env_var",
                "name": d.binary or d.env_var,
                "satisfied": d.satisfied,
                "detail": d.detail,
            }
            for d in report.deps
        ],
    }
    print(json.dumps(out, indent=2))


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------

def main() -> None:
    if not HAS_YAML:
        print("WARNING: pyyaml not installed — cannot parse SKILL.md frontmatter. "
              "Dependency declarations will be skipped.", file=sys.stderr)

    parser = argparse.ArgumentParser(
        description="Check cross-wizard skill dependencies (binaries, env vars)."
    )
    parser.add_argument(
        "--skills-dir",
        default=None,
        help="Skills directory to scan (default: auto-detect)",
    )
    parser.add_argument(
        "--skill",
        default=None,
        help="Filter to a specific skill path substring",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output results as JSON",
    )
    args = parser.parse_args()

    skills_dir = Path(args.skills_dir).resolve() if args.skills_dir else None
    report = run_dep_check(skills_dir=skills_dir, skill_filter=args.skill)

    if args.json:
        _render_json(report)
    else:
        _render_terminal(report)

    sys.exit(0 if report.all_satisfied else 1)


if __name__ == "__main__":
    main()
