#!/usr/bin/env python3
"""Locate-Matrix layer 0 — the FREE metadata map (no audio). See locate-matrix.md.

Joins the canonical site tracklists × take_gig_map by DATE (±3d fallback) to produce
candidate `(track × take)` cells, an inverted track→takes index, and coverage stats.
Canonical track identity = the `.tidal` file path (so "Pitbul Punk" and "PunkAChien"
merge). Everything here is state=`candidate` (metadata prior) — nothing is `verified`
until the ear gate (L1). Honest about what didn't match.
"""
import json, re, glob
from datetime import date
from pathlib import Path

HERE = Path(__file__).parent
LIVES = Path("/home/pln/Work/Web/www/next/content/lives")
TAKE_MAP = HERE / "take_gig_map.md"
OUT = HERE / "track_recording_map.json"
AS_OF = "2026-06-05"
FUZZY_DAYS = 3


def load_gigs():
    gigs = {}
    for f in glob.glob(str(LIVES / "**/tracks.json"), recursive=True):
        slug = str(Path(f).parent.relative_to(LIVES))
        try:
            d = json.load(open(f))
        except Exception:
            continue
        tracks = d.get("tracks") if isinstance(d, dict) else d
        dt = (d.get("date") if isinstance(d, dict) else None) or ""
        gigs[slug] = {"date": dt[:10], "tracks": tracks or []}
    return gigs


def parse_date(s):
    m = re.match(r"(\d{4})-(\d{2})-(\d{2})", s or "")
    return date(*map(int, m.groups())) if m else None


def load_takes():
    takes = []
    for line in TAKE_MAP.read_text().splitlines():
        m = re.match(r"\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*(Take\d+)\s*\|\s*([\d:]+)\s*\|"
                     r"\s*(\d+)\s*\|\s*(\w+)\s*\|\s*(.*?)\s*\|", line)
        if m:
            d, tk, dur, orb, typ, label = m.groups()
            takes.append({"date": d, "take": tk, "dur": dur, "orbits": int(orb),
                          "type": typ, "label": label})
    return takes


def match_gig(take_date, gigs):
    td = parse_date(take_date)
    if not td:
        return None, None
    exact = [s for s, g in gigs.items() if g["date"] == take_date]
    if len(exact) == 1:
        return exact[0], "date-exact"
    if len(exact) > 1:
        return exact[0], "date-exact-AMBIGUOUS"
    # ±FUZZY_DAYS window
    near = sorted(
        ((abs((parse_date(g["date"]) - td).days), s)
         for s, g in gigs.items() if parse_date(g["date"])),
        key=lambda x: x[0],
    )
    if near and near[0][0] <= FUZZY_DAYS:
        return near[0][1], f"date±{near[0][0]}d"
    return None, None


def track_id(t):
    """Canonical id = .tidal path; fall back to name."""
    f = (t.get("file") or "").strip()
    return f or f"name:{t.get('name', '?')}"


def main():
    gigs, takes = load_gigs(), load_takes()
    cells, by_track = [], {}
    matched = fuzzy = unmatched = 0
    gig_has_take = set()

    for tk in takes:
        slug, method = match_gig(tk["date"], gigs)
        if not slug:
            unmatched += 1
            continue
        gig_has_take.add(slug)
        if method == "date-exact":
            matched += 1
        else:
            fuzzy += 1
        is_set = tk["type"].upper() == "SET"
        for tr in gigs[slug]["tracks"]:
            tid = track_id(tr)
            cell = {
                "track": tid, "name": tr.get("name"), "tidal": tr.get("file"),
                "take": tk["take"], "gig": slug, "take_type": tk["type"],
                "bpm": tr.get("bpm"),
                # SET take → track is somewhere in it; single-track take → ambiguous which
                "state": "candidate" if is_set else "candidate-ambiguous",
                "signals": {"metadata": True, "join": method},
                "src": "derived:datejoin(site_tracklist×take_gig_map)", "as_of": AS_OF,
            }
            cells.append(cell)
            rec = by_track.setdefault(tid, {"aliases": set(), "takes": set(), "gigs": set()})
            if tr.get("name"):
                rec["aliases"].add(tr["name"])
            rec["takes"].add(tk["take"])
            rec["gigs"].add(slug)

    # release-readiness: tracks by # distinct takes that (per metadata) contain them
    ranked = sorted(
        ({"track": k, "aliases": sorted(v["aliases"]), "n_takes": len(v["takes"]),
          "takes": sorted(v["takes"]), "gigs": sorted(v["gigs"])}
         for k, v in by_track.items()),
        key=lambda x: -x["n_takes"],
    )
    out = {
        "schema": "locate-matrix L0 (metadata prior; nothing verified)",
        "as_of": AS_OF,
        "stats": {
            "takes_total": len(takes), "takes_matched_exact": matched,
            "takes_matched_fuzzy": fuzzy, "takes_unmatched": unmatched,
            "gigs_total": len(gigs), "gigs_with_take": len(gig_has_take),
            "gigs_without_take": len(gigs) - len(gig_has_take),
            "distinct_tracks": len(by_track),
            "tracks_multi_take": sum(1 for r in ranked if r["n_takes"] >= 2),
            "candidate_cells": len(cells),
        },
        "release_readiness": ranked,
        "cells": cells,
    }
    OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1))
    s = out["stats"]
    print(f"✓ {OUT}")
    print(f"  takes: {s['takes_total']} ({s['takes_matched_exact']} exact, "
          f"{s['takes_matched_fuzzy']} ±{FUZZY_DAYS}d, {s['takes_unmatched']} unmatched)")
    print(f"  gigs: {s['gigs_with_take']}/{s['gigs_total']} have a take")
    print(f"  tracks: {s['distinct_tracks']} distinct, "
          f"{s['tracks_multi_take']} in ≥2 takes (A/B/C possible)")
    print(f"  candidate cells: {s['candidate_cells']}")
    print("\n  top release-ready (most takes to choose from):")
    for r in ranked[:12]:
        nm = r["aliases"][0] if r["aliases"] else r["track"]
        alias = f"  «{', '.join(a for a in r['aliases'] if a != nm)}»" if len(r["aliases"]) > 1 else ""
        print(f"    {r['n_takes']:>2} takes  {nm}{alias}  → {', '.join(r['takes'])}")


if __name__ == "__main__":
    main()
