// pro-lookup-engine.jsx — PRO repertoire lookup + cross-society conflict detector
// ─────────────────────────────────────────────────────────────────────
// In production this would proxy to each society's public-search endpoint:
//
//   ASCAP   ACE Repertory     https://www.ascap.com/repertory
//   BMI     Repertoire Search https://repertoire.bmi.com
//   SESAC   Repertory         https://www.sesac.com/repertory
//   GMR     (request only)
//   PRS     PRS Online        https://www.prsformusic.com/works/work-search
//   SACEM   SACEM Œuvres      https://repertoire.sacem.fr
//   GEMA    Online-Werkdatenb. https://online.gema.de
//   SIAE    OLAF              https://www.siae.it
//   JASRAC  J-WID             https://www2.jasrac.or.jp/eJwid
//   SOCAN   SOCAN Repertoire  https://www.socan.com/repertoire
//   APRA    AMCOS Search      https://www.apraamcos.com.au/search
//   MLC     Public Search     https://portal.themlc.com
//   ISWCNet ISWC Lookup       https://www.iswcnet.cisac.org
//   MusicBrainz Work search   https://musicbrainz.org/ws/2/work
//
// Since the prototype runs offline, this module ships *deterministic*
// stubs that produce believable, varied responses per work — including
// realistic conflicts so the conflict UI has something to chew on.
// The adapter shape is the same one a real backend would honor:
//
//   adapter(query) → Promise<NormalizedHit[]>
//   NormalizedHit = {
//     society, societyWorkId, title, alternateTitles[],
//     iswc, language, registeredAt, status,
//     writers: [{ name, role, ipi, share }],
//     publishers: [{ name, role, ipi, share, parent }],
//     territories[], notes
//   }
//
// Conflict detector takes [hits per society] and emits a flat list of
// findings tagged by kind + severity + suggested action.
// ─────────────────────────────────────────────────────────────────────

