#!/usr/bin/env python3
"""Validate all Claude Code plugins conform to specs."""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path

import yaml


def parse_frontmatter(content: str) -> tuple[dict | None, str]:
    """Parse YAML frontmatter from markdown content."""
    if not content.startswith("---"):
        return None, content
    parts = content.split("---", 2)
    if len(parts) < 3:
        return None, content
    try:
        frontmatter = yaml.safe_load(parts[1])
        return frontmatter, parts[2].strip()
    except yaml.YAMLError:
        return None, content


def validate_plugin_json(plugin_dir: Path) -> list[str]:
    """Validate .claude-plugin/plugin.json exists and is valid."""
    errors = []
    plugin_json = plugin_dir / ".claude-plugin" / "plugin.json"

    if not plugin_json.exists():
        errors.append(f"{plugin_dir.name}: Missing .claude-plugin/plugin.json")
        return errors

    try:
        with open(plugin_json) as f:
            config = json.load(f)

        if "name" not in config:
            errors.append(f"{plugin_dir.name}: plugin.json missing 'name' field")
        elif config["name"] != plugin_dir.name:
            errors.append(f"{plugin_dir.name}: plugin.json name '{config['name']}' doesn't match directory name")
    except json.JSONDecodeError as e:
        errors.append(f"{plugin_dir.name}: Invalid plugin.json - {e}")

    return errors


def validate_skills(plugin_dir: Path) -> list[str]:
    """Validate skills conform to Claude Code specs."""
    errors = []
    skills_dir = plugin_dir / "skills"
    if not skills_dir.exists():
        return errors

    for skill_path in skills_dir.iterdir():
        if not skill_path.is_dir():
            continue

        prefix = f"{plugin_dir.name}/skills/{skill_path.name}"

        # Check directory name is kebab-case
        if not re.match(r"^[a-z0-9-]+$", skill_path.name):
            errors.append(f"{prefix}: Directory must be kebab-case")

        skill_md = skill_path / "SKILL.md"
        if not skill_md.exists():
            errors.append(f"{prefix}: Missing SKILL.md")
            continue

        content = skill_md.read_text()
        frontmatter, body = parse_frontmatter(content)

        if not frontmatter:
            errors.append(f"{prefix}/SKILL.md: Missing YAML frontmatter")
            continue

        # Validate name field
        if "name" not in frontmatter:
            errors.append(f"{prefix}/SKILL.md: Missing 'name' field")
        else:
            name = frontmatter["name"]
            if not isinstance(name, str):
                errors.append(f"{prefix}/SKILL.md: 'name' must be string")
            elif len(name) > 64:
                errors.append(f"{prefix}/SKILL.md: 'name' exceeds 64 chars ({len(name)})")
            elif not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name):
                errors.append(f"{prefix}/SKILL.md: 'name' must be kebab-case: '{name}'")

        # Validate description field
        if "description" not in frontmatter:
            errors.append(f"{prefix}/SKILL.md: Missing 'description' field")
        else:
            desc = frontmatter["description"]
            if not isinstance(desc, str):
                errors.append(f"{prefix}/SKILL.md: 'description' must be string")
            elif len(desc) > 600:
                errors.append(f"{prefix}/SKILL.md: 'description' exceeds 600 chars ({len(desc)})")

        # Check body exists
        if not body or len(body.strip()) < 20:
            errors.append(f"{prefix}/SKILL.md: Body content too short")

    return errors


