/* global React, Ic */
// party-indicator.jsx — single source of truth for the ★ ◆ ● Party indicators.
//
// Indicators are NEVER stored on records. They are recomputed from
// window.AGREEMENTS on every render. If the agreement layer doesn't say a
// party is owned/administered/represented, the party has no indicator.
//
//   ★  owned        — Rocket Science (or one of its imprints) IS the party
//                     itself, OR controls 100% of it via an admin/sub-pub deal
//                     that names them as Original Publisher.
//   ◆  administered — an active Publishing · Admin / Sub-publishing · Admin
//                     names the party as Original Publisher and a controlled
//                     RS imprint as the Administrator/Sub-Publisher.
//   ●  represented  — an active Management / Distribution / Songwriter
//                     agreement ties the party to a controlled imprint.
//
// Highest-power kind wins for the list view. The detail view shows the full
// list with sources so users can see WHY a party is flagged.

(function () {
  // ─────────────────────────── canonical "owned roots"
  // Names (case-insensitive) that ARE Rocket Science or one of its
  // Owned roots: parties whose `claimed_by_account_id` IS your own account, plus their
  // wholly-owned imprints. Anyone listed as a party here is owned outright
  // — no agreement needed. Seeded from the audit doc.
  //
  // Two layers:
  //   1. Hardcoded fallback for names that may not appear in any directory.
  //   2. Runtime augmentation from window.__RS_DIRECTORY (publishers + labels)
  //      where Controlled === 'Yes' AND Interested Party rolls up to a controlled root.
  //      This catches the 8 RS imprints (Rocktscience Music Publishing, Uroyan Publishing,
  //      Worthless Melodies, De Respeto Publishing, Not Rocket Science Publishing,
  //      Rocket Science Tunes, Rocket Science Songs UK, Rocket Science Songs Europe)
  //      without us having to list them by hand.
  const OWNED_ROOTS_SEED = [
    'rocket science',
    'rocket science llc',
    'rocket science group',
    'rocket science management',
    'rocket science music publishing group',
    'rocket science music publishing group llc',
    'rocket science music publishing',
    'rocket science publishing',
    'rsmpg',
    'macromanagement',
    'macromanagement llc',
    'pluralis music',
    'pluralis music ltd.',
    'pluralis music ltd',
    'pluralpub',
    'pluralpub ltd.',
    'pluralpub ltd',
    'kay publishing llc',
    'kay publishing',
    'saint music',
    'saint music gmbh',
  ];
  const OWNED_ROOTS = new Set(OWNED_ROOTS_SEED);
  // Augment from the RS directory at runtime — anything where Controlled === 'Yes'
  // and Interested Party rolls up to a controlled root we already know about.
  function augmentFromDirectory() {
    const dir = window.__RS_DIRECTORY;
    if (!dir) return;
    const lists = [dir.publishers, dir.labels].filter(Array.isArray);
    for (const arr of lists) {
      for (const row of arr) {
        if (row.Controlled !== 'Yes') continue;
        const name = (row.Name || '').trim().toLowerCase();
        if (!name) continue;
        const ip = (row['Interested Party'] || '').trim().toLowerCase();
        // If the row is itself a known controlled root, OR its Interested Party
        // resolves to one — promote the row name into OWNED_ROOTS.
        if (OWNED_ROOTS.has(name) || OWNED_ROOTS.has(ip)) {
          OWNED_ROOTS.add(name);
        }
      }
    }
    // Two-pass — second pass catches imprints whose Interested Party was
    // newly added in the first pass.
    for (const arr of lists) {
      for (const row of arr) {
        if (row.Controlled !== 'Yes') continue;
        const name = (row.Name || '').trim().toLowerCase();
        const ip = (row['Interested Party'] || '').trim().toLowerCase();
        if (name && OWNED_ROOTS.has(ip)) OWNED_ROOTS.add(name);
      }
    }
  }
  augmentFromDirectory();
  // Re-run if directory loads after this script.
  if (typeof window !== 'undefined') {
    window.addEventListener('astro-rs-directory-ready', augmentFromDirectory);
  }
  window.__PARTY_OWNED_ROOTS = OWNED_ROOTS;

  function norm(s) { return (s || '').trim().toLowerCase(); }

  function isOwnedRoot(party) {
    if (!party) return false;
    const n = norm(party.name || party);
    return OWNED_ROOTS.has(n);
  }

  // Active = status==='active'/'live' AND today is within [start, end].
  // Date math is forgiving — missing dates count as open-ended.
  function isActiveOn(ag, today) {
    const s = (ag.status || '').toLowerCase();
    if (s !== 'active' && s !== 'live') return false;
    const t = today.getTime();
    if (ag.start) {
      const st = new Date(ag.start).getTime();
      if (!isNaN(st) && t < st) return false;
    }
    if (ag.end) {
      const en = new Date(ag.end).getTime();
      if (!isNaN(en) && t > en) return false;
    }
    return true;
  }

  // Map agreement kind → which indicator it CAN produce when the party is on
  // the receiving side and a controlled RS imprint is on the giving side.
  // Returns 'owned' | 'administered' | 'represented' | null.
  function indicatorForAgreement(ag) {
    const k = (ag.kind || '').toLowerCase();
    if (k.includes('co-pub') || k.includes('copub')) return 'owned';
    if (k.includes('admin')) return 'administered';
    if (k.includes('sub-publish')) return 'administered';
    if (k.includes('songwriter')) return 'owned'; // writer's catalog assigned to RS imprint
    if (k.includes('management')) return 'represented';
    if (k.includes('distribution')) return 'represented';
    if (k.includes('consulting')) return 'represented';
    return null;
  }

  // Power ranking: owned > administered > represented
  const POWER = { owned: 3, administered: 2, represented: 1 };

  // For an agreement, which named party is the "subject" (the one the
  // indicator describes) vs the controlled RS side? Returns
  // { subject, controller } or null if neither side is a controlled imprint.
  function rolesOf(ag) {
    const ps = ag.parties || [];
    if (!ps.length) return null;
    const controlled = ps.find(p => p.isControlled || isOwnedRoot(p));
    if (!controlled) return null;
    const subject = ps.find(p => p !== controlled);
    if (!subject) return null;
    return { subject, controller: controlled };
  }

  // ─────────────────────────── public derivation
  // Returns { kind, sources } where kind is the highest-power tag and
  // sources is the list of agreement ids that contributed (any kind).
  function getPartyIndicator(party, opts) {
    if (!party) return { kind: null, sources: [] };
    if (isOwnedRoot(party)) {
      return { kind: 'owned', sources: ['__owned_root__'] };
    }
    const today = (opts && opts.today) ? new Date(opts.today) : new Date();
    const ags = (window.AGREEMENTS || []).filter(a => isActiveOn(a, today));
    const targetName = norm(party.name || party);
    const targetIpi = (party.cae || party.ipi || '').replace(/\D/g, '');

    let best = null;
    const sources = [];
    for (const ag of ags) {
      const roles = rolesOf(ag);
      if (!roles) continue;
      const subjName = norm(roles.subject.name);
      const subjIpi = (roles.subject.ipi || '').replace(/\D/g, '');
      const matches =
        (subjName && subjName === targetName) ||
        (targetIpi && subjIpi && subjIpi === targetIpi);
      if (!matches) continue;
      const ind = indicatorForAgreement(ag);
      if (!ind) continue;
      sources.push({ id: ag.id, kind: ag.kind, indicator: ind, end: ag.end });
      if (!best || POWER[ind] > POWER[best]) best = ind;
    }
    return { kind: best, sources };
  }

  // ─────────────────────────── shared component
  // <PartyIndicator party detail={false}/>
  // - list/inline: shows the highest-power glyph; tooltip lists sources.
  // - detail={true}: shows ALL kinds the party qualifies for, comma-separated.
  const SYM = { owned: '★', administered: '◆', represented: '●' };
  const LABEL = {
    owned: 'Owned',
    administered: 'Administered',
    represented: 'Represented',
  };

  function PartyIndicator({ party, detail }) {
    const { kind, sources } = getPartyIndicator(party);
    if (!kind) return null;
    if (!detail) {
      const tip =
        LABEL[kind] +
        (sources.length && sources[0] !== '__owned_root__'
          ? ' · via ' + sources.map(s => s.id || s).join(', ')
          : ' · controlled imprint');
      return (
        <span
          title={tip}
          className="ff-mono"
          style={{
            display: 'inline-block',
            fontSize: '0.8em',
            lineHeight: 1,
            color: 'var(--ink)',
            marginRight: 8,
            verticalAlign: 'baseline',
            fontWeight: 600,
          }}
        >
          {SYM[kind]}
        </span>
      );
    }
    // detail mode — list every distinct kind with source agreement chips
    const kinds = Array.from(new Set(sources.map(s => s.indicator || 'owned')));
    if (kind === 'owned' && !kinds.includes('owned')) kinds.unshift('owned');
    return (
      <div
        className="ff-mono"
        style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.06em', lineHeight: 1.5 }}
      >
        {kinds.map((k, i) => (
          <span key={k} style={{ marginRight: 12 }}>
            <span style={{ color: 'var(--ink)', fontSize: 13, marginRight: 4 }}>{SYM[k]}</span>
            {LABEL[k].toUpperCase()}
            {k === 'owned' && sources[0] === '__owned_root__' ? ' · CONTROLLED IMPRINT' : ''}
          </span>
        ))}
        {sources.length > 0 && sources[0] !== '__owned_root__' && (
          <div style={{ marginTop: 4, color: 'var(--ink-4)' }}>
            VIA {sources.map(s => s.id).join(' · ')}
          </div>
        )}
      </div>
    );
  }

  window.getPartyIndicator = getPartyIndicator;
  window.PartyIndicator = PartyIndicator;

  // ─────────────────────────── share.controlled — DERIVED, never stored
  // Spec (Data Model 4.1):
  //   share.controlled = (share.publisher.indicator ∈ {★ Owned, ◆ Administered})
  //
  // We accept either a share record (with a `publisher` sub-object or `name`
  // field) or a (share, publisher-resolver) pair. Returns:
  //   { controlled: boolean,
  //     reason: 'owned' | 'administered' | 'no-indicator' | 'no-publisher',
  //     publisher, indicatorKind, sources }
  //
  // Stored `share.controlled` flags from CSV imports are IGNORED on read —
  // they're treated as a UI hint for in-progress drafts only. Drift between
  // stored flag and derived value is exactly what the Conformance check
  // `controlled_drift` is designed to surface.
  function getShareControlled(share) {
    if (!share) return { controlled: false, reason: 'no-publisher' };
    // Locate the publisher object on the share. Common shapes:
    //   {publisher: {name, ipi}, ...}
    //   {name, ipi, ...}                 (the share IS the publisher row)
    //   {Publisher: 'name string', ...}  (RS CSV shape)
    let pub = share.publisher;
    if (!pub) {
      if (share.name || share.ipi || share.cae) pub = share;
      else if (share.Publisher) pub = { name: share.Publisher };
    }
    if (!pub || !(pub.name || pub.ipi)) {
      return { controlled: false, reason: 'no-publisher' };
    }
    const ind = getPartyIndicator(pub);
    if (ind.kind === 'owned' || ind.kind === 'administered') {
      return {
        controlled: true,
        reason: ind.kind,
        publisher: pub,
        indicatorKind: ind.kind,
        sources: ind.sources,
      };
    }
    return {
      controlled: false,
      reason: 'no-indicator',
      publisher: pub,
      indicatorKind: ind.kind,
      sources: ind.sources,
    };
  }

  // <ControlledLabel share /> — small "Controlled" / "Non-controlled" pill
  // with a tooltip explaining the derivation chain. Replaces the old
  // CTRL / Controlled plain text everywhere.
  function ControlledLabel({ share, compact }) {
    const r = getShareControlled(share);
    if (r.reason === 'no-publisher') return null;
    const tip =
      r.controlled
        ? `Controlled · publisher ${r.publisher.name} is ${LABEL[r.indicatorKind]}` +
          (r.sources && r.sources[0] && r.sources[0] !== '__owned_root__'
            ? ' via ' + r.sources.map(s => s.id || s).join(', ')
            : ' (controlled imprint)')
        : `Non-controlled · publisher ${r.publisher.name} has no active ★/◆ indicator`;
    if (compact) {
      return (
        <span
          title={tip}
          className="ff-mono upper"
          style={{
            fontSize: 9,
            letterSpacing: '.08em',
            padding: '1px 5px',
            background: r.controlled ? 'var(--ink)' : 'transparent',
            color: r.controlled ? 'var(--bg)' : 'var(--ink-3)',
            border: r.controlled ? '1px solid var(--ink)' : '1px solid var(--rule)',
            fontWeight: 600,
          }}
        >
          {r.controlled ? 'CTRL' : 'NON-CTRL'}
        </span>
      );
    }
    return (
      <span
        title={tip}
        className="ff-mono upper"
        style={{
          fontSize: 10,
          letterSpacing: '.1em',
          padding: '2px 7px',
          background: r.controlled ? 'var(--ink)' : 'transparent',
          color: r.controlled ? 'var(--bg)' : 'var(--ink-3)',
          border: r.controlled ? '1px solid var(--ink)' : '1px solid var(--rule)',
          fontWeight: 600,
        }}
      >
        {r.controlled ? 'CONTROLLED' : 'NON-CONTROLLED'}
      </span>
    );
  }

  // Aggregate helper for Work / Recording headers.
  // Pass an array of { publisher, share (% number) } objects.
  // Returns { controlledPct, nonControlledPct, total }.
  function getControlledAggregate(shares) {
    let controlled = 0, total = 0;
    for (const s of (shares || [])) {
      const pct = Number(s.share || s.percent || 0);
      if (!pct) continue;
      total += pct;
      if (getShareControlled(s).controlled) controlled += pct;
    }
    if (!total) return { controlledPct: 0, nonControlledPct: 0, total: 0 };
    return {
      controlledPct: Math.round((controlled / total) * 100),
      nonControlledPct: Math.round(((total - controlled) / total) * 100),
      total,
    };
  }

  window.getShareControlled = getShareControlled;
  window.ControlledLabel = ControlledLabel;
  window.getControlledAggregate = getControlledAggregate;
})();
