# -*- coding: utf-8 -*-
"""Which instruction files actually load, how often — and which NEVER have.

Usage:  python3 scripts/rules_report.py [--log FILE] [--workspace DIR] [--days N]

READS the InstructionsLoaded hook log (~/.claude/instructions-loaded.jsonl, plus its rotated .1) and
CROSS-REFERENCES it against every .claude/rules/*.md across the workspace.

THE COLUMN THAT MATTERS IS THE EMPTY ONE. A rule that has never fired is not a rule — it is either
mis-globbed (its `paths:` never match the files people actually edit) or dead (the surface it guards
is gone). That is the built-but-never-wired failure class, which this project has shipped four times,
applied to instructions: the rule file exists, reads correctly, passes the gate that checks it
declares `paths:` — and reaches the model never.

A never-fired rule is NOT automatically a defect. A rule scoped to a surface nobody has touched since
it was written is simply untested. The report says which, it does not judge: `paths:` globs are
printed beside each silent rule so the reader can tell "wrong glob" from "quiet month".

Exit code is always 0 — this is a report, not a gate.
"""
import argparse
import collections
import json
import os
import re
import sys


def load_events(paths, days):
    import datetime
    cutoff = None
    if days:
        cutoff = (datetime.datetime.utcnow() - datetime.timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
    events, bad = [], 0
    for p in paths:
        if not os.path.exists(p):
            continue
        with open(p, "r", encoding="utf-8", errors="replace") as fh:
            for line in fh:
                line = line.strip()
                if not line:
                    continue
                try:
                    e = json.loads(line)
                except ValueError:
                    bad += 1
                    continue
                if cutoff and (e.get("ts") or "") < cutoff:
                    continue
                events.append(e)
    return events, bad


def find_rule_files(workspace):
    """Every .claude/rules/*.md in the workspace, with its declared paths: globs."""
    out = {}
    for repo in sorted(os.listdir(workspace)):
        rd = os.path.join(workspace, repo, ".claude", "rules")
        if not os.path.isdir(rd):
            continue
        for name in sorted(os.listdir(rd)):
            if not name.endswith(".md"):
                continue
            full = os.path.join(rd, name)
            try:
                with open(full, "r", encoding="utf-8") as fh:
                    head = fh.read(2048)
            except OSError:
                head = ""
            m = re.search(r"^paths:\s*(.+)$", head, re.M)
            globs = m.group(1).strip() if m else ("(unconditional)" if re.search(
                r"^unconditional:\s*true", head, re.M) else "(NO paths: — loads every session)")
            out[full] = (repo, name, globs)
    return out


def main(argv):
    home = os.path.expanduser("~")
    ap = argparse.ArgumentParser()
    ap.add_argument("--log", default=os.path.join(home, ".claude", "instructions-loaded.jsonl"))
    ap.add_argument("--workspace",
                    default=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
    ap.add_argument("--days", type=int, default=0, help="only count events newer than N days")
    args = ap.parse_args(argv[1:])

    logs = [args.log, args.log + ".1"]
    events, bad = load_events(logs, args.days)

    print("rules_report")
    print("  log       : %s%s" % (args.log, "  (+ rotated .1)" if os.path.exists(args.log + ".1") else ""))
    print("  workspace : %s" % args.workspace)
    print("  events    : %d%s%s" % (len(events),
                                    ("  (last %d days)" % args.days) if args.days else "",
                                    ("  [%d unparseable lines skipped]" % bad) if bad else ""))
    if not events:
        print("\n  No events. The hook may not be installed — see scripts/install_workspace.py.")

    by_reason = collections.Counter(e.get("load_reason") or "?" for e in events)
    print("\n== why instructions loaded ==")
    for reason, n in by_reason.most_common():
        print("  %-18s %d" % (reason, n))

    fired = collections.Counter()
    triggers = collections.defaultdict(collections.Counter)
    for e in events:
        fp = e.get("file_path") or "?"
        fired[fp] += 1
        t = e.get("trigger_file_path")
        if t:
            triggers[fp][os.path.basename(t)] += 1

    print("\n== instruction files that LOADED ==")
    if not fired:
        print("  (none)")
    for fp, n in fired.most_common():
        short = fp.replace(args.workspace + os.sep, "")
        ex = triggers[fp].most_common(1)
        print("  %5d  %-58s %s" % (n, short, ("e.g. via %s" % ex[0][0]) if ex else ""))

    rules = find_rule_files(args.workspace)
    silent = [(p, v) for p, v in sorted(rules.items()) if p not in fired]
    print("\n== rule files that have NEVER fired  (%d of %d) ==" % (len(silent), len(rules)))
    if not silent:
        print("  none — every rule file in the workspace has loaded at least once")
    for p, (repo, name, globs) in silent:
        print("  %-24s %-18s paths: %s" % (repo, name, globs))
    if silent:
        print("\n  A never-fired rule is mis-globbed, dead, or simply guarding a surface nobody has")
        print("  edited since it was written. The globs above are printed so you can tell which.")
    return 0


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