def validate_agents(plugin_dir: Path) -> list[str]:
    """Validate agents conform to Claude Code specs."""
    errors = []
    agents_dir = plugin_dir / "agents"
    if not agents_dir.exists():
        return errors

    valid_models = {"inherit", "sonnet", "opus", "haiku"}
    valid_colors = {"blue", "cyan", "green", "yellow", "magenta", "red"}

    for agent_file in agents_dir.iterdir():
        if not agent_file.is_file() or agent_file.suffix != ".md":
            continue

        prefix = f"{plugin_dir.name}/agents/{agent_file.name}"
        name = agent_file.stem

        # Check filename is kebab-case
        if not re.match(r"^[a-z0-9-]+$", name):
            errors.append(f"{prefix}: Filename must be kebab-case")

        content = agent_file.read_text()
        frontmatter, body = parse_frontmatter(content)

        if not frontmatter:
            errors.append(f"{prefix}: Missing YAML frontmatter")
            continue

        # Validate name field
        if "name" not in frontmatter:
            errors.append(f"{prefix}: Missing 'name' field")
        else:
            agent_name = frontmatter["name"]
            if not isinstance(agent_name, str):
                errors.append(f"{prefix}: 'name' must be string")
            elif len(agent_name) < 3 or len(agent_name) > 50:
                errors.append(f"{prefix}: 'name' must be 3-50 chars ({len(agent_name)})")
            elif not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$", agent_name):
                errors.append(
                    f"{prefix}: 'name' must be lowercase with hyphens, start/end alphanumeric: '{agent_name}'"
                )

        # Validate description field
        if "description" not in frontmatter:
            errors.append(f"{prefix}: Missing 'description' field")
        else:
            desc = frontmatter["description"]
            if not isinstance(desc, str):
                errors.append(f"{prefix}: 'description' must be string")
            elif len(desc) < 10 or len(desc) > 5000:
                errors.append(f"{prefix}: 'description' must be 10-5000 chars ({len(desc)})")

        # Validate model field
        if "model" not in frontmatter:
            errors.append(f"{prefix}: Missing 'model' field")
        elif frontmatter["model"] not in valid_models:
            errors.append(f"{prefix}: 'model' must be one of {valid_models}: '{frontmatter['model']}'")

        # Validate color field
        if "color" not in frontmatter:
            errors.append(f"{prefix}: Missing 'color' field")
        elif frontmatter["color"] not in valid_colors:
            errors.append(f"{prefix}: 'color' must be one of {valid_colors}: '{frontmatter['color']}'")

        # Validate tools field if present
        if "tools" in frontmatter:
            tools = frontmatter["tools"]
            if not isinstance(tools, list):
                errors.append(f"{prefix}: 'tools' must be array")

        # Check body exists
        if not body or len(body.strip()) < 20:
            errors.append(f"{prefix}: System prompt too short (<20 chars)")
        elif len(body.strip()) > 10000:
            errors.append(f"{prefix}: System prompt too long (>10000 chars)")

    return errors


def validate_commands(plugin_dir: Path) -> list[str]:
    """Validate commands conform to Claude Code specs."""
    errors = []
    commands_dir = plugin_dir / "commands"
    if not commands_dir.exists():
        return errors

    valid_models = {"sonnet", "opus", "haiku"}

    for cmd_file in commands_dir.rglob("*.md"):
        prefix = f"{plugin_dir.name}/commands/{cmd_file.relative_to(commands_dir)}"
        name = cmd_file.stem

        # Check filename is kebab-case
        if not re.match(r"^[a-z0-9-]+$", name):
            errors.append(f"{prefix}: Filename must be kebab-case")

        content = cmd_file.read_text()
        frontmatter, body = parse_frontmatter(content)

        # Frontmatter is optional for commands
        if frontmatter:
            # Validate model if present
            if "model" in frontmatter and frontmatter["model"] not in valid_models:
                errors.append(f"{prefix}: 'model' must be one of {valid_models}: '{frontmatter['model']}'")

            # Validate disable-model-invocation if present
            if "disable-model-invocation" in frontmatter:
                if not isinstance(frontmatter["disable-model-invocation"], bool):
                    errors.append(f"{prefix}: 'disable-model-invocation' must be boolean")

        # Check body exists
        if not body and not (frontmatter and body == ""):
            # If no frontmatter, content is the body
            if not content.strip():
                errors.append(f"{prefix}: Command body is empty")

    return errors


