#!/usr/bin/env python3
"""Version the skill bundle (TRK-125).

Skills changed constantly while nothing recorded which plugin version shipped
which skill content. This generator derives `skills-manifest.json` from the
canonical `skills/` directory: per-skill sha256 over every file in the skill,
a bundle hash over all skills, stamped with the package version. The manifest
ships in the npm package, so any repo can compare its installed skill bundle
against what a given plugin version actually contained.

Usage:
  python scripts/generate_skills_manifest.py          # write skills-manifest.json
  python scripts/generate_skills_manifest.py --check  # exit 1 if drifted
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
from pathlib import Path


def _normalized(path: Path) -> bytes:
    """File bytes with CRLF folded to LF, so Windows and Linux checkouts of the
    same commit hash identically (REQ-121: CRLF working-tree hashes broke the
    publish suite on the LF runner and three releases silently never shipped)."""
    return path.read_bytes().replace(b"\r\n", b"\n")


def _skill_hash(skill_dir: Path) -> str:
    # Sort by the POSIX relative path STRING, not by Path objects: Windows
    # paths order case-insensitively while Linux orders case-sensitively, so
    # Path-sorting fed files to the digest in platform-dependent order and the
    # same tree hashed differently on the publish runner (REQ-121, the second
    # false-green cause after CRLF).
    digest = hashlib.sha256()
    entries = sorted(
        (p.relative_to(skill_dir).as_posix(), p)
        for p in skill_dir.rglob("*") if p.is_file())
    for rel, path in entries:
        digest.update(rel.encode("utf-8"))
        digest.update(_normalized(path))
    return digest.hexdigest()


def build_manifest(repo_root) -> dict:
    root = Path(repo_root)
    package = json.loads((root / "package.json").read_text(encoding="utf-8-sig"))
    skills = {}
    for skill_dir in sorted((root / "skills").iterdir()):
        if not skill_dir.is_dir():
            continue
        skills[skill_dir.name] = {
            "sha256": hashlib.sha256(
                _normalized(skill_dir / "SKILL.md")).hexdigest(),
            "tree_sha256": _skill_hash(skill_dir),
            "files": sum(1 for p in skill_dir.rglob("*") if p.is_file()),
        }
    joined = "".join(f"{name}:{entry['sha256']}" for name, entry in sorted(skills.items()))
    return {
        "schema_version": "1.0",
        "plugin_version": package["version"],
        "skill_count": len(skills),
        "bundle_sha256": hashlib.sha256(joined.encode("utf-8")).hexdigest(),
        "skills": skills,
    }


def main(argv=None) -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--check", action="store_true",
                        help="Exit 1 when the committed manifest lags the skills.")
    args = parser.parse_args(argv)
    root = Path(args.repo_root)
    manifest = build_manifest(root)
    target = root / "skills-manifest.json"
    current = None
    if target.is_file():
        try:
            current = json.loads(target.read_text(encoding="utf-8-sig"))
        except ValueError:
            current = None
    if args.check:
        if current != manifest:
            print("skills-manifest.json is stale; regenerate with "
                  "python scripts/generate_skills_manifest.py")
            return 1
        print("skills-manifest.json is current "
              f"({manifest['skill_count']} skills, bundle {manifest['bundle_sha256'][:12]})")
        return 0
    target.write_text(json.dumps(manifest, indent=2) + "\n",
                      encoding="utf-8", newline="\n")
    print(f"wrote skills-manifest.json: {manifest['skill_count']} skills, "
          f"plugin {manifest['plugin_version']}, bundle {manifest['bundle_sha256'][:12]}")
    return 0


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