#!/usr/bin/env python3
"""
skillgenie — CLI for the skill-genie environment (skills, rules, templates).

Usage:
  skillgenie list
  skillgenie read <skill>
  skillgenie status
  skillgenie install <skill>|--all [--claude|-cc] [--openclaw|-oc] [--hermes] [--opencode] [--global|-g] [--dry-run]
  skillgenie update  <skill>|--all [--claude|-cc] [--openclaw|-oc] [--hermes] [--opencode] [--global|-g] [--dry-run]

Targets:
  --claude / -cc     Install into Claude Code  (~/.claude/skills/)
  --openclaw / -oc   Install into OpenClaw     (~/.openclaw/skills/) via file copy
  --hermes           Install into Hermes       (~/.hermes/skills/) via file copy
  --opencode         Install into OpenCode     (~/.config/opencode/skills/)
  --kiro             Install into Kiro         (~/.kiro/skills/)
  --devin            Install into Devin CLI    (~/.config/devin/skills/)
  --global / -g      All supported targets
  (no flag)          Auto-detect: all runtimes found on this machine

Install backends:
  Claude Code, Codex, Cursor, Antigravity, Copilot, Windsurf, Kiro, OpenCode → symlink
  OpenClaw     →  shutil.copytree  →  ~/.openclaw/skills/<skill>/
  Hermes       →  shutil.copytree  →  ~/.hermes/skills/<skill>/
  Devin CLI    →  shutil.copytree  →  ~/.config/devin/skills/<skill>/
"""

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

# ── Config ────────────────────────────────────────────────────────────────────

REPO_ROOT     = Path(os.path.realpath(__file__)).parent / "skills"
AGENTS_DIR    = Path.home() / ".agents" / "skills"
CLAUDE_DIR    = Path.home() / ".claude" / "skills"
OPENCLAW_DIR  = Path.home() / ".openclaw" / "skills"
HERMES_DIR    = Path.home() / ".hermes" / "skills"
KIRO_SKILLS_DIR = Path.home() / ".kiro" / "skills"
CODEX_DIR     = Path.home() / ".codex" / "skills"
OPENCODE_DIR  = Path.home() / ".config" / "opencode" / "skills"
DEVIN_DIR     = Path.home() / ".config" / "devin" / "skills"
ANTIGRAVITY_DIR = Path.home() / ".gemini" / "antigravity" / "skills"
CURSOR_DIR    = Path.home() / ".cursor" / "skills"
COPILOT_DIR   = Path.home() / ".github" / "skills"
COPILOT_CLI_DIR = Path.home() / ".copilot" / "skills"
WINDSURF_DIR  = Path.home() / ".codeium" / "windsurf" / "skills"
WINDSURF_NEXT_DIR = Path.home() / ".codeium" / "windsurf-next" / "skills"

SKIP_DIRS = {".git", ".tmp-claude-code-security-review-ref", "__pycache__", "node_modules"}

TARGETS = {
    "agents": ("AgentSkills", AGENTS_DIR, "symlink"),
    "claude": ("Claude Code", CLAUDE_DIR, "symlink"),
    "openclaw": ("OpenClaw", OPENCLAW_DIR, "copy"),
    "hermes": ("Hermes", HERMES_DIR, "copy"),
    "kiro": ("Kiro", KIRO_SKILLS_DIR, "symlink"),
    "codex": ("Codex", CODEX_DIR, "symlink"),
    "opencode": ("OpenCode", OPENCODE_DIR, "symlink"),
    "devin": ("Devin (CLI + Desktop)", DEVIN_DIR, "copy"),
    "antigravity": ("Gemini Antigravity", ANTIGRAVITY_DIR, "symlink"),
    "cursor": ("Cursor", CURSOR_DIR, "symlink"),
    "copilot": ("GitHub Copilot", COPILOT_DIR, "symlink"),
    "copilot-cli": ("GitHub Copilot CLI/app", COPILOT_CLI_DIR, "symlink"),
    "windsurf": ("Windsurf", WINDSURF_DIR, "symlink"),
    "windsurf-next": ("Windsurf Next", WINDSURF_NEXT_DIR, "symlink"),
}

# ── Helpers ───────────────────────────────────────────────────────────────────

RESET  = "\033[0m"
BOLD   = "\033[1m"
GREEN  = "\033[32m"
YELLOW = "\033[33m"
RED    = "\033[31m"
CYAN   = "\033[36m"
DIM    = "\033[2m"

def ok(msg):   print(f"  {GREEN}✓{RESET} {msg}")
def warn(msg): print(f"  {YELLOW}⚠{RESET} {msg}")
def err(msg):  print(f"  {RED}✗{RESET} {msg}", file=sys.stderr)
def info(msg): print(f"  {CYAN}→{RESET} {msg}")
def dry(msg):  print(f"  {DIM}[dry-run]{RESET} {msg}")


