#!/usr/bin/env python3
"""Aggregate `agents/runtime/metrics/skill-usage.jsonl` into a per-skill report.

Implements step-2-skill-inventory-rationalization.md Phase 1 Step 3.
Groups records by slug; emits `agents/runtime/metrics/skill-usage-report.md`
with columns:

  slug · exposures_total · mentions_total · exposures_30d · mentions_30d
       · last_seen · status

`status` ∈ { active, exposed-only, dead } per:

  active        = mentions_30d ≥ 1
  exposed-only  = exposures_30d ≥ 1 ∧ mentions_30d == 0
  dead          = exposures_30d == 0

The report is **a baseline, not a verdict**. Rationalization decisions
live in Phase 2 (`skill-rationalization-candidates.md`).
"""
from __future__ import annotations

import argparse
import json
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent.parent
IN = REPO / "agents" / "metrics" / "skill-usage.jsonl"
OUT = REPO / "agents" / "metrics" / "skill-usage-report.md"


def parse_ts(raw: str) -> datetime | None:
    if not raw:
        return None
    try:
        return datetime.fromisoformat(raw.replace("Z", "+00:00"))
    except ValueError:
        return None


def load_records(path: Path) -> list[dict]:
    if not path.exists():
        return []
    records: list[dict] = []
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        if not line.strip():
            continue
        try:
            records.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return records


def aggregate(records: list[dict], now: datetime, window_days: int = 30) -> dict[str, dict]:
    cutoff = now - timedelta(days=window_days)
    per: dict[str, dict] = defaultdict(lambda: {
        "exposures_total": 0, "mentions_total": 0,
        "exposures_30d": 0, "mentions_30d": 0,
        "last_seen": None,
    })
    for rec in records:
        slug = rec.get("slug")
        kind = rec.get("kind")
        if not slug or kind not in ("exposure", "mention"):
            continue
        ts = parse_ts(rec.get("ts") or "")
        bucket = per[slug]
        bucket[f"{kind}s_total"] += 1
        if ts and ts >= cutoff:
            bucket[f"{kind}s_30d"] += 1
        if ts and (bucket["last_seen"] is None or ts > bucket["last_seen"]):
            bucket["last_seen"] = ts
    return per


def status_for(row: dict) -> str:
    if row["mentions_30d"] >= 1:
        return "active"
    if row["exposures_30d"] >= 1:
        return "exposed-only"
    return "dead"


def all_known_slugs(repo: Path) -> set[str]:
    slugs: set[str] = set()
    for root in (repo / ".augment" / "skills", repo / ".claude" / "skills", repo / "dist/agent-src" / "skills"):
        if not root.is_dir():
            continue
        for skill_md in root.glob("*/SKILL.md"):
            slugs.add(skill_md.parent.name)
    return slugs


def render(per: dict[str, dict], known: set[str]) -> str:
    rows = []
    for slug in sorted(known | set(per)):
        data = per.get(slug, {
            "exposures_total": 0, "mentions_total": 0,
            "exposures_30d": 0, "mentions_30d": 0, "last_seen": None,
        })
        rows.append({"slug": slug, **data, "status": status_for(data)})
    rows.sort(key=lambda r: (r["status"] != "dead", -r["exposures_total"], r["slug"]))

    counts = {"active": 0, "exposed-only": 0, "dead": 0}
    for r in rows:
        counts[r["status"]] += 1
    total = len(rows)

    lines = [
        "# Skill Usage Report (baseline)",
        "",
        "> Generated by `scripts/skill_usage_report.py`. Source:",
        "> `agents/runtime/metrics/skill-usage.jsonl` (collector emits per-turn",
        "> exposure/mention records). See",
        "> [`step-2-skill-inventory-rationalization.md`](../roadmaps/step-2-skill-inventory-rationalization.md)",
        "> Phase 1.",
        "",
        f"**Window:** 30-day rolling \u00b7 **Skills tracked:** {total} \u00b7 "
        f"**Active:** {counts['active']} \u00b7 **Exposed-only:** {counts['exposed-only']} \u00b7 "
        f"**Dead:** {counts['dead']}",
        "",
        "| # | slug | status | exposures_30d | mentions_30d | exposures_total | mentions_total | last_seen |",
        "|---|---|---|---|---|---|---|---|",
    ]
    for i, r in enumerate(rows, 1):
        last = r["last_seen"].date().isoformat() if r["last_seen"] else "\u2014"
        lines.append(
            f"| {i} | `{r['slug']}` | {r['status']} | {r['exposures_30d']} | "
            f"{r['mentions_30d']} | {r['exposures_total']} | {r['mentions_total']} | {last} |"
        )
    lines.append("")
    lines.append("**Read-out:** rows tagged `dead` are first-cut archive candidates; "
                 "rows tagged `exposed-only` are first-cut merge / rename candidates "
                 "(catalog noise, agent never invokes). Phase 2 confirms with "
                 "structural overlap before any deletion.")
    lines.append("")
    return "\n".join(lines)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--in", dest="inp", type=Path, default=IN)
    ap.add_argument("--out", type=Path, default=OUT)
    ap.add_argument("--window", type=int, default=30, help="Rolling window in days")
    ap.add_argument("--quiet", action="store_true")
    args = ap.parse_args()

    records = load_records(args.inp)
    now = datetime.now(timezone.utc)
    per = aggregate(records, now, args.window)
    known = all_known_slugs(REPO)
    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(render(per, known), encoding="utf-8")
    if not args.quiet:
        print(f"\u2705  Wrote {args.out.relative_to(REPO)} ({len(known | set(per))} skill(s))")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
