#!/usr/bin/python3
# people_data.py: the data layer behind HVN Voice Capture's "People" view.
#
# Reads the on-device voiceprint library (who the diarization pipeline can recognize) and the
# curated roster (who Jason meets with most), merges them, locates each enrolled person's voice
# sample clip, and prints one JSON blob the Swift shell injects via window.setState({people:[...]}).
#
# Pure stdlib so the system /usr/bin/python3 can run it (same as preflight.py). Reads only; never
# writes. The voiceprint embeddings and sample clips are biometric data and stay under the library
# dir on this Mac — this script reports their PATHS for local playback, it never copies them.
#
#   people_data.py --json [--library-dir DIR]
#
# Default library dir: ~/.claude/artifacts/hvn-voiceprints
import sys, os, json, glob, re

DEFAULT_LIB = os.path.expanduser("~/.claude/artifacts/hvn-voiceprints")
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]


def friendly_date(iso):
    """'2026-02-24' -> 'Feb 24, 2026'. Falls back to the raw string on any surprise."""
    m = re.match(r"^(\d{4})-(\d{2})-(\d{2})", iso or "")
    if not m:
        return iso or ""
    y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
    if not (1 <= mo <= 12):
        return iso
    return f"{_MONTHS[mo - 1]} {d}, {y}"


def load_json(path):
    try:
        with open(path) as f:
            return json.load(f)
    except (OSError, ValueError):
        return None


def index_samples(lib_dir):
    """Map a speaker's full name -> their voice sample clip path.

    Sample clips are named like:
      'SPEAKER_00 - RENAME_ME (starts at 48:52) - Jason Paez.m4a'
      'SPEAKER_02 - unknown - clip 1 (starts at 50:42) - Bobby Gibson.m4a'
    The real name is the trailing segment, so a case-insensitive 'stem ends with the name' match is
    robust (and 'Jason Paez' won't wrongly match 'Meg Partridge-Paez' because we test the full name).
    """
    by_name = {}
    pattern = os.path.join(lib_dir, "*-speaker-samples", "*.m4a")
    # Sort so a deterministic clip wins when a person has several; newest dated folder first.
    for path in sorted(glob.glob(pattern), reverse=True):
        stem = os.path.splitext(os.path.basename(path))[0].strip().lower()
        by_name[path] = stem
    return by_name


def sample_for(name, sample_stems):
    low = name.strip().lower()
    for path, stem in sample_stems.items():
        if stem.endswith(low):
            return path
    return None


def build(lib_dir):
    vp = load_json(os.path.join(lib_dir, "voiceprints.json")) or {}
    roster = load_json(os.path.join(lib_dir, "roster.json")) or {}
    speakers = vp.get("speakers", {}) if isinstance(vp, dict) else {}
    roster_people = roster.get("people", []) if isinstance(roster, dict) else []
    sample_stems = index_samples(lib_dir)

    def enrich(name, role, tier, relationship):
        rec = speakers.get(name)
        enrolled = rec is not None
        sample_path = sample_for(name, sample_stems) if enrolled else None
        ref = (rec or {}).get("reference_meetings", []) if enrolled else []
        n = (rec or {}).get("samples_count", 0) if enrolled else 0
        since = friendly_date(sorted(ref)[0]) if ref else ""
        if enrolled:
            bits = []
            if n:
                bits.append(f"{n} sample" + ("s" if n != 1 else ""))
            if since:
                bits.append(f"since {since}")
            status_label = " · ".join(bits) if bits else "Recognized"
        else:
            status_label = "Not yet enrolled"
        return {
            "name": name,
            "role": role or "",
            "tier": tier,
            "relationship": relationship or "",
            "enrolled": enrolled,
            "samples_count": n,
            "reference_meetings": ref,
            "since": since,
            "status_label": status_label,
            "allow_sharing": (rec or {}).get("allow_sharing") if enrolled else None,
            "sample_path": sample_path,
            "sample_exists": bool(sample_path and os.path.exists(sample_path)),
        }

    people = []
    seen = set()
    for p in roster_people:
        nm = (p.get("name") or "").strip()
        if not nm or nm in seen:
            continue
        seen.add(nm)
        people.append(enrich(nm, p.get("role"), p.get("tier", 3), p.get("relationship")))

    # Anyone enrolled but not in the curated roster still shows up as a recognized voice.
    for nm in speakers:
        if nm not in seen:
            seen.add(nm)
            people.append(enrich(nm, "", 3, ""))

    # Recognized first; within a group, by tier then name.
    people.sort(key=lambda x: (not x["enrolled"], x["tier"], x["name"].lower()))

    enrolled_n = sum(1 for p in people if p["enrolled"])
    return {
        "version": 1,
        "library_dir": lib_dir,
        "embedder": vp.get("embedder", "") if isinstance(vp, dict) else "",
        "counts": {
            "enrolled": enrolled_n,
            "needs_enrollment": len(people) - enrolled_n,
            "total": len(people),
        },
        "people": people,
    }


def main(argv):
    lib = DEFAULT_LIB
    if "--library-dir" in argv:
        i = argv.index("--library-dir")
        if i + 1 < len(argv):
            lib = os.path.expanduser(argv[i + 1])
    out = build(lib)
    sys.stdout.write(json.dumps(out))


if __name__ == "__main__":
    main(sys.argv[1:])