def parse_frontmatter(skill_md: Path) -> dict:
    """Extract YAML frontmatter fields (name, description) without a YAML dep."""
    text = skill_md.read_text(errors="replace")
    m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
    if not m:
        return {}
    front = m.group(1)

    result = {}
    # name
    nm = re.search(r"^name:\s*(.+)", front, re.MULTILINE)
    if nm:
        result["name"] = nm.group(1).strip().strip('"').strip("'")

    # description — may be a plain string or a block scalar (|, >)
    dm = re.search(r"^description:\s*(.+)", front, re.MULTILINE)
    if dm:
        val = dm.group(1).strip()
        if val not in ("|", ">"):
            result["description"] = val.strip('"').strip("'")
        else:
            # Grab the indented block that follows
            block_m = re.search(
                r"^description:\s*[|>]\n((?:[ \t]+.+\n?)+)", front, re.MULTILINE
            )
            if block_m:
                lines = [l.strip() for l in block_m.group(1).splitlines()]
                result["description"] = " ".join(lines)
    return result


def all_skills() -> list[str]:
    """Return sorted list of skill folder names in this repo."""
    names = []
    for entry in sorted(REPO_ROOT.iterdir()):
        if entry.is_dir() and entry.name not in SKIP_DIRS and not entry.name.startswith("."):
            if (entry / "SKILL.md").exists():
                names.append(entry.name)
    return names


def detect_targets() -> list[str]:
    """Return targets present on this machine."""
    targets = ["agents"]
    checks = {
        "claude": lambda: shutil.which("claude") or CLAUDE_DIR.parent.exists(),
        "openclaw": lambda: shutil.which("openclaw") or OPENCLAW_DIR.parent.exists(),
        "hermes": lambda: shutil.which("hermes") or HERMES_DIR.parent.exists(),
        "kiro": lambda: shutil.which("kiro") or KIRO_SKILLS_DIR.parent.exists(),
        "codex": lambda: shutil.which("codex") or CODEX_DIR.parent.exists(),
        "opencode": lambda: shutil.which("opencode") or OPENCODE_DIR.parent.exists(),
        "devin": lambda: shutil.which("devin") or DEVIN_DIR.parent.exists() or (Path.home() / ".devin").exists() or (Path.home() / ".devin-next").exists(),
        "antigravity": lambda: shutil.which("antigravity") or ANTIGRAVITY_DIR.parent.exists(),
        "cursor": lambda: shutil.which("cursor") or CURSOR_DIR.parent.exists(),
        "copilot": lambda: shutil.which("gh") and COPILOT_DIR.parent.exists(),
        "copilot-cli": lambda: shutil.which("copilot") or COPILOT_CLI_DIR.parent.exists(),
        "windsurf": lambda: WINDSURF_DIR.parent.exists(),
        "windsurf-next": lambda: WINDSURF_NEXT_DIR.parent.exists(),
    }
    for name, present in checks.items():
        if present():
            targets.append(name)
    return targets


def resolve_targets(args) -> list[str]:
    flags = []
    if getattr(args, "global", False):
        flags = list(TARGETS)
    else:
        for target in TARGETS:
            if getattr(args, target.replace("-", "_"), False):
                flags.append(target)
    return flags if flags else detect_targets()

# ── Commands ──────────────────────────────────────────────────────────────────

def cmd_list(_args):
    skills = all_skills()
    print(f"\n{BOLD}Available skills{RESET} ({len(skills)} total)\n")
    for name in skills:
        skill_md = REPO_ROOT / name / "SKILL.md"
        meta = parse_frontmatter(skill_md)
        desc = meta.get("description", "")
        if len(desc) > 90:
            desc = desc[:87] + "..."
        print(f"  {CYAN}{name:<32}{RESET} {DIM}{desc}{RESET}")
    print()


def cmd_read(args):
    skill = args.skill
    skill_md = REPO_ROOT / skill / "SKILL.md"
    if not skill_md.exists():
        global_path = AGENTS_DIR / skill / "SKILL.md"
        if global_path.exists():
            skill_md = global_path
        else:
            err(f"Unknown skill: {skill}")
            sys.exit(1)
    print(f"Reading: {skill}")
    print(f"Base directory: {skill_md.parent}\n")
    print(skill_md.read_text(errors="replace"))
    print(f"\nSkill read: {skill}")


