#!/usr/bin/env python3
"""Commit census and fix-candidate extraction for "Green but Wrong".

Read-only: runs only `git log`, `git rev-list`, `git for-each-ref` and
`git show -s` against each repository. Never checks out, writes or fetches.

Usage:
    python3 mine_commits.py --root ~/Documents --out ../data

Outputs (CSV, UTF-8):
    census.csv            one row per repository: commit counts by attributed author kind
    candidates.csv        every commit whose subject or body matches FIX_RE, with attribution
    bupples_sample.csv    seeded random sample (seed 20261101, n=40) of Bupples candidates

Attribution rule (per commit), applied in order:
    1. A Co-Authored-By trailer naming Claude            -> "claude"
       (if the message also names Codex, e.g. "Codex completion pass",
        the kind is "codex+claude": Codex work committed by Claude)
    2. No Claude trailer, and the commit is reachable only through a codex/*
       branch (the second-parent side of a "Merge pull request ... from
       <owner>/codex/..." merge, or a codex/* ref minus every non-codex ref)  -> "codex"
    3. Otherwise                                         -> "unknown"
A commit is never attributed to a human author by this script; the human
owner is the committer of record on every commit, so authorship of record
carries no information about who wrote the change.
"""
import argparse
import csv
import os
import random
import re
import subprocess

REPOS = ["Bupples", "careerapp", "WayClub", "photoshoot", "GitSpark",
         "Adelante", "planket", "hackathontinder"]
PRODUCT = {"Bupples": "Bupples", "careerapp": "CareerOS", "WayClub": "WayClub",
           "photoshoot": "Photoshoot", "GitSpark": "CodeAtlas", "Adelante": "Adelante",
           "planket": "Planket", "hackathontinder": "HACKFAM"}
FIX_RE = re.compile(r"\b(fix(es|ed)?|bug|regress(ion)?|revert(ed)?|root cause|defect)\b", re.I)
SEP = "\x1e"


def git(repo, *args):
    return subprocess.run(["git", "-C", repo, *args], capture_output=True,
                          text=True, check=True).stdout


def codex_set(repo):
    """Commits reachable only via codex/* branches (merged PRs or unmerged refs)."""
    out = set()
    # merged pull requests from codex/* branches
    for line in git(repo, "log", "--all", "--merges", "--format=%H %s").splitlines():
        h, _, subj = line.partition(" ")
        if re.search(r"from \S+/codex/", subj):
            out.update(git(repo, "rev-list", f"{h}^2", f"^{h}^1").split())
    refs = [r for r in git(repo, "for-each-ref", "--format=%(refname)",
                          "refs/heads", "refs/remotes").split() if not r.endswith("/HEAD")]
    codex = [r for r in refs if "/codex/" in r]
    other = [r for r in refs if "/codex/" not in r]
    for r in codex:
        out.update(git(repo, "rev-list", r, *[f"^{o}" for o in other]).split())
    return out


def commits(repo):
    fmt = SEP.join(["%H", "%h", "%ad", "%s", "%b"]) + "\x1f"
    raw = git(repo, "log", "--all", "--date=short", f"--format={fmt}")
    for rec in raw.split("\x1f"):
        rec = rec.strip("\n")
        if not rec:
            continue
        full, short, date, subj, body = (rec.split(SEP) + [""] * 5)[:5]
        yield full, short, date, subj, body


def kind(msg, cset, full):
    has_claude = re.search(r"Co-Authored-By:\s*Claude", msg, re.I) is not None
    if has_claude:
        return "codex+claude" if re.search(r"\bcodex\b", msg, re.I) else "claude"
    if full in cset:
        return "codex"
    return "unknown"


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--root", default=os.path.expanduser("~/Documents"))
    ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "..", "data"))
    a = ap.parse_args()
    os.makedirs(a.out, exist_ok=True)
    census, cands = [], []
    for name in REPOS:
        repo = os.path.join(a.root, name)
        cset = codex_set(repo)
        counts = {"claude": 0, "codex": 0, "codex+claude": 0, "unknown": 0}
        n = nfix = 0
        dates = []
        for full, short, date, subj, body in commits(repo):
            msg = subj + "\n" + body
            k = kind(msg, cset, full)
            counts[k] += 1
            n += 1
            dates.append(date)
            if FIX_RE.search(msg) and not subj.lower().startswith("merge"):
                nfix += 1
                cands.append({"repo": name, "product": PRODUCT[name], "commit": short,
                              "date": date, "author_kind": k, "subject": subj[:160]})
        census.append({"repo": name, "product": PRODUCT[name], "commits": n,
                       "first": min(dates), "last": max(dates), "fix_candidates": nfix,
                       **{f"kind_{k}": v for k, v in counts.items()}})
    with open(os.path.join(a.out, "census.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(census[0]))
        w.writeheader()
        w.writerows(census)
    with open(os.path.join(a.out, "candidates.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(cands[0]))
        w.writeheader()
        w.writerows(cands)
    bup = [c for c in cands if c["repo"] == "Bupples"]
    rng = random.Random(20261101)
    sample = sorted(rng.sample(bup, 40), key=lambda c: c["date"])
    with open(os.path.join(a.out, "bupples_sample.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(sample[0]))
        w.writeheader()
        w.writerows(sample)
    for c in census:
        print(c)


if __name__ == "__main__":
    main()
