#!/usr/bin/env python3
"""Extract the owner's own typed messages about design/UI/UX from Claude Code
session transcripts (read-only).

Inclusion rule (applied in this order):
  1. Only top-level session files (<projects>/<project>/<session>.jsonl);
     subagent transcripts are skipped, because their "user" turns are prompts
     written by another agent, not by the owner.
  2. Only records with type == "user", isSidechain false, isMeta not true, and
     origin.kind absent or "human" (drops task notifications, peer messages,
     subagent hand-backs).
  3. Only text blocks (tool results are never read). Text that starts with a
     harness marker (<command-..., <local-command-..., <system-reminder>,
     <task-notification>, [Image, [Request interrupted, "This session is being
     continued", "Another Claude session") is dropped.
  4. The session that commissioned this study is excluded (--exclude).
  5. Keep a message if it matches the design keyword pattern (DESIGN_RE) and
     is at most 4,000 characters (longer pastes are usually documents).
  6. Exact duplicates (same normalised text) are kept once, earliest first;
     resumed and forked sessions replay the same turns.

Outputs:
  --out-private  JSONL with the message text (stays outside the public
                 artifact: it can contain names, links and other people's data)
  --out-public   CSV with only: msg_id, date, project_hint, chars, keyword hits,
                 sha256 prefix of the text. No text.
"""
import argparse, csv, hashlib, json, os, re, sys

DESIGN_RE = re.compile(r"\b(generic|ai[- ]?(feel|feeling|look|looking|slop|tell|elements?|coded|basic)|"
    r"basic|ugly|clean(er)?|spacing|font|fonts|typeface|colou?rs?|motion|animation|animations|"
    r"premium|consistent|consistency|clutter(ed)?|boring|smooth|dull|dead|bland|cringe|"
    r"polish(ed)?|refined?|ui|ux|design|layout|icons?|glass|glow|gradient|purple|dot|"
    r"awkward|over the top|template|vibe|aesthetic|fancy|look(s)? (bad|off|weird))\b", re.I)

SKIP_PREFIX = ("<command-", "<local-command", "<system-reminder", "<task-notification",
               "[Image", "[Request interrupted", "This session is being continued",
               "Another Claude session", "<bash-", "Caveat:", "<user-prompt-submit-hook")

def texts(rec):
    c = rec.get("message", {}).get("content")
    if isinstance(c, str):
        return [c]
    if isinstance(c, list):
        return [b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text"]
    return []

ALIASES = []  # extra (keyword, project) pairs, e.g. a person's name -> "partner-portfolio";
              # loaded from --alias-file so no personal name is published here

def project_hint(cwd, text):
    s = (cwd or "") + " " + text
    s = s.lower()
    for key, name in ALIASES + [("bupples", "bupples"), ("adelante", "adelante"), ("wayclub", "wayclub"),
                      ("joinlet", "hackfam"), ("hackfam", "hackfam"), ("hackathon", "hackfam"),
                      ("planket", "planket"), ("photoshoot", "photoshoot"), ("photo booth", "photoshoot"),
                      ("careerapp", "careeros"), ("careeros", "careeros"), ("nutri", "nutrigo"),
                      ("himalaya", "himalaya"), ("tajweed", "tajweed"), ("onplan", "onplan"),
                      ("portfolio", "portfolio"), ("yeegz.github.io", "portfolio"),
                      ("experts", "experts-marine"), ("marine", "experts-marine")]:
        if key in s:
            return name
    return "unknown"

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--root", default=os.path.expanduser("~/.claude/projects"))
    ap.add_argument("--exclude", action="append", default=[])
    ap.add_argument("--out-private", required=True)
    ap.add_argument("--out-public", required=True)
    ap.add_argument("--alias-file", help="JSON list of [keyword, project] pairs (kept private)")
    a = ap.parse_args()
    if a.alias_file:
        ALIASES.extend(tuple(x) for x in json.load(open(a.alias_file)))
    rows, seen, stats = [], set(), {"sessions": 0, "human_msgs": 0, "design_msgs": 0, "dupes": 0}
    for proj in sorted(os.listdir(a.root)):
        pdir = os.path.join(a.root, proj)
        if not os.path.isdir(pdir):
            continue
        for fn in sorted(os.listdir(pdir)):
            if not fn.endswith(".jsonl"):
                continue
            sid = fn[:-6]
            if any(sid.startswith(x) for x in a.exclude):
                continue
            stats["sessions"] += 1
            with open(os.path.join(pdir, fn), errors="replace") as fh:
                for line in fh:
                    if '"type":"user"' not in line and '"type": "user"' not in line:
                        continue
                    try:
                        r = json.loads(line)
                    except Exception:
                        continue
                    if r.get("type") != "user" or r.get("isSidechain") or r.get("isMeta"):
                        continue
                    o = r.get("origin")
                    if isinstance(o, dict) and o.get("kind") not in (None, "human"):
                        continue
                    c = r.get("message", {}).get("content")
                    has_img = isinstance(c, list) and any(isinstance(b, dict) and b.get("type") == "image" for b in c)
                    for t in texts(r):
                        t = t.strip()
                        if not t or t.startswith(SKIP_PREFIX):
                            continue
                        stats["human_msgs"] += 1
                        if len(t) > 4000 or not DESIGN_RE.search(t):
                            continue
                        norm = re.sub(r"\s+", " ", t.lower())
                        if norm in seen:
                            stats["dupes"] += 1
                            continue
                        seen.add(norm)
                        hits = sorted({m.group(0).lower() for m in DESIGN_RE.finditer(t)})
                        rows.append({"session": sid, "ts": r.get("timestamp", ""), "cwd": r.get("cwd", ""),
                                     "project_hint": project_hint(r.get("cwd", ""), t), "text": t, "hits": hits,
                                     "has_image": bool(has_img or re.search(r"\.(png|jpe?g|heic)\b|/uploads/", t, re.I))})
    rows.sort(key=lambda x: x["ts"])
    for i, x in enumerate(rows, 1):
        x["msg_id"] = f"T{i:04d}"
    stats["design_msgs"] = len(rows)
    with open(a.out_private, "w") as f:
        for x in rows:
            f.write(json.dumps(x, ensure_ascii=False) + "\n")
    with open(a.out_public, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["msg_id", "date", "project_hint", "chars", "keyword_hits", "has_image", "text_sha256_12"])
        for x in rows:
            w.writerow([x["msg_id"], x["ts"][:10], x["project_hint"], len(x["text"]), ";".join(x["hits"]), int(x["has_image"]),
                        hashlib.sha256(x["text"].encode()).hexdigest()[:12]])
    print(json.dumps(stats))

if __name__ == "__main__":
    main()