def cmd_status(_args):
    skills = all_skills()
    targets = [(label, path) for label, path, _ in TARGETS.values()]

    print(f"\n{BOLD}Skill status{RESET}\n")
    header = f"  {'Skill':<32}" + "".join(f"  {name:<14}" for name, _ in targets)
    print(header)
    print("  " + "-" * (32 + 16 * len(targets)))

    for skill in skills:
        row = f"  {skill:<32}"
        for _, base in targets:
            installed = (base / skill / "SKILL.md").exists()
            mark = f"{GREEN}✓ installed{RESET}" if installed else f"{DIM}–{RESET}"
            row += f"  {mark:<14}"
        print(row)
    print()


def validate_frontmatter(skill_md: Path) -> list[str]:
    text = skill_md.read_text(errors="replace")
    m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
    if not m:
        return ["missing YAML frontmatter"]
    front = m.group(1)
    errors = []
    data = parse_frontmatter(skill_md)
    if not data.get("name"):
        errors.append("missing frontmatter name")
    if not data.get("description"):
        errors.append("missing frontmatter description")
    name = data.get("name", "")
    if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name):
        errors.append("name must be lowercase alphanumeric with single hyphen separators")
    if name and name != skill_md.parent.name:
        errors.append("name must match the skill directory name")
    if len(name) > 64:
        errors.append("name exceeds 64 characters")
    if len(data.get("description", "")) > 1024:
        errors.append("description exceeds OpenCode/Kiro 1024 character limit")
    for lineno, line in enumerate(front.splitlines(), 1):
        if line.startswith(" ") or line.startswith("-"):
            errors.append(f"frontmatter line {lineno} is not single-line key syntax")
        if line.strip().endswith((": |", ": >")):
            errors.append(f"frontmatter line {lineno} uses a block scalar")
    meta_match = re.search(r"^metadata:\s*(.+)$", front, re.MULTILINE)
    if meta_match:
        try:
            metadata = json.loads(meta_match.group(1))
            if not isinstance(metadata, dict):
                errors.append("metadata is not a JSON object")
            if "version" not in metadata:
                errors.append("metadata.version is missing")
            if "hermes" not in metadata:
                errors.append("metadata.hermes is missing")
        except json.JSONDecodeError as exc:
            errors.append(f"metadata is not single-line JSON: {exc}")
    else:
        errors.append("metadata single-line JSON is missing")
    return errors


def cmd_validate(args):
    skills = all_skills() if args.all else [args.skill] if args.skill else all_skills()
    available = set(all_skills())
    failed = {}
    for skill in skills:
        if skill not in available:
            failed[skill] = ["unknown skill"]
            continue
        errors = validate_frontmatter(REPO_ROOT / skill / "SKILL.md")
        if errors:
            failed[skill] = errors
    if failed:
        print(f"\n{BOLD}Skill validation failed{RESET}\n")
        for skill, errors in failed.items():
            print(f"  {RED}{skill}{RESET}")
            for error in errors:
                print(f"    - {error}")
        print()
        sys.exit(1)
    ok(f"All checked skills are AgentSkills/OpenClaw/Hermes/OpenCode/Kiro/Devin compatible ({len(skills)})")


def remove_existing(path: Path):
    if path.is_symlink() or path.is_file():
        path.unlink()
    elif path.exists():
        shutil.rmtree(path)


def install_to_target(skill: str, target: str, is_dry: bool):
    src  = REPO_ROOT / skill
    label, base, mode = TARGETS[target]
    dest = base / skill
    arrow = "copy" if mode == "copy" else "symlink"
    info(f"{target:<13} ← {arrow} {src} → {dest}")
    if is_dry:
        return True
    try:
        remove_existing(dest)
        dest.parent.mkdir(parents=True, exist_ok=True)
        if mode == "copy":
            shutil.copytree(src, dest, symlinks=True)
        else:
            dest.symlink_to(src)
        return True
    except Exception as e:
        err(f"{label}: {e}")
        return False


def install_skill(skill: str, targets: list[str], is_dry: bool) -> bool:
    print(f"\n{BOLD}{skill}{RESET}")
    success = True
    for target in targets:
        r = install_to_target(skill, target, is_dry) if target in TARGETS else False
        if target not in TARGETS:
            warn(f"Unknown target: {target}")
        if r:
            ok(target)
        else:
            err(f"{target} failed")
            success = False
    return success


