#!/usr/bin/env python3
"""Generate a compressed skills manifest and inject it into CLAUDE.md and AGENTS.md.

Scans .claude/skills/*/SKILL.md for YAML frontmatter, extracts name/description/triggers,
and produces a markdown table injected between manifest markers.

Usage:
    python3 .flydocs/scripts/generate_manifest.py [--root PATH] [--dry-run]

Options:
    --root PATH   Project root (default: current working directory)
    --dry-run     Print manifest to stdout without writing files
"""

import argparse
import json
import os
import re
import sys


MARKER_START = "<!-- flydocs:skills-manifest:start -->"
MARKER_END = "<!-- flydocs:skills-manifest:end -->"

TARGET_FILES = [
    os.path.join(".claude", "CLAUDE.md"),
    "AGENTS.md",
]


def parse_frontmatter(text):
    """Parse YAML frontmatter from a SKILL.md file.

    Handles simple key: value, block/folded scalars (| and >), and lists (- item).
    No PyYAML dependency required.
    """
    match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
    if not match:
        return None

    block = match.group(1)
    result = {}
    current_key = None
    current_mode = None  # "scalar", "block", "list"
    current_lines = []

    for line in block.split("\n"):
        # Check for a new top-level key
        key_match = re.match(r"^(\w[\w-]*):\s*(.*)", line)
        if key_match and not line.startswith("  "):
            # Flush previous key
            if current_key is not None:
                result[current_key] = _flush(current_mode, current_lines)

            current_key = key_match.group(1)
            value = key_match.group(2).strip()

            if value in ("|", ">"):
                current_mode = "block"
                current_lines = []
            elif value == "":
                # Could be a list or block starting next line
                current_mode = "list"
                current_lines = []
            else:
                # Simple inline value — strip quotes
                current_mode = "scalar"
                current_lines = [value.strip("\"'")]
            continue

        # Continuation lines (indented)
        stripped = line.strip()
        if current_key is not None:
            if current_mode == "list" and stripped.startswith("- "):
                current_lines.append(stripped[2:].strip().strip("\"'"))
            elif current_mode == "block":
                current_lines.append(line.lstrip())

    # Flush last key
    if current_key is not None:
        result[current_key] = _flush(current_mode, current_lines)

    return result


def _flush(mode, lines):
    """Convert accumulated lines into a final value."""
    if mode == "scalar":
        return lines[0] if lines else ""
    elif mode == "list":
        return lines
    elif mode == "block":
        return "\n".join(lines).strip()
    return ""


def scan_skills(root):
    """Scan .claude/skills/*/SKILL.md and return parsed skill metadata."""
    skills_dir = os.path.join(root, ".claude", "skills")
    if not os.path.isdir(skills_dir):
        print(f"Skills directory not found: {skills_dir}", file=sys.stderr)
        return []

    skills = []
    for entry in sorted(os.listdir(skills_dir)):
        skill_file = os.path.join(skills_dir, entry, "SKILL.md")
        if not os.path.isfile(skill_file):
            continue

        with open(skill_file, "r", encoding="utf-8") as f:
            content = f.read()

        fm = parse_frontmatter(content)
        if fm is None:
            continue

        name = fm.get("name", entry)
        description = fm.get("description", "")
        triggers = fm.get("triggers", [])

        if isinstance(triggers, str):
            triggers = [t.strip() for t in triggers.split(",") if t.strip()]

        if not triggers:
            continue

        skills.append({
            "name": name,
            "description": description,
            "triggers": triggers,
            "entry": os.path.join(".claude", "skills", entry, "SKILL.md"),
        })

    return skills


