#!/usr/bin/env python3
"""Validate plugin.json files against the strict ClawHub schema.

Required fields (exactly these 8, no others):
  name, description, version, author{name,url}, homepage, repository, license, skills

skills: must be either a string ("./skills") or an array of relative paths.
        The bare "./" form is REJECTED (Claude Code v2.1.107+ rejects it).
"""
import argparse
import json
import os
import re
import sys

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ALLOWED = {"name", "description", "version", "author", "homepage", "repository", "license", "skills"}
STRING_FIELDS = ("name", "description", "homepage", "repository", "license")
SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[\w.]+)?$")


def _check_keys(data):
    keys = set(data.keys())
    errors = []
    extra = keys - ALLOWED
    missing = ALLOWED - keys
    if extra:
        errors.append(f"extra fields: {sorted(extra)}")
    if missing:
        errors.append(f"missing fields: {sorted(missing)}")
    return errors


def _check_strings(data):
    return [f"{k}: must be string" for k in STRING_FIELDS if k in data and not isinstance(data[k], str)]


def _check_version(data):
    if "version" not in data:
        return []
    v = data["version"]
    if not isinstance(v, str) or not SEMVER.match(v):
        return [f"version: must match semver, got {v!r}"]
    return []


def _check_author(data):
    if "author" not in data:
        return []
    a = data["author"]
    if not isinstance(a, dict):
        return ["author: must be object {name, url}"]
    errors = []
    if not isinstance(a.get("name"), str):
        errors.append("author.name: must be string")
    if not isinstance(a.get("url"), str):
        errors.append("author.url: must be string")
    extra = set(a.keys()) - {"name", "url"}
    if extra:
        errors.append(f"author: extra fields {sorted(extra)}")
    return errors


def _check_skills_string(s):
    if s in ("./", ""):
        return ['skills: "./" is rejected by Claude Code v2.1.107+; use "./skills" or an array']
    return []


def _check_skills_array(s):
    if not s:
        return ["skills: array is empty"]
    errors = []
    for entry in s:
        if not isinstance(entry, str):
            errors.append(f"skills: entries must be strings, got {entry!r}")
        elif entry == "./":
            errors.append('skills: "./" is rejected by Claude Code v2.1.107+; list explicit subfolders')
    return errors


def _check_skills(data):
    if "skills" not in data:
        return []
    s = data["skills"]
    if isinstance(s, str):
        return _check_skills_string(s)
    if isinstance(s, list):
        return _check_skills_array(s)
    return ["skills: must be string or array of strings"]


def validate(path):
    try:
        with open(path) as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        return [f"unreadable JSON: {e}"]
    return (_check_keys(data) + _check_strings(data) + _check_version(data)
            + _check_author(data) + _check_skills(data))


def find_all():
    out = []
    for root, dirs, files in os.walk(REPO):
        if any(skip in root for skip in (".git", "node_modules", "eval-workspace", ".gemini")):
            dirs[:] = []
            continue
        if "plugin.json" in files and root.endswith(".claude-plugin"):
            out.append(os.path.join(root, "plugin.json"))
    return sorted(out)


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("path", nargs="?", help="Path to a plugin.json file")
    g.add_argument("--all", action="store_true", help="Validate every plugin.json in the repo")
    args = ap.parse_args()

    targets = find_all() if args.all else [args.path]
    failed = 0
    for t in targets:
        errs = validate(t)
        rel = os.path.relpath(t, REPO)
        if errs:
            failed += 1
            print(f"FAIL {rel}")
            for e in errs:
                print(f"  - {e}")
        else:
            print(f"OK   {rel}")
    if failed:
        print(f"\n{failed} file(s) failed validation", file=sys.stderr)
        return 1
    return 0


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