def cmd_install(args):
    targets  = resolve_targets(args)
    is_dry   = args.dry_run
    is_all   = args.all
    skills   = all_skills() if is_all else [args.skill] if args.skill else []

    if not skills:
        err("Specify a skill name or use --all")
        sys.exit(1)

    # Validate skill names
    available = set(all_skills())
    for s in skills:
        if s not in available:
            err(f"Unknown skill: {s}  (run 'skillgenie list' to see available skills)")
            sys.exit(1)

    verb = "update" if getattr(args, "_update", False) else "install"
    label = f"{DIM}[dry-run]{RESET} " if is_dry else ""
    print(f"\n{label}{BOLD}{verb.capitalize()}{RESET} → targets: {', '.join(targets)}")

    failed = []
    for skill in skills:
        if not install_skill(skill, targets, is_dry):
            failed.append(skill)

    print()
    if failed:
        err(f"Failed: {', '.join(failed)}")
        sys.exit(1)
    else:
        ok(f"Done ({len(skills)} skill{'s' if len(skills) != 1 else ''})")


def cmd_update(args):
    args._update = True
    cmd_install(args)

# ── CLI wiring ────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        prog="skillgenie",
        description="Skill manager for the skill-genie repo.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
examples:
  skillgenie list
  skillgenie validate
  skillgenie status
  skillgenie install research-to-wechat
  skillgenie install research-to-wechat --openclaw
  skillgenie install research-to-wechat --hermes
  skillgenie install research-to-wechat --opencode
  skillgenie install research-to-wechat --kiro
  skillgenie install --all --global
  skillgenie install --all --dry-run
  skillgenie update close-loop --claude
        """,
    )

    sub = parser.add_subparsers(dest="command", required=True)

    # list
    sub.add_parser("list", help="List available skills in this repo")

    # read
    p_read = sub.add_parser("read", help="Read a skill's SKILL.md content")
    p_read.add_argument("skill", help="Skill name")

    # status
    sub.add_parser("status", help="Show install status per runtime")

    # validate
    p_validate = sub.add_parser("validate", help="Validate cross-agent skill compatibility")
    p_validate.add_argument("skill", nargs="?", help="Skill name (omit for all)")
    p_validate.add_argument("--all", action="store_true", help="All skills")

    # shared flags for install / update
    def add_target_flags(p):
        p.add_argument("skill", nargs="?", help="Skill name (omit with --all)")
        p.add_argument("--all",      action="store_true", help="All skills")
        g = p.add_mutually_exclusive_group()
        g.add_argument("--global", "-g",   action="store_true", dest="global",   help="All supported runtimes")
        g.add_argument("--agents",          action="store_true", dest="agents",   help="AgentSkills root (~/.agents/skills/)")
        g.add_argument("--claude", "-cc",  action="store_true", dest="claude",   help="Claude Code only (~/.claude/skills/)")
        g.add_argument("--openclaw", "-oc",action="store_true", dest="openclaw", help="OpenClaw only (~/.openclaw/skills/)")
        g.add_argument("--hermes",          action="store_true", dest="hermes",   help="Hermes only (~/.hermes/skills/)")
        g.add_argument("--kiro",            action="store_true", dest="kiro",     help="Kiro only (~/.kiro/skills/)")
        g.add_argument("--codex",           action="store_true", dest="codex",    help="Codex only (~/.codex/skills/)")
        g.add_argument("--opencode",        action="store_true", dest="opencode", help="OpenCode only (~/.config/opencode/skills/)")
        g.add_argument("--devin",           action="store_true", dest="devin",    help="Devin CLI + Desktop, formerly Windsurf (~/.config/devin/skills/)")
        g.add_argument("--antigravity",     action="store_true", dest="antigravity", help="Gemini Antigravity only (~/.gemini/antigravity/skills/)")
        g.add_argument("--cursor",          action="store_true", dest="cursor",   help="Cursor only (~/.cursor/skills/)")
        g.add_argument("--copilot",         action="store_true", dest="copilot",  help="GitHub Copilot only (~/.github/skills/)")
        g.add_argument("--copilot-cli",     action="store_true", dest="copilot_cli", help="GitHub Copilot CLI/app only (~/.copilot/skills/)")
        g.add_argument("--windsurf",        action="store_true", dest="windsurf", help="Windsurf only (~/.codeium/windsurf/skills/)")
        g.add_argument("--windsurf-next",   action="store_true", dest="windsurf_next", help="Windsurf Next only (~/.codeium/windsurf-next/skills/)")
        p.add_argument("--dry-run", action="store_true", help="Show actions without executing")

    p_install = sub.add_parser("install", help="Install skill(s)")
    add_target_flags(p_install)

    p_update = sub.add_parser("update", help="Update (re-install) skill(s)")
    add_target_flags(p_update)

    args = parser.parse_args()

    dispatch = {
        "list":    cmd_list,
        "read":    cmd_read,
        "validate": cmd_validate,
        "status":  cmd_status,
        "install": cmd_install,
        "update":  cmd_update,
    }
    dispatch[args.command](args)


if __name__ == "__main__":
    main()