def validate_hooks(plugin_dir: Path) -> list[str]:
    """Validate hooks conform to Claude Code specs."""
    errors = []
    hooks_dir = plugin_dir / "hooks"
    if not hooks_dir.exists():
        return errors

    hooks_json = hooks_dir / "hooks.json"
    if not hooks_json.exists():
        errors.append(f"{plugin_dir.name}/hooks: Missing hooks.json")
        return errors

    try:
        with open(hooks_json) as f:
            config = json.load(f)
    except json.JSONDecodeError as e:
        errors.append(f"{plugin_dir.name}/hooks/hooks.json: Invalid JSON - {e}")
        return errors

    # Check for wrapper format
    if "hooks" not in config:
        errors.append(f"{plugin_dir.name}/hooks/hooks.json: Must use wrapper format with 'hooks' field")
        return errors

    valid_events = {
        "PreToolUse",
        "PostToolUse",
        "Stop",
        "SubagentStop",
        "SessionStart",
        "SessionEnd",
        "UserPromptSubmit",
        "PreCompact",
        "Notification",
    }

    hooks_config = config["hooks"]
    for event, hook_list in hooks_config.items():
        if event not in valid_events:
            errors.append(f"{plugin_dir.name}/hooks/hooks.json: Invalid event '{event}'. Must be one of {valid_events}")
            continue

        if not isinstance(hook_list, list):
            errors.append(f"{plugin_dir.name}/hooks/hooks.json: '{event}' must be array")
            continue

        for i, hook_entry in enumerate(hook_list):
            if not isinstance(hook_entry, dict):
                continue

            hooks = hook_entry.get("hooks", [])
            for j, hook in enumerate(hooks):
                if not isinstance(hook, dict):
                    continue

                hook_type = hook.get("type")
                if hook_type == "command":
                    cmd = hook.get("command", "")
                    # Check for ${CLAUDE_PLUGIN_ROOT} usage (only for script paths, not inline commands)
                    is_inline_cmd = any(op in cmd for op in [" ", "|", ";", "&&", "||", "$("])
                    if cmd and not cmd.startswith("${CLAUDE_PLUGIN_ROOT}") and not is_inline_cmd:
                        if "/" in cmd and not cmd.startswith("$"):
                            errors.append(
                                f"{plugin_dir.name}/hooks/hooks.json: "
                                f"{event}[{i}].hooks[{j}] should use ${{CLAUDE_PLUGIN_ROOT}}"
                            )

                    # Check script exists
                    if cmd and "${CLAUDE_PLUGIN_ROOT}" in cmd:
                        script_path = cmd.replace("${CLAUDE_PLUGIN_ROOT}", str(plugin_dir))
                        if not Path(script_path).exists():
                            errors.append(f"{plugin_dir.name}/hooks/hooks.json: Script not found: {cmd}")

                elif hook_type == "prompt":
                    if "prompt" not in hook:
                        errors.append(
                            f"{plugin_dir.name}/hooks/hooks.json: {event}[{i}].hooks[{j}] missing 'prompt' field"
                        )

    # Validate script naming in hooks/scripts/
    scripts_dir = hooks_dir / "scripts"
    if scripts_dir.exists():
        for script in scripts_dir.iterdir():
            if script.is_file() and script.suffix in {".py", ".sh"}:
                name = script.stem
                if not re.match(r"^[a-z0-9_]+$", name):
                    errors.append(f"{plugin_dir.name}/hooks/scripts/{script.name}: Script name must use snake_case")

    return errors


def validate_mcp(plugin_dir: Path) -> list[str]:
    """Validate MCP configuration if present."""
    errors = []
    mcp_json = plugin_dir / ".mcp.json"
    if not mcp_json.exists():
        return errors

    try:
        with open(mcp_json) as f:
            json.load(f)
    except json.JSONDecodeError as e:
        errors.append(f"{plugin_dir.name}/.mcp.json: Invalid JSON - {e}")

    return errors


def main():
    """Validate all plugins and return exit code."""
    plugins_dir = Path("plugins")
    if not plugins_dir.exists():
        print("No plugins directory found")
        return 0

    all_errors = []

    for plugin_dir in sorted(plugins_dir.iterdir()):
        if not plugin_dir.is_dir():
            continue
        if plugin_dir.name.startswith("."):
            continue

        all_errors.extend(validate_plugin_json(plugin_dir))
        all_errors.extend(validate_skills(plugin_dir))
        all_errors.extend(validate_agents(plugin_dir))
        all_errors.extend(validate_commands(plugin_dir))
        all_errors.extend(validate_hooks(plugin_dir))
        all_errors.extend(validate_mcp(plugin_dir))

    if all_errors:
        print("Plugin Validation Failed:")
        for error in all_errors:
            print(f"  - {error}")
        return 1

    print("All plugins validated successfully")
    return 0


if __name__ == "__main__":
    sys.exit(main())
