// rs-bridge.jsx
// ─────────────────────────────────────────────────────────────────────
// Maps Rocket Science (real) data into the seed-data shapes the existing
// screens consume, then merges them onto the global arrays.
//
// Shapes the app expects:
//   WORKS    : {id, iswc, title, writers[], pro, status, shares, catalog,
//               copyright, duration, plays, societies, unrecorded?}
//   RECORDING_GRAPH : {id, workId, isrc, title, artist, album, label,
//                     duration, year, plays, art, explicit, derivesFrom[],
//                     alsoWorks[], releaseIds[]}
//   RELEASES_X : {id, releaseGroupId, editionLabel, isPrimary, title, artist, kind}
//   RELEASES   : {id, groupId, editionLabel, upc, title, artist, label, type,
//                 format, status, releaseDate, region, ...}
//   PUBLISHERS : {id, name, role, ...}
//   ARTISTS    : {id, name, ...} (varies)
//
// This script runs LAST in the load chain (after songs.jsx + screens3.jsx)
// so it can merge into the existing arrays in place.
// ─────────────────────────────────────────────────────────────────────

(function rsBridge() {
  if (!window.RS || !window.RS.loaded) {
    console.warn('[rs-bridge] RS layer not loaded — bridge skipped');
    return;
  }

  // ── ID prefix so RS rows don't collide with seed IDs ──────────────
  const wId  = id => `rs_w_${id}`;
  const rId  = id => `rs_r_${id}`;
  const rlId = id => `rs_rl_${id}`;
  const rgId = id => `rs_rg_${id}`;
  const pubId = id => `rs_pub_${id}`;
  const profId = id => `rs_prof_${id}`;

  // ── Helpers ───────────────────────────────────────────────────────
  const sumPct = (arr, key) =>
    arr.reduce((a, x) => a + (parseFloat(x[key]) || 0), 0);

  // Stable pseudo-color for art swatches based on a string id hash
  const swatch = (s) => {
    s = String(s || '');
    let h = 0;
    for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i);
    const hue = Math.abs(h) % 360;
    return `oklch(72% 0.13 ${hue})`;
  };

  // Pick a deterministic-ish "PRO" hint from a writer share's USA License
  // or fall back to first publisher's society / ASCAP.
  function inferPro(writerShare) {
    const pubs = writerShare.publishingShares || [];
    for (const p of pubs) {
      if (p['USA License']) return p['USA License'];
    }
    return 'ASCAP';
  }

  // Map RS work status → app status enum
  function mapWorkStatus(ws) {
    if (!ws.length) return 'pending';
    const totalPerf = sumPct(ws, 'Performance Share');
    if (totalPerf >= 99.99) return 'registered';
    if (totalPerf > 0) return 'pending';
    return 'pending';
  }

  // Build "shares" string like "62/38" or "100"
  function sharesString(ws) {
    if (!ws.length) return '—';
    if (ws.length === 1) return '100';
    return ws.map(w => Math.round(parseFloat(w['Performance Share']) || 0)).join('/');
  }

  // Build copyright string from publisher chain
  function copyrightString(ws) {
    const pubs = (ws[0]?.publishingShares || []);
    const admin = pubs.find(p => p['Publisher Role'] === 'Administrator') || pubs[0];
    if (!admin) return '';
    return `© ${admin.Publisher}`;
  }

  // Build catalog tag (admin publisher name)
  function catalogTag(ws) {
    const pubs = (ws[0]?.publishingShares || []);
    const admin = pubs.find(p => p['Publisher Role'] === 'Administrator') || pubs[0];
    return admin ? admin.Publisher : '';
  }

  // ── 1. WORKS ──────────────────────────────────────────────────────
  function songwriterName(profile, fallback) {
    const fullName = profile
      ? [profile.first_name, profile.middle_name, profile.last_name].filter(Boolean).join(' ').trim()
      : '';
    return fullName || fallback || profile?.artist_name || '';
  }

  const rsWorks = (window.RS.works || []).map(w => {
    const ws = w.writerShares || [];
    const writers = ws.map(s => songwriterName(s.profile, s.Writer)).filter(Boolean);
    const pro = inferPro(ws[0] || {});
    const totalPerf = sumPct(ws, 'Performance Share');
    return {
      id: wId(w['Work ID']),
      iswc: w.ISWC || '',
      title: w['Work Title'],
      writers,
      pro,
      status: mapWorkStatus(ws),
      shares: sharesString(ws),
      catalog: catalogTag(ws),
      copyright: copyrightString(ws),
      duration: 0, // RS works don't carry duration — set by recording
      plays: 0,
      societies: new Set(ws.flatMap(s => (s.publishingShares || []).map(p => p['USA License']).filter(Boolean))).size || 1,
      // RS-specific augmentations (read by RS-aware screens):
      __rs: true,
      __rsRaw: w,
      __rsTotalPerf: totalPerf,
      __rsControlled: ws.some(s => s.Controlled === 'Yes'),
      __rsRecordings: w.recordings || [],
      __rsWriterShares: ws,
    };
  });

  // ── 2. RECORDING_GRAPH ────────────────────────────────────────────
  // Each work's recordings → a recording row keyed by Recording ID.
  // Duration in RS is "m:ss" or "mm:ss" — convert to seconds.
  const parseDur = (s) => {
    if (!s || typeof s !== 'string') return 0;
    const parts = s.split(':').map(n => parseInt(n) || 0);
    if (parts.length === 2) return parts[0] * 60 + parts[1];
    if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
    return parseInt(s) || 0;
  };
  const rsRecordings = [];
  // ISRC-based dedup: same ISRC = same master, surfaced multiple times because RS Airtable
  // creates a separate "recording" row per release/compilation appearance. We merge them
  // into a single recording, aggregating all releaseIds and exposing the alias rec IDs.
  // Build a release lookup so we can resolve "Release" string → date/label
  const relByName = new Map();
  for (const rel of (window.RS.releases || [])) {
    const name = rel['Release Name'] || rel['Release Title'];
    if (name && !relByName.has(name)) relByName.set(name, rel);
  }
  const pickYear = (s) => {
    if (!s) return 0;
    const m = String(s).match(/^(\d{4})/);
    if (!m) return 0;
    const y = parseInt(m[1]);
    return (y >= 1900 && y <= 2099) ? y : 0;
  };

  // Pass 1: collect all (work, rec) pairs with the rec's full context.
  const allRecRows = [];
  const seenRecRawIds = new Set();
  for (const w of (window.RS.works || [])) {
    for (const rec of (w.recordings || [])) {
      const rawId = rec['Recording ID'];
      if (seenRecRawIds.has(rawId)) continue; // same rec attached to multiple works in the join graph
      seenRecRawIds.add(rawId);
      allRecRows.push({ w, rec, rawId });
    }
  }

  // Pass 2: bucket by ISRC. Recordings without an ISRC stay individual (keyed by their raw ID).
  const recBuckets = new Map();
  for (const row of allRecRows) {
    const isrc = (row.rec['ISRC (Audio)'] || row.rec.ISRC || '').trim();
    const key = isrc || `__noisrc_${row.rawId}`;
    if (!recBuckets.has(key)) recBuckets.set(key, []);
    recBuckets.get(key).push(row);
  }

  // Pass 3: build one merged recording per bucket. Pick canonical fields from the row whose
  // release-date is earliest; aggregate releaseIds across all rows in the bucket.
  for (const [bucketKey, rows] of recBuckets) {
    // Sort by earliest release date so the canonical row is the original master release.
    rows.sort((a, b) => {
      const ra = relByName.get((a.rec.Release||'').split(',')[0]?.trim()) || {};
      const rb = relByName.get((b.rec.Release||'').split(',')[0]?.trim()) || {};
      return (ra['Release Date']||'9999').localeCompare(rb['Release Date']||'9999');
    });
    const canonical = rows[0];
    const w = canonical.w;
    const rec = canonical.rec;
    const recId = rId(rec['Recording ID']);

    // Aggregate release IDs across all alias rows.
    const allReleaseIds = new Set();
    const allReleaseNames = new Set();
    const aliasRecIds = [];
    for (const row of rows) {
      if (row.rawId !== rec['Recording ID']) aliasRecIds.push(row.rawId);
      const names = (row.rec.Release || '').split(',').map(s => s.trim()).filter(Boolean);
      for (const n of names) {
        allReleaseNames.add(n);
        const rel = relByName.get(n);
        if (rel) allReleaseIds.add(rlId(rel['Release ID']));
      }
    }

    const artists = (rec.artistShares || []).map(a => a.profile?.artist_name || a.Artist).filter(Boolean);
    const artistStr = artists.length ? artists.join(', ') : (rec.Artist || 'Unknown');
    const releaseNames = (rec.Release || '').split(',').map(s => s.trim()).filter(Boolean);
    const firstReleaseName = releaseNames[0] || '';
    const firstReleaseRow = firstReleaseName ? relByName.get(firstReleaseName) : null;
    const releaseLabel = firstReleaseName || '—';
    const releaseDateStr = firstReleaseRow?.['Release Date'] || '';
    const recordingDateStr = rec['Recording Date'] || '';
    const validYear = pickYear(releaseDateStr) || pickYear(recordingDateStr);

    rsRecordings.push({
      id: recId,
      workId: wId(w['Work ID']),
      isrc: rec['ISRC (Audio)'] || rec.ISRC || '',
      title: rec['Track Name'] || rec.title || w['Work Title'],
      artist: artistStr,
      album: releaseLabel,
      label: rec.Label || firstReleaseRow?.['Release Label'] || '',
      duration: parseDur(rec.Duration),
      year: validYear,
      releaseDate: releaseDateStr || '',
      recordingDate: recordingDateStr || '',
      firstReleaseDate: releaseDateStr || '',
      plays: 0,
      art: swatch(rec['Recording ID']),
      explicit: rec.Explicit === 'Yes' || rec.Explicit === true,
      genre: rec.Genre || '',
      language: rec.Language || '',
      derivesFrom: [],
      alsoWorks: [],
      releaseIds: [...allReleaseIds],
      // Hygiene metadata — surfaced on the recording detail to show that this master
      // was rolled up from multiple Airtable rows (different release appearances).
      __rsAliasIds: aliasRecIds,           // other RS Recording IDs that fold into this master
      __rsAppearances: [...allReleaseNames], // every release/compilation this master appears on
      __rsAppearanceCount: rows.length,
      __rs: true,
      __rsRaw: rec,
    });
  }

  // ── 3. RELEASES (full catalog rows + groups) ─────────────────────
  // Build a recording-count index per release ID by resolving release names.
  // Use a Set per release to avoid double-counting when a recording lists
  // the same release name twice in its "Release" string.
  const recCountByRelId = new Map();
  const recsPerRelease = new Map(); // releaseId → Set<recordingId>
  for (const w of (window.RS.works || [])) {
    for (const rec of (w.recordings || [])) {
      const recIdRaw = rec['Recording ID'];
      const names = (rec.Release || '').split(',').map(s => s.trim()).filter(Boolean);
      for (const n of names) {
        const rel = relByName.get(n);
        if (rel) {
          const k = rel['Release ID'];
          if (!recsPerRelease.has(k)) recsPerRelease.set(k, new Set());
          recsPerRelease.get(k).add(recIdRaw);
        }
      }
    }
  }
  for (const [k, set] of recsPerRelease) recCountByRelId.set(k, set.size);

  // ── Release grouping (editions roll up under a parent Release Group) ──
  // RS Airtable has separate rows for: Digital original, CD pressing, regional re-distribution,
  // 2015 reissue, "Reloaded" edition, etc. They share Release Name + Artist but differ on UPC,
  // Configuration, Country and Release Date.
  //
  // Strategy: bucket by lower(name)+lower(artist). The earliest-dated row in a bucket becomes
  // the "primary" edition; siblings get a derived edition label like "CD · 2005",
  // "2015 Reissue", "MX Re-distribution".
  const releaseBuckets = new Map(); // bucketKey → array of raw release rows
  for (const raw of (window.RS.releases || [])) {
    const k = (raw['Release Name']||'').toLowerCase().trim() + '§' + (raw['Release Artist']||'').toLowerCase().trim();
    if (!releaseBuckets.has(k)) releaseBuckets.set(k, []);
    releaseBuckets.get(k).push(raw);
  }
  // Sort each bucket by date asc — earliest is the primary edition.
  for (const arr of releaseBuckets.values()) {
    arr.sort((a,b) => (a['Release Date']||'9999').localeCompare(b['Release Date']||'9999'));
  }
  // Stable groupId per bucket — derived from the earliest release ID in the bucket.
  const groupIdByRawId = new Map();
  const primaryRawIdByBucket = new Map();
  for (const [k, arr] of releaseBuckets) {
    const primaryRaw = arr[0];
    const gid = rgId(primaryRaw['Release ID']);
    primaryRawIdByBucket.set(k, primaryRaw['Release ID']);
    for (const raw of arr) groupIdByRawId.set(raw['Release ID'], gid);
  }
  // Edition label heuristic: format + year for siblings; "Original" for the earliest.
  function editionLabelFor(raw, primaryRaw) {
    const cfg = (raw.Configuration||'').trim();
    const ver = (raw.Version||'').trim();
    const country = (raw.Country||'').trim();
    const date = raw['Release Date']||'';
    const yr = date.slice(0,4);
    const primaryYr = (primaryRaw['Release Date']||'').slice(0,4);
    const primaryCfg = (primaryRaw.Configuration||'').trim();
    if (raw['Release ID'] === primaryRaw['Release ID']) {
      // The first edition just gets "Original" or the version label if present.
      if (ver) return ver;
      return 'Original' + (cfg ? ` · ${cfg}` : '');
    }
    // Distinguishing axis: prefer Version, then format change, then year reissue, then country.
    if (ver) return ver;
    if (cfg && cfg !== primaryCfg) return `${cfg}${yr ? ` · ${yr}` : ''}`;
    if (yr && yr !== primaryYr) return `${yr} Reissue${cfg && cfg !== 'Digital' ? ` · ${cfg}` : ''}`;
    if (country && country !== 'World' && country !== (primaryRaw.Country||'')) return `${country} Re-dist`;
    // Last resort — disambiguate by UPC tail.
    const upc = (raw['UPC (Release)']||'').slice(-4);
    return `Edition · ${upc || raw['Release ID'].slice(-3)}`;
  }

  const rsReleases = (window.RS.releases || []).map(r => {
    const id = r['Release ID'];
    const date = r['Release Date'] || '';
    const totalTracks = parseInt(r['Total Tracks']) || recCountByRelId.get(id) || 0;
    const groupKey = (r['Release Name']||'').toLowerCase().trim() + '§' + (r['Release Artist']||'').toLowerCase().trim();
    const bucket = releaseBuckets.get(groupKey) || [r];
    const primaryRaw = bucket[0];
    const isPrimary = id === primaryRaw['Release ID'];
    return {
      id: rlId(id),
      groupId: groupIdByRawId.get(id) || rgId(id),
      editionLabel: editionLabelFor(r, primaryRaw),
      isPrimary,
      upc: r['UPC (Release)'] || r.UPC || '',
      title: r['Release Name'] || r['Release Title'] || '—',
      artist: r['Release Artist'] || r['Primary Artist'] || '—',
      label: r['Release Label'] || r.Label || '',
      type: r['Album Type'] || r['Release Type'] || 'Album',
      format: r.Configuration || 'Digital',
      // Catalog ReleasesView filters by `status === 'live'` to pick canonical edition.
      // RS Status values: 'Released', 'Taken Down' etc — collapse to 'live' so they appear.
      status: 'live',
      date,
      releaseDate: date,
      region: r.Country || r.Territory || 'World',
      tracks: totalTracks,
      dsps: 14,
      ddex: 'delivered',
      art: swatch(id),
      __rs: true,
      __rsRaw: r,
    };
  });

  // De-duped Release Groups — one per bucket, not one per row.
  const rsReleaseGroups = [];
  const seenGroups = new Set();
  for (const r of rsReleases) {
    if (seenGroups.has(r.groupId)) continue;
    seenGroups.add(r.groupId);
    const siblings = rsReleases.filter(x => x.groupId === r.groupId);
    const primary = siblings.find(x => x.isPrimary) || siblings[0];
    rsReleaseGroups.push({
      id: r.groupId,
      title: primary.title,
      artist: primary.artist,
      primaryReleaseId: primary.id,
      editionCount: siblings.length,
      __rs: true,
    });
  }

  const rsReleasesX = rsReleases.map(r => ({
    id: r.id,
    releaseGroupId: r.groupId,
    editionLabel: r.editionLabel,
    isPrimary: r.isPrimary,
    title: r.title,
    artist: r.artist,
    kind: r.type,
    __rs: true,
  }));

  // ── 4. PUBLISHERS ─────────────────────────────────────────────────
  // Country lookup keyed off the publisher's PRO (heuristic — can be overridden by Airtable territory).
  const proToCountry = (pro) => {
    if (!pro) return '';
    const u = pro.toUpperCase();
    if (['ASCAP','BMI','SESAC','HFA'].includes(u)) return 'US';
    if (u === 'PRS' || u === 'MCPS') return 'GB';
    if (u === 'SOCAN') return 'CA';
    if (u === 'GEMA')  return 'DE';
    if (u === 'SACEM') return 'FR';
    if (u === 'SACVEN' || u === 'SAYCO') return 'VE';
    if (u === 'SADAIC') return 'AR';
    if (u === 'SACM' || u === 'SGAE') return 'ES';
    if (u === 'STIM') return 'SE';
    if (u === 'SCD') return 'CL';
    if (u === 'UBC' || u === 'ABRAMUS' || u === 'ECAD') return 'BR';
    return '';
  };
  // Helper: country code → human territory string.
  const countryToTerritory = (cc) => ({US:'United States',GB:'United Kingdom',CA:'Canada',DE:'Germany',FR:'France',VE:'Venezuela',AR:'Argentina',ES:'Spain',SE:'Sweden',CL:'Chile',BR:'Brazil'}[cc]) || 'Worldwide';

  // Pull richer Airtable publisher rows (keyed by Name).
  // Index synthesized RS publishers (from publishing_share rollup) by name so we can merge them.
  const synthByName = new Map(
    (window.RS.publishers || []).map(p => [p.name, p])
  );

  // Walk every Airtable publisher row first (the canonical, comprehensive list).
  const dirPubs = window.RS.directory?.publishers || [];
  const rsPublishers = dirPubs.map(p => {
    const synth = synthByName.get(p.Name);
    const pro = p.PRO || synth?.role || '';
    const cc = proToCountry(pro);
    const ip = p['Interested Party'] || '';
    const admin = p['Admin/Co-Publisher'] || '';
    const isRSCorp = /^Rocket Science (Music )?Publishing/i.test(p.Name);
    const parent = isRSCorp ? '—' : (ip && ip !== p.Name ? ip : '—');
    return {
      // Use the real RS publisher ID — falls back to a stable name-derived id.
      id: 'rs_pub_' + (p['Publisher ID'] || p.IPI || p.Name.replace(/\W+/g,'_')),
      name: p.Name,
      aliases: p.Name === ip ? '' : (ip || ''),
      cae: p.IPI || '',
      ipi: p.IPI || '',
      pNumber: p['P Number'] || '',
      submitterCode: p['CWR Sender Code'] || '',
      cwrSenderId: p['CWR Sender ID'] || '',
      cwrSenderCode: p['CWR Sender Code'] || '',
      ipiNameNumber: p['IPI Name Number'] || '',
      dpid: p.DPID || '',
      isni: p.ISNI || '',
      abramusId: p['Abramus ID'] || '',
      ecadId: p['Ecad ID'] || '',
      cmrraId: p['CMRRA Account #'] || '',
      pro,
      country: cc,
      territory: countryToTerritory(cc),
      controlled: p.Controlled === 'Yes',
      mech: p.Mech || '',
      sync: p.Sync || '',
      interestedParty: ip,
      administrator: admin || 'Self',
      parent,
      parentPub: parent !== '—' ? parent : null,
      role: synth?.role || (ip === p.Name ? 'Original Publisher' : 'Co-Publisher'),
      since: '2018',
      works: parseInt(p['Count (Publishing Shares)']) || synth?.workCount || 0,
      __rs: true,
      __rsRaw: p,
    };
  });

  // ── 5. PROFILES (writers + artists from RS) ───────────────────────
  // Stable per-profile color swatch (used by the Profiles grid tiles).
  const PROFILE_PALETTE = ['#0f1f4f','#1a1a1a','#5a3d1c','#b04a3a','#a02a4d','#000','#d4a02a','#0f4c2a','#3a4a5a','#4a2a5a','#2a4a5a','#5a2a3a'];
  const proKindPalette = (id) => {
    let h = 0; const s = String(id || '');
    for (let i = 0; i < s.length; i++) h = ((h<<5) - h) + s.charCodeAt(i);
    return PROFILE_PALETTE[Math.abs(h) % PROFILE_PALETTE.length];
  };
  // Count works/recordings per profile (from the RS layer).
  const worksByProfile = new Map();
  const recsByProfile = new Map();
  for (const w of (window.RS.works || [])) {
    for (const ws of (w.writerShares || [])) {
      const pid = ws.profile?.profile_id;
      if (pid) worksByProfile.set(pid, (worksByProfile.get(pid) || 0) + 1);
    }
  }
  for (const w of (window.RS.works || [])) {
    for (const rec of (w.recordings || [])) {
      for (const a of (rec.artistShares || [])) {
        const pid = a.profile?.profile_id;
        if (pid) recsByProfile.set(pid, (recsByProfile.get(pid) || 0) + 1);
      }
    }
  }
  const rsProfiles = (window.RS.profiles || []).map(p => {
    const fullName = [p.first_name, p.middle_name, p.last_name].filter(Boolean).join(' ').trim();
    const cc = proToCountry(p.pro_affiliation);
    return {
      id: 'rs_prof_' + p.profile_id,
      name: fullName || p.artist_name || '—',
      artistName: p.artist_name || '',
      realName: fullName && fullName !== p.artist_name ? fullName : null,
      ipi: p.ipi_name_number || '',
      cae: p.ipi_name_number || '',
      pro: p.pro_affiliation || '',
      country: cc,
      type: p.profile_type || 'Person',
      kind: p.profile_type || 'Person',
      legal: (p.profile_type || 'Person') === 'Person' && !!fullName,
      color: proKindPalette(p.profile_id),
      roster: worksByProfile.get(p.profile_id) || 0,
      recCount: recsByProfile.get(p.profile_id) || 0,
      affiliations: p.pro_affiliation ? [p.pro_affiliation] : [],
      aliases: p.alias ? p.alias.split(',').map(s=>s.trim()).filter(Boolean) : [],
      territories: cc ? [cc] : [],
      __rs: true,
      __rsRaw: p,
    };
  });

  // ── 6. LABELS (from Airtable label export) ────────────────────────
  // Build per-label release/recording counts by parsing the comma-list strings.
  const dirLabels = window.RS.directory?.labels || [];
  const LABEL_COLORS = ['#0f1f4f','#1a1a1a','#5a3d1c','#b04a3a','#a02a4d','#000','#d4a02a','#0f4c2a','#3a4a5a','#4a2a5a','#2a4a5a'];
  const labelColor = (id) => {
    let h = 0; const s = String(id || '');
    for (let i = 0; i < s.length; i++) h = ((h<<5) - h) + s.charCodeAt(i);
    return LABEL_COLORS[Math.abs(h) % LABEL_COLORS.length];
  };
  // Heuristic: labels with "Rocket Science" or "Independent" parent are indie.
  // Counting "artists" — derive by attributing recordings to their artists in the RS recording table by Label match.
  const recsByLabel = new Map(); // label name → Set(artists)
  for (const rec of (window.RS.recordings || [])) {
    const lbl = rec.Label;
    if (!lbl) continue;
    if (!recsByLabel.has(lbl)) recsByLabel.set(lbl, new Set());
    for (const a of (rec.Artist || '').split(',').map(s=>s.trim()).filter(Boolean)) {
      recsByLabel.get(lbl).add(a);
    }
  }
  const rsLabels = dirLabels.map((l, idx) => {
    const ip = l['Interested Party'] || '';
    // Splitting comma-list: tracks/releases — note album titles may contain commas already, so this is imperfect.
    // We use the count of pipe-or-comma split as an approximation.
    const recordingsList = (l.Recordings || '').split(/,(?![^"]*"\s*[,$])/).map(s => s.trim()).filter(Boolean);
    const releasesList   = (l.Releases  || '').split(/,(?![^"]*"\s*[,$])/).map(s => s.trim()).filter(Boolean);
    const artistsSet = recsByLabel.get(l.Name) || new Set();
    // Parent + kind:
    // - If `Interested Party` matches the label name → it's the corporate self.
    // - Else use IP as parent.
    // - "Rocket Science LLC" is the user's own label group → indie.
    const parent = (!ip || ip === l.Name) ? 'Independent' : ip;
    const kind = /^(Sony|Universal|Warner|UMG|WMG)/i.test(parent) ? 'major' : 'indie';
    return {
      id: 'rs_lb_' + (l['Label ID'] || ('LB' + idx)),
      name: l.Name,
      code: (l['Label Code'] || l.Name.replace(/\W+/g,'').slice(0,4) || `LB${idx+1}`).toUpperCase(),
      labelCode: l['Label Code'] || '',
      pplId: l['PPL ID'] || '',
      dpid: l.DPID || '',
      isni: l.ISNI || '',
      controlled: l.Controlled === 'Yes',
      interestedParty: ip,
      parent,
      kind,
      releases: releasesList.length,
      artists: artistsSet.size,
      deals: l.Controlled === 'Yes' ? `Direct · ${parent === 'Independent' ? 'Self' : parent}` : 'Distribution · Worldwide',
      color: labelColor(l['Label ID'] || l.Name),
      __rs: true,
      __rsRaw: l,
    };
  });

  // ── 7. AGREEMENTS (Recording / Publishing / etc.) ──────────────────
  // Real RS contracts. Map "Agreement Type" → app's `kind` taxonomy that
  // AgreementsView, agreement-detail.jsx, and audit-log.jsx all read.
  const rsRaw = window.RS.raw || {};
  const partiesByAg = {};
  for (const p of (rsRaw.agreement_party || [])) {
    const aid = p.Agreement; if (!aid) continue;
    (partiesByAg[aid] ||= []).push(p);
  }
  const territoriesByAg = {};
  for (const t of (rsRaw.agreement_territory || [])) {
    const aid = t.Agreement; if (!aid) continue;
    (territoriesByAg[aid] ||= []).push(t);
  }
  // RS Type → app kind label
  function mapAgreementKind(t) {
    if (!t) return 'Agreement';
    const s = t.toLowerCase();
    if (s.includes('record')) return 'Recording';
    if (s.includes('publish')) return 'Publishing · Admin';
    if (s.includes('admin')) return 'Publishing · Admin';
    if (s.includes('co-pub') || s.includes('copub')) return 'Publishing · Co-Pub';
    if (s.includes('writer')) return 'Songwriter Agreement';
    if (s.includes('master')) return 'Master Use · License';
    if (s.includes('sync')) return 'Sync · License';
    if (s.includes('distribution')) return 'Distribution Agreement';
    return t;
  }
  function mapAgreementStatus(s) {
    if (!s) return 'pending';
    const v = s.toLowerCase();
    if (v.includes('renew')) return 'live';     // "Renewed" → currently live
    if (v.includes('active') || v.includes('live')) return 'live';
    if (v === 'pending' || v.includes('draft')) return 'pending';
    if (v.includes('expir') || v === 'ended' || v.includes('terminat')) return 'expired';
    return 'live';
  }
  function fmtTerritories(rows) {
    if (!rows || !rows.length) return 'World';
    const inc = rows.filter(r => r['Inclusion/Exclusion'] === 'Inclusion').map(r => r.Territory);
    const exc = rows.filter(r => r['Inclusion/Exclusion'] === 'Exclusion').map(r => r.Territory);
    if (inc.length === 1 && inc[0] === 'World' && !exc.length) return 'World';
    if (inc.includes('World') && exc.length) return `WW ex. ${exc.slice(0,3).join(', ')}`;
    if (inc.length) return inc.slice(0, 3).join(', ') + (inc.length > 3 ? `, +${inc.length-3}` : '');
    return 'World';
  }
  // Term math — RS gives "Term" (years) and start date; compute ‘5y’ style.
  function termLabel(a) {
    const yrs = parseInt(a.Term);
    if (yrs && !isNaN(yrs)) return `${yrs}y`;
    if (a['Agreement Start Date'] && a['Expiration Date']) {
      const s = new Date(a['Agreement Start Date']).getFullYear();
      const e = new Date(a['Expiration Date']).getFullYear();
      if (e > s) return `${e-s}y`;
    }
    return '—';
  }
  // Compute the dominant share string for the parties list (e.g. "80/20 net").
  function shareLabel(parties) {
    if (!parties || !parties.length) return '—';
    const shares = parties.map(p => parseFloat(p.Share)).filter(n => !isNaN(n));
    if (!shares.length) return '—';
    if (shares.length === 1) return `${Math.round(shares[0])}%`;
    const isNet = parties.some(p => (p['Net/Gross']||'').toLowerCase() === 'net');
    return shares.map(s => Math.round(s)).join('/') + (isNet ? ' net' : '');
  }
  const rsAgreements = (window.RS.agreements || []).map(a => {
    const id = a['Agreement ID'];
    const parties = partiesByAg[id] || [];
    const territories = territoriesByAg[id] || [];
    // For 2-party contracts: a + b are the labels for the header
    const lic = parties.find(p => /licen/i.test(p['Party Role']));
    const oth = parties.find(p => p !== lic) || parties[1] || parties[0];
    return {
      id,
      kind: mapAgreementKind(a['Agreement Type'] || a.Type),
      a: lic?.['Interested Party'] || parties[0]?.['Interested Party'] || '—',
      b: oth?.['Interested Party'] || parties[1]?.['Interested Party'] || '—',
      territory: fmtTerritories(territories),
      share: shareLabel(parties),
      start: a['Agreement Start Date'] || '',
      end: a['Expiration Date'] || a['Agreement End Date'] || '',
      term: termLabel(a),
      status: mapAgreementStatus(a.Status),
      value: '',
      typeCwr: '',
      refNumber: id,
      societyAgreementNumber: a['Society-assigned Agreement Number'] || null,
      autoRenew: (a['Auto Renewal']||'').toLowerCase() === 'checked',
      renewNoticeMonths: parseInt(a['Non-Renewal Notice Period']) ? Math.round(parseInt(a['Non-Renewal Notice Period'])/30) : null,
      retentionEndDate: a['Retention End Date'] || null,
      priorRoyaltyStatus: a['Prior Royalty Status'] || 'N',
      sharesCanChange: (a['Shares Change']||'').toLowerCase() === 'yes',
      advanceGiven: !!a['Advance'],
      salesOrManufacture: a['S/M Clause'] || null,
      jurisdiction: '',
      disputeResolution: '',
      parties: parties.map(p => ({
        role: p['Party Role'] || 'Party',
        name: p['Interested Party'] || '—',
        kind: /writer|composer/i.test(p['Party Role']) ? 'writer' :
              /publish/i.test(p['Party Role']) ? 'publisher' :
              /artist|performer/i.test(p['Party Role']) ? 'artist' : 'company',
        ipi: '',
        share: parseFloat(p.Share) || 0,
        isControlled: /^Rocket Science/i.test(p['Interested Party']||''),
      })),
      territories: territories.map(t => ({
        code: t.Territory === 'World' ? 'WW' : t.Territory,
        label: t.Territory,
        rights: t['Inclusion/Exclusion'] === 'Exclusion' ? 'Excluded' : 'Included',
        collection: 100, ownership: 100, excluded: [],
      })),
      works: [],          // RS agreements link releases more than works directly
      versions: [{ n:1, date: a['Agreement Start Date']||'', author:'Rocket Science', note:'Imported · Airtable', current:true }],
      signatures: parties.filter(p => p.Signee).map(p => ({
        party: p['Interested Party'] || '—',
        signer: p.Signee || null,
        role: p['Party Role'] || null,
        method: 'Wet ink',
        at: a['Agreement Start Date'] || null,
        verified: true,
      })),
      assets: [],
      advanceSchedule: [],
      audit: [
        { at: (a['Agreement Start Date']||'') + 'T00:00:00Z', who: 'System', what: `${mapAgreementKind(a['Agreement Type'] || a.Type)} executed` },
      ],
      releases: (a.Releases || '').split(/\s*,\s*/).filter(Boolean).slice(0, 8),
      __rs: true,
      __rsRaw: a,
    };
  });

  // ── Merge into globals ───────────────────────────────────────────
  // Strategy: prepend RS rows so they appear first in lists, then keep
  // the original demo rows for screens that haven't been audited yet.
  // IMPORTANT: existing demo arrays (LABELS, ARTISTS, PUBLISHERS) are `const` —
  // we must mutate in place via splice() so the view components see the changes.
  function mergePrepend(globalKey, newRows) {
    const existing = window[globalKey];
    if (Array.isArray(existing)) {
      // Drop any prior RS rows (idempotent) — splice in place so const refs stay valid.
      for (let i = existing.length - 1; i >= 0; i--) {
        if (existing[i] && existing[i].__rs) existing.splice(i, 1);
      }
      // Prepend the new RS rows.
      existing.unshift(...newRows);
    } else {
      window[globalKey] = newRows.slice();
    }
  }

  mergePrepend('WORKS', rsWorks);
  mergePrepend('ALL_WORKS', rsWorks); // ALL_WORKS is what songs.jsx exposes
  mergePrepend('RECORDINGS', rsRecordings);
  mergePrepend('RECORDING_GRAPH', rsRecordings);
  mergePrepend('RELEASES', rsReleases);
  mergePrepend('RELEASES_X', rsReleasesX);
  mergePrepend('RELEASE_GROUPS', rsReleaseGroups);
  mergePrepend('PUBLISHERS', rsPublishers);
  mergePrepend('PROFILES', rsProfiles);
  // ARTISTS is the global the Profiles grid reads — merge profiles into it too.
  mergePrepend('ARTISTS', rsProfiles);
  if (typeof window !== 'undefined' && window.__PUBLISHERS) {
    // Keep the legacy alias in sync (PublishersView reads via __PUBLISHERS fallback).
    window.__PUBLISHERS = window.PUBLISHERS;
  }
  // LABELS — Airtable export is the canonical RS labels list.
  mergePrepend('LABELS', rsLabels);
  // AGREEMENTS — real RS contracts (Recording, Publishing · Admin, etc.)
  mergePrepend('AGREEMENTS', rsAgreements);

  // Rebuild lookup maps that songs.jsx pre-computed
  if (window.WORK_BY_ID) {
    rsWorks.forEach(w => { window.WORK_BY_ID[w.id] = w; });
  }
  if (window.REC_BY_ID) {
    rsRecordings.forEach(r => { window.REC_BY_ID[r.id] = r; });
  }
  if (window.REL_BY_ID) {
    rsReleasesX.forEach(r => { window.REL_BY_ID[r.id] = r; });
  }
  if (window.RG_BY_ID) {
    rsReleaseGroups.forEach(g => { window.RG_BY_ID[g.id] = g; });
  }

  // Update CATALOG_STATS counts so dashboard shows the real totals
  window.CATALOG_STATS = window.CATALOG_STATS || {};
  window.CATALOG_STATS.works      = { total: (window.WORKS || []).length };
  window.CATALOG_STATS.recordings = { total: (window.RECORDINGS || []).length };
  window.CATALOG_STATS.releases   = { total: (window.RELEASE_GROUPS || []).length };
  window.CATALOG_STATS.publishers = { total: (window.PUBLISHERS || []).length };
  window.CATALOG_STATS.profiles   = { total: (window.PROFILES || []).length };
  window.CATALOG_STATS.agreements = { total: (window.AGREEMENTS || []).length };
  // Directory tab counts — drives the DIRECTORY · N PROFILES · N PUBLISHERS · N LABELS · N SOCIETIES ribbon.
  window.CATALOG_STATS.directory  = {
    profiles:   (window.ARTISTS || []).length,
    publishers: (window.PUBLISHERS || []).length,
    labels:     (window.LABELS || []).length,
  };

  console.log('[rs-bridge] merged', {
    works: rsWorks.length,
    recordings: rsRecordings.length,
    releases: rsReleases.length,
    publishers: rsPublishers.length,
    profiles: rsProfiles.length,
    labels: rsLabels.length,
    agreements: rsAgreements.length,
    total_works: window.WORKS.length,
    total_recordings: window.RECORDINGS?.length || 0,
    total_agreements: window.AGREEMENTS?.length || 0,
  });
})();