(function () {
  'use strict';
  if (typeof window === 'undefined') return;

  // ─── society catalog ─────────────────────────────────────────────
  const SOCIETIES = [
    { code:'ASCAP',  name:'ASCAP',                     country:'US', kind:'PRO',  scope:'PR' },
    { code:'BMI',    name:'BMI',                       country:'US', kind:'PRO',  scope:'PR' },
    { code:'SESAC',  name:'SESAC',                     country:'US', kind:'PRO',  scope:'PR' },
    { code:'GMR',    name:'Global Music Rights',       country:'US', kind:'PRO',  scope:'PR' },
    { code:'MLC',    name:'The MLC',                   country:'US', kind:'MRO',  scope:'MR' },
    { code:'PRS',    name:'PRS for Music',             country:'GB', kind:'PRO',  scope:'PR' },
    { code:'SACEM',  name:'SACEM',                     country:'FR', kind:'MIX',  scope:'PR/MR' },
    { code:'GEMA',   name:'GEMA',                      country:'DE', kind:'MIX',  scope:'PR/MR' },
    { code:'SIAE',   name:'SIAE',                      country:'IT', kind:'MIX',  scope:'PR/MR' },
    { code:'JASRAC', name:'JASRAC',                    country:'JP', kind:'MIX',  scope:'PR/MR' },
    { code:'SOCAN',  name:'SOCAN',                     country:'CA', kind:'PRO',  scope:'PR' },
    { code:'APRA',   name:'APRA AMCOS',                country:'AU', kind:'MIX',  scope:'PR/MR' },
    { code:'ISWCNet',name:'ISWCNet · CISAC',           country:'XX', kind:'REG',  scope:'ID' },
    { code:'MB',     name:'MusicBrainz',               country:'XX', kind:'OPEN', scope:'ID' },
  ];

  // ─── deterministic helpers — seeded by query string ──────────────
  function hash32(s) {
    let h = 2166136261 >>> 0;
    for (let i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = Math.imul(h, 16777619) >>> 0;
    }
    return h;
  }
  function rng(seed) {
    let x = seed || 1;
    return () => {
      x ^= x << 13; x >>>= 0;
      x ^= x >>> 17; x >>>= 0;
      x ^= x << 5;  x >>>= 0;
      return (x % 1000000) / 1000000;
    };
  }
  const pick = (r, arr) => arr[Math.floor(r() * arr.length)];

  // ─── canonical work synthesis ────────────────────────────────────
  // For a given query, we synthesize ONE canonical truth, then each
  // society's adapter lightly mutates it to introduce realistic drift.
  function canonicalize(query) {
    const seed = hash32(String(query.title || '') + '|' + String(query.artist || ''));
    const r = rng(seed);

    const writerNames = [
      'Max Martin','Ester Dean','Greg Kurstin','Justin Tranter','Julia Michaels',
      'Sia Furler','Ali Tamposi','Andrew Watt','Dua Lipa','Caroline Ailin',
      'Bruno Mars','Philip Lawrence','Sarah Aarons','Stephen Kozmeniuk','Bonnie McKee',
      'Joel Little','Jack Antonoff','Finneas O\'Connell','Billie Eilish','Tobias Jesso Jr.',
    ];
    const pubNames = [
      'MXM Music','Kobalt','Universal Music Publishing','Sony/ATV','Warner Chappell',
      'BMG Rights','Concord Music Publishing','Reservoir','peermusic','Downtown Music',
      'These Are Songs of Pulse','Big Deal Music','Pulse Music','Hipgnosis Songs',
    ];
    const langs = ['EN','EN','EN','ES','FR','DE','IT','JA','PT','SV'];

    const writerCount = 2 + Math.floor(r() * 4); // 2–5 writers
    const writers = [];
    let remaining = 100;
    for (let i = 0; i < writerCount; i++) {
      const isLast = i === writerCount - 1;
      const share = isLast ? remaining : Math.max(5, Math.round(remaining / (writerCount - i) * (0.7 + r() * 0.6)));
      const final = Math.min(share, remaining);
      remaining -= final;
      const name = writerNames[(seed + i * 17) % writerNames.length];
      writers.push({
        name,
        role: i === 0 ? 'CA' : (r() < 0.3 ? 'C' : (r() < 0.5 ? 'A' : 'CA')),
        ipi: String(100000000 + ((seed * (i + 7)) % 899999999)),
        share: final,
      });
    }
    if (remaining > 0) writers[0].share += remaining;

    // publisher chain: each writer has a publisher with same share
    const publishers = writers.map((w, i) => ({
      name: pubNames[(seed + i * 23) % pubNames.length],
      role: 'OP',
      ipi: String(200000000 + ((seed * (i + 13)) % 799999999)),
      share: w.share,
      writerLink: w.name,
    }));

    const iswcDigits = String((seed % 999999999) + 100000000).padStart(9, '0');
    const iswc = `T-${iswcDigits.slice(0,3)}.${iswcDigits.slice(3,6)}.${iswcDigits.slice(6,9)}-${(seed % 10)}`;

    return {
      title: query.title || 'Untitled',
      iswc,
      language: langs[seed % langs.length],
      registeredAt: new Date(2015 + (seed % 9), seed % 12, 1 + (seed % 27)).toISOString().slice(0,10),
      writers,
      publishers,
      seed,
    };
  }

  // ─── per-society adapter mutators ────────────────────────────────
  // Each adapter introduces realistic society-specific drift:
  //   • title spelling variants
  //   • missing/different ISWC
  //   • IPI differences (same writer, different IPI Name #)
  //   • split percentage drift
  //   • missing writers (party not registered there)
  //   • duplicate registrations
  //   • registration delay / status mismatch
  function mutateForSociety(canon, society, r) {
    const probHasWork = society.code === 'MB' ? 0.85
                      : society.code === 'ISWCNet' ? 0.95
                      : society.code === 'GMR' ? 0.18
                      : society.code === 'SESAC' ? 0.35
                      : 0.7 + r() * 0.2;
    if (r() > probHasWork) return []; // no registration

    const writers = canon.writers.map(w => ({ ...w }));
    const publishers = canon.publishers.map(p => ({ ...p }));

    // 25% chance of title variant
    let title = canon.title;
    const altTitles = [];
    if (r() < 0.25) {
      const variants = [
        title.toUpperCase(),
        title.replace(/'/g, ''),
        title + ' (Original Mix)',
        title.split(' ').slice(0, -1).join(' '),
      ];
      altTitles.push(variants[Math.floor(r() * variants.length)]);
    }

    // 15% chance ISWC missing at this society
    let iswc = canon.iswc;
    if (r() < 0.15) iswc = null;

    // 20% chance one writer is missing here
    if (writers.length > 2 && r() < 0.20) {
      const idx = Math.floor(r() * writers.length);
      const removed = writers.splice(idx, 1)[0];
      // redistribute share to first writer
      writers[0].share += removed.share;
      // also remove their publisher
      const pi = publishers.findIndex(p => p.writerLink === removed.name);
      if (pi >= 0) publishers.splice(pi, 1);
    }

    // 30% chance of IPI drift (same writer, different IPI Name # vs IPI Base)
    if (r() < 0.30) {
      const w = writers[Math.floor(r() * writers.length)];
      if (w) w.ipi = String(parseInt(w.ipi) + 1);
    }

    // 12% chance share drift (rounding error)
    if (r() < 0.12 && writers.length >= 2) {
      writers[0].share = Math.max(0, writers[0].share - 1);
      writers[1].share = writers[1].share + 1;
    }

    // 8% chance shares don't sum to 100
    if (r() < 0.08) {
      writers[0].share = Math.max(0, writers[0].share - 5); // 95% total
    }

    // 5% chance of duplicate registration at this society
    const hits = [];
    const baseHit = {
      society: society.code,
      societyWorkId: society.code + '-' + (canon.seed % 9999999).toString().padStart(7, '0'),
      title,
      alternateTitles: altTitles,
      iswc,
      language: canon.language,
      registeredAt: canon.registeredAt,
      status: r() < 0.05 ? 'PENDING' : (r() < 0.03 ? 'DISPUTED' : 'REGISTERED'),
      writers,
      publishers,
      territories: society.country === 'XX' ? ['World'] : [society.country],
      raw: { source: society.code, demoMode: true },
    };
    hits.push(baseHit);
    if (r() < 0.05) {
      // duplicate with a different ID
      hits.push({
        ...baseHit,
        societyWorkId: society.code + '-' + ((canon.seed + 7) % 9999999).toString().padStart(7, '0'),
        registeredAt: new Date(Date.parse(canon.registeredAt) + 1000 * 60 * 60 * 24 * 90).toISOString().slice(0,10),
        notes: 'Possible duplicate registration',
      });
    }
    return hits;
  }

  // ─── adapter constructor ─────────────────────────────────────────
  function buildAdapter(society) {
    return async function lookup(query) {
      const canon = canonicalize(query);
      const r = rng(canon.seed ^ hash32(society.code));
      // simulated network jitter
      const delay = 200 + Math.floor(r() * 900);
      await new Promise(res => setTimeout(res, delay));
      return mutateForSociety(canon, society, r);
    };
  }

  const ADAPTERS = Object.fromEntries(
    SOCIETIES.map(s => [s.code, { society: s, lookup: buildAdapter(s) }])
  );

  // ─── parallel query orchestrator ─────────────────────────────────
  async function queryAll(query, opts = {}) {
    const codes = opts.societies && opts.societies.length
      ? opts.societies
      : SOCIETIES.map(s => s.code);
    const start = Date.now();
    const results = await Promise.all(codes.map(async code => {
      const a = ADAPTERS[code];
      if (!a) return { society: code, hits: [], error: 'No adapter', ms: 0 };
      const t0 = Date.now();
      try {
        const hits = await a.lookup(query);
        return { society: code, societyName: a.society.name, country: a.society.country, kind: a.society.kind, hits, ms: Date.now() - t0 };
      } catch (e) {
        return { society: code, societyName: a.society.name, country: a.society.country, kind: a.society.kind, hits: [], error: String(e.message || e), ms: Date.now() - t0 };
      }
    }));
    return { query, results, totalMs: Date.now() - start, queriedAt: new Date().toISOString() };
  }

  // ─── conflict detector ───────────────────────────────────────────
  // Given the query bundle, emit a flat list of findings:
  //   { kind, severity, society|societies, message, details, suggested }
  //
  // Severities:
  //   'critical' — hard blocker (duplicate, dispute, missing writer)
  //   'high'     — needs reconciliation (IPI mismatch, share total)
  //   'medium'   — drift / hygiene (title variant, ISWC missing)
  //   'info'     — observation (registration gap, status)
  function detectConflicts(bundle, internal) {
    const findings = [];
    const allHits = [];
    for (const r of bundle.results) for (const h of r.hits) allHits.push(h);
    if (allHits.length === 0) {
      findings.push({
        id: 'no-registration',
        kind: 'no-registration',
        severity: 'info',
        societies: [],
        message: 'Work not found at any queried society.',
        details: 'Verify the title/artist or register the work.',
        suggested: { action: 'register-cwr', label: 'Generate CWR' },
      });
      return findings;
    }

    // 1) ISWC mismatch / missing
    const iswcs = new Set(allHits.map(h => h.iswc).filter(Boolean));
    if (iswcs.size > 1) {
      findings.push({
        id: 'iswc-mismatch',
        kind: 'iswc-mismatch',
        severity: 'critical',
        societies: bundle.results.filter(r => r.hits.some(h => h.iswc)).map(r => r.society),
        message: `Conflicting ISWCs across societies (${iswcs.size} different values).`,
        details: Array.from(iswcs).join(' · '),
        suggested: { action: 'rev-iswc', label: 'Pick canonical & generate REV' },
      });
    }
    const missingIswc = bundle.results.filter(r => r.hits.length > 0 && r.hits.every(h => !h.iswc));
    if (missingIswc.length && iswcs.size === 1) {
      findings.push({
        id: 'iswc-missing',
        kind: 'iswc-missing',
        severity: 'high',
        societies: missingIswc.map(r => r.society),
        message: `ISWC missing at ${missingIswc.length} ${missingIswc.length === 1 ? 'society' : 'societies'}.`,
        details: `Canonical ISWC: ${Array.from(iswcs)[0]}`,
        suggested: { action: 'rev-iswc', label: 'Push ISWC via REV' },
      });
    }

    // 2) Title drift
    const titlesBySoc = bundle.results.flatMap(r => r.hits.map(h => ({ society: r.society, title: h.title })));
    const titleSet = new Set(titlesBySoc.map(x => x.title.toLowerCase().replace(/[^a-z0-9]/g, '')));
    if (titleSet.size > 1) {
      findings.push({
        id: 'title-drift',
        kind: 'title-drift',
        severity: 'medium',
        societies: Array.from(new Set(titlesBySoc.map(x => x.society))),
        message: `Title differs across ${titleSet.size} variants.`,
        details: titlesBySoc.map(x => `${x.society}: ${x.title}`).join(' · '),
        suggested: { action: 'normalize-title', label: 'Normalize title' },
      });
    }

    // 3) Writer disagreement
    const allWriters = new Map(); // name → { societies:Set, ipis:Set, totalShare:[] }
    for (const r of bundle.results) for (const h of r.hits) for (const w of h.writers) {
      if (!allWriters.has(w.name)) allWriters.set(w.name, { name: w.name, societies: new Set(), ipis: new Set(), shares: [] });
      const e = allWriters.get(w.name);
      e.societies.add(r.society);
      if (w.ipi) e.ipis.add(w.ipi);
      e.shares.push({ society: r.society, share: w.share });
    }
    const allSocsWithHits = bundle.results.filter(r => r.hits.length).map(r => r.society);
    for (const w of allWriters.values()) {
      const missingFrom = allSocsWithHits.filter(s => !w.societies.has(s));
      if (missingFrom.length && w.societies.size > 0) {
        findings.push({
          id: 'writer-missing-' + w.name,
          kind: 'writer-missing',
          severity: 'critical',
          societies: missingFrom,
          message: `${w.name} is not registered at ${missingFrom.join(', ')}.`,
          details: `Present at: ${Array.from(w.societies).join(', ')}`,
          suggested: { action: 'add-writer', label: 'Add writer via CWR' },
          writer: w.name,
        });
      }
      if (w.ipis.size > 1) {
        findings.push({
          id: 'ipi-mismatch-' + w.name,
          kind: 'ipi-mismatch',
          severity: 'high',
          societies: Array.from(w.societies),
          message: `${w.name}: ${w.ipis.size} different IPI numbers across societies.`,
          details: Array.from(w.ipis).join(' · '),
          suggested: { action: 'reconcile-ipi', label: 'Reconcile IPI' },
          writer: w.name,
        });
      }
      // share drift
      const distinctShares = new Set(w.shares.map(s => s.share));
      if (distinctShares.size > 1) {
        findings.push({
          id: 'share-drift-' + w.name,
          kind: 'share-drift',
          severity: 'high',
          societies: w.shares.map(s => s.society),
          message: `${w.name}: share % differs across societies.`,
          details: w.shares.map(s => `${s.society}: ${s.share}%`).join(' · '),
          suggested: { action: 'reconcile-shares', label: 'Reconcile shares' },
          writer: w.name,
        });
      }
    }

    // 4) Share total ≠ 100 at any society
    for (const r of bundle.results) for (const h of r.hits) {
      const total = h.writers.reduce((a, w) => a + (w.share || 0), 0);
      if (total !== 100) {
        findings.push({
          id: 'share-total-' + r.society + '-' + h.societyWorkId,
          kind: 'share-total',
          severity: 'critical',
          societies: [r.society],
          message: `${r.society}: writer shares total ${total}%, not 100%.`,
          details: h.writers.map(w => `${w.name} ${w.share}%`).join(' · '),
          suggested: { action: 'fix-shares', label: 'Open share editor' },
          societyWorkId: h.societyWorkId,
        });
      }
    }

    // 5) Duplicate registrations
    for (const r of bundle.results) {
      if (r.hits.length > 1) {
        findings.push({
          id: 'duplicate-' + r.society,
          kind: 'duplicate-reg',
          severity: 'critical',
          societies: [r.society],
          message: `${r.society} has ${r.hits.length} registrations for this work.`,
          details: r.hits.map(h => `${h.societyWorkId} · ${h.registeredAt}`).join(' · '),
          suggested: { action: 'merge-dupes', label: 'Open dispute · merge' },
        });
      }
    }

    // 6) Disputed / pending status
    for (const r of bundle.results) for (const h of r.hits) {
      if (h.status === 'DISPUTED') {
        findings.push({
          id: 'disputed-' + r.society + '-' + h.societyWorkId,
          kind: 'status-disputed',
          severity: 'critical',
          societies: [r.society],
          message: `${r.society}: registration is in DISPUTE.`,
          details: `Society work ID ${h.societyWorkId}`,
          suggested: { action: 'open-dispute', label: 'Review dispute' },
        });
      } else if (h.status === 'PENDING') {
        findings.push({
          id: 'pending-' + r.society + '-' + h.societyWorkId,
          kind: 'status-pending',
          severity: 'info',
          societies: [r.society],
          message: `${r.society}: registration pending.`,
          details: `Filed ${h.registeredAt}`,
          suggested: { action: 'wait', label: 'Watchlist' },
        });
      }
    }

    // 7) Internal mismatch — does the catalog version agree with PRO consensus?
    if (internal) {
      // ISWC
      if (internal.iswc && iswcs.size === 1) {
        const consensus = Array.from(iswcs)[0];
        if (consensus !== internal.iswc) {
          findings.push({
            id: 'internal-iswc',
            kind: 'internal-mismatch',
            severity: 'high',
            societies: ['INTERNAL'],
            message: `Internal ISWC (${internal.iswc}) differs from PRO consensus (${consensus}).`,
            details: 'Society records agree but the catalog disagrees.',
            suggested: { action: 'update-internal', label: 'Update catalog' },
          });
        }
      }
      // title
      if (internal.title) {
        const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
        const proTitles = new Set(allHits.map(h => norm(h.title)));
        if (proTitles.size === 1 && !proTitles.has(norm(internal.title))) {
          findings.push({
            id: 'internal-title',
            kind: 'internal-mismatch',
            severity: 'medium',
            societies: ['INTERNAL'],
            message: `Catalog title differs from PRO consensus.`,
            details: `Catalog: ${internal.title} · PROs: ${allHits[0].title}`,
            suggested: { action: 'update-internal', label: 'Update catalog' },
          });
        }
      }
    }

    // sort by severity
    const order = { critical: 0, high: 1, medium: 2, info: 3 };
    findings.sort((a, b) => order[a.severity] - order[b.severity]);
    return findings;
  }

  // ─── batch — query many works at once ────────────────────────────
  async function queryBatch(queries, opts = {}) {
    const out = [];
    for (const q of queries) {
      const bundle = await queryAll(q, opts);
      const findings = detectConflicts(bundle, q.internal);
      out.push({ query: q, bundle, findings });
    }
    return out;
  }

  // ─── consolidated record (best-of merge) ─────────────────────────
  // Used for "what should the canonical record look like"
  function consolidate(bundle) {
    const hits = bundle.results.flatMap(r => r.hits);
    if (!hits.length) return null;

    // most common ISWC
    const iswcCount = {};
    hits.forEach(h => h.iswc && (iswcCount[h.iswc] = (iswcCount[h.iswc] || 0) + 1));
    const iswc = Object.entries(iswcCount).sort((a, b) => b[1] - a[1])[0]?.[0] || null;

    // most common title
    const titleCount = {};
    hits.forEach(h => h.title && (titleCount[h.title] = (titleCount[h.title] || 0) + 1));
    const title = Object.entries(titleCount).sort((a, b) => b[1] - a[1])[0]?.[0] || null;

    // writer roster — union, with majority IPI
    const wMap = new Map();
    for (const h of hits) for (const w of h.writers) {
      if (!wMap.has(w.name)) wMap.set(w.name, { name: w.name, ipiCounts: {}, shareCounts: {}, count: 0 });
      const e = wMap.get(w.name);
      e.count++;
      if (w.ipi) e.ipiCounts[w.ipi] = (e.ipiCounts[w.ipi] || 0) + 1;
      e.shareCounts[w.share] = (e.shareCounts[w.share] || 0) + 1;
    }
    const writers = Array.from(wMap.values()).map(e => ({
      name: e.name,
      ipi: Object.entries(e.ipiCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || null,
      share: Number(Object.entries(e.shareCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 0),
      seenAt: e.count,
    }));

    return { iswc, title, writers };
  }

  // ─── exports ─────────────────────────────────────────────────────
  window.PROLookup = {
    SOCIETIES,
    ADAPTERS,
    queryAll,
    queryBatch,
    detectConflicts,
    consolidate,
    canonicalize, // exposed for tests/devtools
  };
  console.log('[pro-lookup-engine] loaded ·', SOCIETIES.length, 'adapters');
})();