def load_skill_ordering(root):
    """Load PRECEDES edges from the context graph to derive skill ordering.

    Returns a dict mapping skill name -> list of skill names it loads after.
    """
    graph_path = os.path.join(root, "flydocs", "context", "graph.json")
    if not os.path.isfile(graph_path):
        return {}

    try:
        with open(graph_path, "r", encoding="utf-8") as f:
            graph = json.load(f)
    except (json.JSONDecodeError, OSError):
        return {}

    # Build loads_after map from PRECEDES edges
    # PRECEDES means: from loads before to (from PRECEDES to)
    loads_after = {}
    for edge in graph.get("edges", []):
        if edge.get("rel") != "PRECEDES":
            continue
        src = edge["from"]
        dst = edge["to"]
        # Both must be skill nodes
        if not src.startswith("skill:") or not dst.startswith("skill:"):
            continue
        dep_name = src.split(":", 1)[1]
        skill_name = dst.split(":", 1)[1]
        if skill_name not in loads_after:
            loads_after[skill_name] = []
        loads_after[skill_name].append(dep_name)

    return loads_after


def build_manifest(skills, loads_after=None):
    """Build the manifest markdown block.

    If loads_after is provided and non-empty, includes a "Loads After" column.
    """
    if loads_after is None:
        loads_after = {}

    has_ordering = any(
        skill["name"] in loads_after for skill in skills
    )

    if has_ordering:
        lines = [
            MARKER_START,
            "## Skills Index",
            "",
            "Consult the workflow skill for **issue operations and status transitions only**.",
            "For general coding tasks, skip this \u2014 just write code.",
            "",
            "| Skill | Triggers | Entry | Loads After |",
            "|-------|----------|-------|-------------|",
        ]

        for skill in skills:
            trigger_str = ", ".join(skill["triggers"])
            deps = loads_after.get(skill["name"], [])
            deps_str = ", ".join(deps) if deps else "\u2014"
            lines.append(
                f"| {skill['name']} | {trigger_str} | {skill['entry']} | {deps_str} |"
            )
    else:
        lines = [
            MARKER_START,
            "## Skills Index",
            "",
            "Consult the workflow skill for **issue operations and status transitions only**.",
            "For general coding tasks, skip this \u2014 just write code.",
            "",
            "| Skill | Triggers | Entry |",
            "|-------|----------|-------|",
        ]

        for skill in skills:
            trigger_str = ", ".join(skill["triggers"])
            lines.append(f"| {skill['name']} | {trigger_str} | {skill['entry']} |")

    lines.append(MARKER_END)
    return "\n".join(lines)


def inject_manifest(file_path, manifest):
    """Inject manifest between markers in a file.

    If markers exist, replaces content between them.
    If markers don't exist, appends to end of file.
    Returns True if the file was modified.
    """
    if not os.path.isfile(file_path):
        return False

    with open(file_path, "r", encoding="utf-8") as f:
        content = f.read()

    pattern = re.compile(
        re.escape(MARKER_START) + r".*?" + re.escape(MARKER_END),
        re.DOTALL,
    )

    if pattern.search(content):
        new_content = pattern.sub(manifest, content)
    else:
        # Append with spacing
        new_content = content.rstrip("\n") + "\n\n" + manifest + "\n"

    if new_content == content:
        return False

    with open(file_path, "w", encoding="utf-8") as f:
        f.write(new_content)

    return True


def main():
    parser = argparse.ArgumentParser(description="Generate FlyDocs skill manifest")
    parser.add_argument("--root", default=os.getcwd(), help="Project root directory")
    parser.add_argument("--dry-run", action="store_true", help="Print manifest without writing")
    args = parser.parse_args()

    root = os.path.abspath(args.root)
    skills = scan_skills(root)

    if not skills:
        print("No skills with triggers found.", file=sys.stderr)
        sys.exit(1)

    loads_after = load_skill_ordering(root)
    manifest = build_manifest(skills, loads_after)

    if args.dry_run:
        print(manifest)
        return

    for target in TARGET_FILES:
        file_path = os.path.join(root, target)
        if inject_manifest(file_path, manifest):
            print(f"Updated: {target}")
        elif os.path.isfile(file_path):
            print(f"No change: {target}")
        else:
            print(f"Not found: {target}")


if __name__ == "__main__":
    main()
