// ref-data.jsx — boot-time loader for the ref.* schema.
//
// Loads db/ref/all.json once on app start and hydrates window.REF.* with both
// raw arrays AND derived lookup maps. Also installs LEGACY ALIASES so existing
// code (window.SOC_WRITER, window.SOC_PUBLISHER, window.SOC_CWR, etc.)
// keeps working without edits — we incrementally migrate callers to window.REF.
//
// Shape: window.REF = {
//   ready: false → true,                       // flips when load completes
//   loadedAt: Date,                            // when load finished
//   tables: ['territories', 'societies', …],   // names of every table loaded
//   raw: { territories: [...], societies: [...], … },   // raw arrays, by table
//
//   // Convenience aliases (most common dropdowns)
//   territories,            // [{id, iso_alpha_2, common_name, emoji_flag, …}]
//   societies,              // [{id, acronym, full_name, country, cmo_type, …}]
//   writerRoles,            // [{id, code, label, cwr_code}]
//   publisherRoles,
//   agreementTypes,
//   currencies, languages, genres, workTypes, releaseTypes, recordingTypes,
//   identifierTypes,
//   …
//
//   // Lookup maps (build on demand-ish, cheap)
//   territoryByIso2: Map('US' → row),
//   societyByAcronym: Map('ASCAP' → row),
//   workTypeByCode: Map(...),
//   …
// }

(function bootRefData(){
  const REF = window.REF = window.REF || {
    ready: false,
    loadedAt: null,
    error: null,
    tables: [],
    raw: {},
  };

  // Tiny event so React components can re-render once data lands.
  const announceReady = () => {
    REF.ready = true;
    REF.loadedAt = new Date();
    window.dispatchEvent(new CustomEvent('astro-ref-ready', { detail: { tables: REF.tables.length } }));
  };

  const buildIndices = (raw) => {
    REF.raw = raw;
    REF.tables = Object.keys(raw).filter(k => Array.isArray(raw[k]));

    // Convenience aliases — keep keys camelCase, source of truth stays in raw.
    REF.territories            = raw.territories            || [];
    REF.territoryGroups        = raw.territory_groups       || [];
    REF.regions                = raw.regions                || [];
    REF.subregions             = raw.subregions             || [];
    REF.societies              = raw.societies              || [];
    REF.societyTypes           = raw.society_types          || [];
    REF.industryOrganizations  = raw.industry_organizations || [];
    REF.writerRoles            = raw.writer_roles           || [];
    REF.publisherRoles         = raw.publisher_roles        || [];
    REF.ownershipRoles         = raw.ownership_roles        || [];
    REF.agreementTypes         = raw.agreement_types        || [];
    REF.agreementStatuses      = raw.agreement_statuses     || [];
    REF.agreementPartyRoles    = raw.agreement_party_roles  || [];
    REF.rightsTypes            = raw.rights_types           || [];
    REF.royaltyTypes           = raw.royalty_types          || [];
    REF.royaltyPeriods         = raw.royalty_periods        || [];
    REF.revenueTypes           = raw.revenue_types          || [];
    REF.identifierTypes        = raw.identifier_types       || [];
    REF.aliasTypes             = raw.alias_types            || [];
    // Copyright registration domain — wired from astro.copyright_* ref tables.
    REF.copyrightTypes                = raw.copyright_types                  || [];
    REF.copyrightJurisdictions        = raw.copyright_jurisdictions          || [];
    REF.copyrightTitleTypes           = raw.copyright_title_types            || [];
    REF.copyrightTransferStatements   = raw.copyright_transfer_statements    || [];
    REF.copyrightRegistrationStatuses = raw.copyright_registration_statuses  || [];
    REF.copyrightIsnTypes             = raw.copyright_isn_types              || [];
    REF.currencies             = raw.currencies             || [];
    REF.languages              = raw.languages              || [];
    REF.genres                 = raw.genres                 || [];
    REF.genders                = raw.genders                || [];
    REF.artistTypes            = raw.artist_types           || [];
    REF.artistRoles            = raw.artist_roles           || [];
    REF.memberRoles            = raw.member_roles           || [];
    REF.creditCategories       = raw.credit_categories      || [];
    REF.creditRoles            = raw.credit_roles           || [];
    REF.workTypes              = raw.work_types             || [];
    REF.releaseTypes           = raw.release_types          || [];
    REF.configurations         = raw.configurations         || [];
    REF.channelConfigurations  = raw.channel_configurations || [];
    REF.explicitContentTypes   = raw.explicit_content_types || [];
    REF.recordingTypes         = raw.recording_types        || [];
    REF.musicalKeys            = raw.musical_keys           || [];
    REF.labelTypes             = raw.label_types            || [];
    REF.partyTypes             = raw.party_types            || [];
    REF.businessStructures     = raw.business_structures    || [];
    REF.externalLinkTypes      = raw.external_link_types    || [];
    REF.userRoles              = raw.user_roles             || [];
    REF.accountTypes           = raw.account_types          || [];

    // Lookup maps. Keep these as Maps for stable hit/miss semantics.
    REF.territoryByIso2     = new Map(REF.territories.map(t => [t.iso_alpha_2, t]).filter(([k])=>k));
    REF.territoryByIso3     = new Map(REF.territories.map(t => [t.iso_alpha_3, t]).filter(([k])=>k));
    REF.territoryById       = new Map(REF.territories.map(t => [t.id, t]));
    REF.societyByAcronym    = new Map(REF.societies.map(s => [s.acronym, s]));
    REF.societyById         = new Map(REF.societies.map(s => [s.id, s]));
    REF.societyByCisac      = new Map(REF.societies.map(s => [s.cisac_code, s]).filter(([k])=>k));
    REF.workTypeByCode      = new Map(REF.workTypes.map(t => [t.code, t]));
    REF.releaseTypeByCode   = new Map(REF.releaseTypes.map(t => [t.code, t]));
    REF.languageByIso1      = new Map(REF.languages.map(l => [l.iso_639_1, l]).filter(([k])=>k));
    REF.languageByIso2      = new Map(REF.languages.map(l => [l.iso_639_2, l]).filter(([k])=>k));
    REF.currencyByIso       = new Map(REF.currencies.map(c => [c.iso_code, c]));
    REF.identifierTypeByCode= new Map(REF.identifierTypes.map(t => [t.code, t]));

    // CWR namespace (busy enough to deserve its own bag)
    REF.cwr = {
      recordTypes:           raw.cwr_record_types            || [],
      transactionTypes:      raw.cwr_transaction_types       || [],
      agreementTypes:        raw.cwr_agreement_types         || [],
      writerDesignations:    raw.cwr_writer_designations     || [],
      typeOfRights:          raw.cwr_type_of_rights          || [],
      workCategories:        raw.cwr_work_categories         || [],
      titleTypes:            raw.cwr_title_types             || [],
      versionTypes:          raw.cwr_version_types           || [],
      subjectCodes:          raw.cwr_subject_codes           || [],
      intendedPurposes:      raw.cwr_intended_purposes       || [],
      textMusicRelationships:raw.cwr_text_music_relationships|| [],
      compositeTypes:        raw.cwr_composite_types         || [],
      recordingTechniques:   raw.cwr_recording_techniques    || [],
      instruments:           raw.cwr_instruments             || [],
      standardInstrumentations: raw.cwr_standard_instrumentations || [],
      senderTypes:           raw.cwr_sender_types            || [],
      submitterCodes:        raw.cwr_submitter_codes         || [],
      errorCodes:            raw.cwr_error_codes             || [],
    };
    REF.cwr.recordTypeByCode      = new Map(REF.cwr.recordTypes.map(t => [t.code, t]));
    REF.cwr.errorByCode           = new Map(REF.cwr.errorCodes.map(t => [t.error_code, t]));

    // ── LEGACY ALIASES — keep existing prototype code working ─────────────────
    // Society label lists, sliced by repertoire/type. The prototype currently
    // uses these for PRO dropdowns on splits, ID step, etc. Society rows have
    // `cmo_type` (one of PRO, MRO, NRO, MIX, etc.) and `main_repertoire`.
    const acronyms = (rows) => Array.from(new Set(rows.map(s => s.acronym).filter(Boolean))).sort();

    // Writer-side: PROs that collect performance income for writers/publishers
    const proAcronyms = acronyms(REF.societies.filter(s => /PRO|MIX|HUB/i.test(s.cmo_type || '')));
    // Publisher-side: same set practically — publishers register with the same PROs
    const pubAcronyms = proAcronyms;
    // CWR-eligible: PRO/MRO/HUB/MIX (anything that ingests CWR transactions, drops pure NRO)
    const cwrAcronyms = acronyms(REF.societies.filter(s => /PRO|MRO|MIX|HUB/i.test(s.cmo_type || '')));

    // Only set if the prototype hasn't pre-populated them — never overwrite.
    if (!window.SOC_WRITER || !window.SOC_WRITER.length)    window.SOC_WRITER    = proAcronyms;
    if (!window.SOC_PUBLISHER || !window.SOC_PUBLISHER.length) window.SOC_PUBLISHER = pubAcronyms;
    if (!window.SOC_CWR || !window.SOC_CWR.length)          window.SOC_CWR       = cwrAcronyms;
  };

  fetch('db/ref/all.json', { cache: 'force-cache' })
    .then(r => {
      if (!r.ok) throw new Error('HTTP ' + r.status);
      return r.json();
    })
    .then(json => {
      buildIndices(json);
      announceReady();
      console.info('[ref] loaded', REF.tables.length, 'tables —',
        REF.territories.length, 'territories ·',
        REF.societies.length, 'societies ·',
        REF.languages.length, 'languages ·',
        REF.currencies.length, 'currencies');
    })
    .catch(err => {
      REF.error = err;
      // Still mark ready so components don't hang waiting; they'll fall back
      // to their hardcoded lists.
      announceReady();
      console.warn('[ref] load failed — falling back to hardcoded lists:', err.message);
    });
})();

// ── Helper: language options for InlineSelect (returns string[] of names).
//
// Returns a sorted list of language names from REF.languages. Falls back to
// the small hardcoded list the prototype used before ref data was wired.
// Used by Work edit (metadata language, lyrics language, alt-title language)
// and any other place that takes language as a free-text label.
function refLanguageOptions() {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.languages) && REF.languages.length) {
    return REF.languages.map(l => l.name).filter(Boolean).sort();
  }
  return ['English','Spanish','French','Portuguese','German','Italian','Japanese',
          'Korean','Mandarin','Arabic','Hindi','Russian','Other'];
}
window.refLanguageOptions = refLanguageOptions;

// Helper: resolve a language label/code → ISO 639-1 code.
//   refLangIso('English')  → 'en'
//   refLangIso('en')       → 'en'
//   refLangIso('eng')      → 'en' (via 639-2)
//   refLangIso('')         → '' (empty input passes through)
function refLangIso(label) {
  if (!label) return '';
  const REF = window.REF;
  const s = String(label).trim();
  if (!REF || !REF.ready) return s.slice(0,2).toLowerCase();
  // Exact name match (case-insensitive)
  const byName = REF.languages.find(l => l.name && l.name.toLowerCase() === s.toLowerCase());
  if (byName && byName.iso_639_1) return byName.iso_639_1;
  // Already a 2-letter code?
  if (s.length === 2 && REF.languageByIso1.has(s.toLowerCase())) return s.toLowerCase();
  // 3-letter code?
  if (s.length === 3 && REF.languageByIso2.has(s.toLowerCase())) {
    return REF.languageByIso2.get(s.toLowerCase()).iso_639_1 || '';
  }
  // Fallback: best-effort 2-char prefix
  return s.slice(0,2).toLowerCase();
}
window.refLangIso = refLangIso;

// ── refCwrLabels(tableKey, fallback) ──────────────────────────────────────
//
// Returns label[] for a CWR ref table, sorted by `code`. tableKey is the
// raw key into REF.raw (e.g. 'cwr_composite_types', 'cwr_work_categories').
// Falls back to the provided list if REF isn't ready or the table is empty.
//
// Used by the work-creation dialog's CWR-detail step to keep dropdown
// options in lockstep with the spec rows shipped in db/ref/all.json.
function refCwrLabels(tableKey, fallback) {
  const REF = window.REF;
  if (REF && REF.ready) {
    const rows = REF.raw && REF.raw[tableKey];
    if (Array.isArray(rows) && rows.length) {
      return rows
        .filter(r => r && r.label)
        .sort((a,b) => String(a.code||'').localeCompare(String(b.code||'')))
        .map(r => r.label);
    }
  }
  return fallback || [];
}
window.refCwrLabels = refCwrLabels;

// refWorkTypeLabels — labels from ref.work_types (Original / Arrangement / …)
function refWorkTypeLabels(fallback) {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.workTypes) && REF.workTypes.length) {
    return REF.workTypes.filter(t => t && t.label).map(t => t.label);
  }
  return fallback || ['Original work','Compilation','Derivative work','Translation','Arrangement'];
}
window.refWorkTypeLabels = refWorkTypeLabels;

// refGenreLabels(scope='MUSIC') — top-level genre labels (sorted, dedup).
// Default returns the 152 top-level genres only (parent_id=null), keeping
// the dropdown manageable. Pass {includeChildren:true} to flatten all 1304.
function refGenreLabels(opts) {
  const { scope = 'MUSIC', includeChildren = false, fallback } = opts || {};
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.genres) && REF.genres.length) {
    const rows = REF.genres.filter(g => g && g.label && (!scope || g.scope === scope)
      && (includeChildren || g.parent_id == null));
    const labels = [...new Set(rows.map(g => g.label))].sort((a,b)=>a.localeCompare(b));
    if (labels.length) return labels;
  }
  return fallback || ['Pop','R&B','Soul','Indie','Electronic','Hip-Hop','Jazz','Folk','Rock','Alternative','Country','Classical','Reggae','Latin','Metal','Punk','Blues','Gospel'];
}
window.refGenreLabels = refGenreLabels;

// refSubGenreLabels(parentLabel) — children of a top-level genre (by label).
// Returns [] if no children exist or parent isn't found.
function refSubGenreLabels(parentLabel, opts) {
  const { scope = 'MUSIC' } = opts || {};
  const REF = window.REF;
  if (!parentLabel || !REF || !REF.ready || !Array.isArray(REF.genres)) return [];
  const parent = REF.genres.find(g => g && g.label === parentLabel && (!scope || g.scope === scope) && g.parent_id == null);
  if (!parent) return [];
  const kids = REF.genres.filter(g => g && g.parent_id === parent.id && g.label);
  return [...new Set(kids.map(g => g.label))].sort((a,b)=>a.localeCompare(b));
}
window.refSubGenreLabels = refSubGenreLabels;

// ── Identifier formatters ─────────────────────────────────────────────────
//
// Every identifier has a {normalize, display, validate} trio.
//   normalize(input)  → canonical storage form, or null if invalid
//   display(canonical)→ pretty rendering for tables / labels
//   validate(input)   → boolean
//
// Conventions:
//   • normalize() is forgiving (strips dashes/dots/spaces, accepts variants)
//   • storage form matches the official spec where possible
//   • display() is the short, dense form (better for tables)

// ───── ISWC ───────────────────────────────────────────────────────────────
//   Spec:    T + 10 digits (last is mod-10 checksum, but we don't enforce)
//   Storage: T-NNN.NNN.NNN-C   e.g. T-913.221.084-2
//   Display: TNNNNNNNNNNC      e.g. T9132210842   (CWR-friendly, 11 chars)
//   Accepts: any of the above, plus loose whitespace / case
function iswcNormalize(input) {
  if (input == null) return null;
  const cleaned = String(input).toUpperCase().replace(/[^T0-9]/g, '');
  const m = /^T(\d{10})$/.exec(cleaned);
  if (!m) return null;
  const d = m[1];
  return `T-${d.slice(0,3)}.${d.slice(3,6)}.${d.slice(6,9)}-${d.slice(9,10)}`;
}
function iswcDisplay(canonical) {
  if (!canonical) return '';
  const cleaned = String(canonical).toUpperCase().replace(/[^T0-9]/g, '');
  return /^T\d{10}$/.test(cleaned) ? cleaned : String(canonical);
}
window.iswcNormalize = iswcNormalize;
window.iswcDisplay = iswcDisplay;

// ───── ISRC ───────────────────────────────────────────────────────────────
//   Spec:    CC-XXX-YY-NNNNN  (country, registrant, year, designation)
//   Storage: CC-XXX-YY-NNNNN  e.g. US-RC1-24-00831
//   Display: CCXXXYYNNNNN     e.g. USRC12400831  (12 chars, DSP-friendly)
//   Accepts: any of the above, hyphens or none, case-insensitive
function isrcNormalize(input) {
  if (input == null) return null;
  const cleaned = String(input).toUpperCase().replace(/[^A-Z0-9]/g, '');
  const m = /^([A-Z]{2})([A-Z0-9]{3})(\d{2})(\d{5})$/.exec(cleaned);
  if (!m) return null;
  return `${m[1]}-${m[2]}-${m[3]}-${m[4]}`;
}
function isrcDisplay(canonical) {
  if (!canonical) return '';
  const cleaned = String(canonical).toUpperCase().replace(/[^A-Z0-9]/g, '');
  return /^[A-Z]{2}[A-Z0-9]{3}\d{7}$/.test(cleaned) ? cleaned : String(canonical);
}
window.isrcNormalize = isrcNormalize;
window.isrcDisplay = isrcDisplay;

// ───── IPI (Interested Party Information number) ─────────────────────────
//   Spec:    9–11 digits, zero-padded to 11 in CWR
//   Storage: 11 digits, zero-padded   e.g. 00883112023
//   Display: 11 digits w/ thin spaces e.g. 0088 3112 023
//             (use displayCompact() if you want bare 11-digit form)
//   Accepts: any digit string up to 11 digits, with or without spaces/dashes
function ipiNormalize(input) {
  if (input == null) return null;
  const digits = String(input).replace(/\D/g, '');
  if (digits.length === 0 || digits.length > 11) return null;
  return digits.padStart(11, '0');
}
function ipiDisplay(canonical) {
  if (!canonical) return '';
  const digits = String(canonical).replace(/\D/g, '');
  if (digits.length !== 11) return String(canonical);
  // Group as 4-4-3 with thin space (visually scannable).
  return `${digits.slice(0,4)} ${digits.slice(4,8)} ${digits.slice(8,11)}`;
}
function ipiDisplayCompact(canonical) {
  if (!canonical) return '';
  const digits = String(canonical).replace(/\D/g, '');
  return digits.length === 11 ? digits : String(canonical);
}
window.ipiNormalize = ipiNormalize;
window.ipiDisplay = ipiDisplay;
window.ipiDisplayCompact = ipiDisplayCompact;

// ───── ISNI (International Standard Name Identifier) ──────────────────────
//   Spec:    16 chars: 15 digits + 1 check character (digit or X)
//   Storage: 16 chars, no separators  e.g. 0000000114831056
//   Display: groups of 4              e.g. 0000 0001 1483 1056
//   Accepts: any 16-char form with/without spaces, case-insensitive (final X allowed)
function isniNormalize(input) {
  if (input == null) return null;
  const cleaned = String(input).toUpperCase().replace(/[^0-9X]/g, '');
  if (!/^\d{15}[0-9X]$/.test(cleaned)) return null;
  return cleaned;
}
function isniDisplay(canonical) {
  if (!canonical) return '';
  const cleaned = String(canonical).toUpperCase().replace(/[^0-9X]/g, '');
  if (!/^\d{15}[0-9X]$/.test(cleaned)) return String(canonical);
  return `${cleaned.slice(0,4)} ${cleaned.slice(4,8)} ${cleaned.slice(8,12)} ${cleaned.slice(12,16)}`;
}
window.isniNormalize = isniNormalize;
window.isniDisplay = isniDisplay;

// ───── UPC / EAN ──────────────────────────────────────────────────────────
//   UPC: 12 digits.   EAN-13: 13 digits.   We accept either and store as-is.
//   Storage: digits only
//   Display: digits only (bare); pretty form groups as 1-5-5-1 for UPC,
//            1-6-6 for EAN-13.
function upcNormalize(input) {
  if (input == null) return null;
  const digits = String(input).replace(/\D/g, '');
  return /^\d{12}$/.test(digits) ? digits : null;
}
function upcDisplay(canonical) {
  if (!canonical) return '';
  const digits = String(canonical).replace(/\D/g, '');
  return /^\d{12}$/.test(digits) ? digits : String(canonical);
}
function eanNormalize(input) {
  if (input == null) return null;
  const digits = String(input).replace(/\D/g, '');
  return /^\d{13}$/.test(digits) ? digits : null;
}
function eanDisplay(canonical) {
  if (!canonical) return '';
  const digits = String(canonical).replace(/\D/g, '');
  return /^\d{13}$/.test(digits) ? digits : String(canonical);
}
// Combined UPC/EAN normalizer — accepts either length, returns whichever fits.
function upcEanNormalize(input) {
  if (input == null) return null;
  const digits = String(input).replace(/\D/g, '');
  if (/^\d{12}$/.test(digits) || /^\d{13}$/.test(digits)) return digits;
  return null;
}
window.upcNormalize = upcNormalize;
window.upcDisplay = upcDisplay;
window.eanNormalize = eanNormalize;
window.eanDisplay = eanDisplay;
window.upcEanNormalize = upcEanNormalize;

// refWriterRoleLabels(fallback) — labels from ref.writer_roles, sorted by code.
// Used by Add Song writer dropdowns and Splits writer-role pickers.
function refWriterRoleLabels(fallback) {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.writerRoles) && REF.writerRoles.length) {
    return REF.writerRoles
      .filter(r => r && r.label)
      .slice()
      .sort((a,b) => String(a.code||'').localeCompare(String(b.code||'')))
      .map(r => r.label);
  }
  return fallback || ['Composer','Composer/Author','Author','Arranger','Adaptor','Translator','Sub-Arranger','Sub-Author','Income Participant'];
}
window.refWriterRoleLabels = refWriterRoleLabels;

// refPublisherRoleLabels(fallback) — labels from ref.publisher_roles, sorted by code.
function refPublisherRoleLabels(fallback) {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.publisherRoles) && REF.publisherRoles.length) {
    return REF.publisherRoles
      .filter(r => r && r.label)
      .slice()
      .sort((a,b) => String(a.code||'').localeCompare(String(b.code||'')))
      .map(r => r.label);
  }
  return fallback || ['Original Publisher','Sub-Publisher','Administrator','Acquirer','Income Participant','Substituted Publisher'];
}
window.refPublisherRoleLabels = refPublisherRoleLabels;

// refAliasTypes(fallback) — array of {v, l, hint?} from ref.alias_types.
// `v` = alias_types.code (legal_name, stage_name, …); `l` = label.
// Returns the same shape the prototype uses for ALIAS_TYPES dropdowns.
function refAliasTypes(fallback) {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.aliasTypes) && REF.aliasTypes.length) {
    return REF.aliasTypes
      .filter(t => t && t.label && t.code)
      .map(t => ({ v: t.code, l: t.label, hint: t.description || '' }));
  }
  return fallback || [
    { v:'legal_name',  l:'Legal name'  },
    { v:'stage_name',  l:'Stage name'  },
    { v:'artist_name', l:'Artist name' },
    { v:'birth_name',  l:'Birth name'  },
    { v:'real_name',   l:'Real name'   },
    { v:'misspelling', l:'Misspelling' },
    { v:'other',       l:'Other'       },
  ];
}
window.refAliasTypes = refAliasTypes;

// refArtistRoles(fallback) — array of {v, l} for the 3 accepted release-credit roles.
// Source: ref.artist_roles → PRIMARY / FEATURED / REMIXER.
// Used by _ArArtistsField (Add Release modal) and any other release-credit picker.
function refArtistRoles(fallback) {
  const REF = window.REF;
  if (REF && REF.ready && Array.isArray(REF.artistRoles) && REF.artistRoles.length) {
    return REF.artistRoles
      .filter(t => t && t.code)
      .map(t => ({ v: String(t.code).toLowerCase(), l: t.label || t.code }));
  }
  return fallback || [
    { v: 'primary',  l: 'Primary Artist'  },
    { v: 'featured', l: 'Featured Artist' },
    { v: 'remixer',  l: 'Remixer'         },
  ];
}
window.refArtistRoles = refArtistRoles;

// refExplicitContentTypes(opts) — array of {v, l} from ref.explicit_content_types.
// `v` = code (lowercase form: 'not_explicit', 'explicit', 'clean', …)
// `l` = label
//
//   opts.userPickable=true (default) → only the 3 release-form options:
//     NOT_EXPLICIT, EXPLICIT, CLEAN. Hides NOT_APPLICABLE / UNKNOWN, which are
//     catch-all values for ingested catalog data and don't belong in a creation
//     form.
//   opts.userPickable=false → all 5 codes (for filters, ingest UIs, etc.)
function refExplicitContentTypes(opts) {
  const { userPickable = true } = opts || {};
  const REF = window.REF;
  const userSet = new Set(['EXPLICIT', 'CLEAN', 'NOT_EXPLICIT']);
  if (REF && REF.ready && Array.isArray(REF.explicitContentTypes) && REF.explicitContentTypes.length) {
    const rows = userPickable
      ? REF.explicitContentTypes.filter(t => userSet.has(t.code))
      : REF.explicitContentTypes;
    // Order: NOT_EXPLICIT → EXPLICIT → CLEAN → others. Matches user mental model.
    const order = ['NOT_EXPLICIT', 'EXPLICIT', 'CLEAN', 'NOT_APPLICABLE', 'UNKNOWN'];
    return rows.slice().sort((a, b) =>
      (order.indexOf(a.code) === -1 ? 999 : order.indexOf(a.code)) -
      (order.indexOf(b.code) === -1 ? 999 : order.indexOf(b.code))
    ).map(t => ({ v: t.code, l: t.label || t.code }));
  }
  // Fallback mirrors DB row-for-row.
  const fallback = [
    { v: 'NOT_EXPLICIT', l: 'Not Explicit' },
    { v: 'EXPLICIT',     l: 'Explicit'     },
    { v: 'CLEAN',        l: 'Clean'        },
  ];
  return userPickable ? fallback : fallback.concat([
    { v: 'NOT_APPLICABLE', l: 'Not Applicable' },
    { v: 'UNKNOWN',        l: 'Unknown'        },
  ]);
}
window.refExplicitContentTypes = refExplicitContentTypes;

// useRefReady — re-render hook. Returns true once REF.ready flips.
function useRefReady() {
  const [ready, setReady] = React.useState(!!(window.REF && window.REF.ready));
  React.useEffect(() => {
    if (window.REF && window.REF.ready) return;
    const h = () => setReady(true);
    window.addEventListener('astro-ref-ready', h);
    return () => window.removeEventListener('astro-ref-ready', h);
  }, []);
  return ready;
}
window.useRefReady = useRefReady;

// RefSelect — thin <select> bound to a ref table. Falls back to `fallback`
// (an array of strings or {value,label} objects) if ref data hasn't loaded.
//
//   <RefSelect kind="territories" valueField="iso_alpha_2" labelField="common_name"
//              value={t} onChange={setT} fallback={['US','GB','DE']} />
function RefSelect({ kind, valueField = 'code', labelField = 'label',
                    value, onChange, placeholder, fallback = [], filter, sort = true,
                    style, className, disabled }) {
  const ready = useRefReady();
  let rows = ready && window.REF && window.REF.raw[kind];
  if (!Array.isArray(rows) || !rows.length) {
    rows = fallback.map(f => typeof f === 'string'
      ? { [valueField]: f, [labelField]: f }
      : f);
  }
  if (filter) rows = rows.filter(filter);
  if (sort) {
    rows = rows.slice().sort((a, b) =>
      String(a[labelField] || '').localeCompare(String(b[labelField] || '')));
  }
  return (
    <select value={value || ''} onChange={e => onChange && onChange(e.target.value)}
            disabled={disabled} className={className}
            style={{padding:'4px 6px',background:'var(--bg)',border:'1px solid var(--rule)',
                    color:'var(--ink-1)',font:'inherit',...style}}>
      {placeholder && <option value="">{placeholder}</option>}
      {rows.map((r, i) => (
        <option key={r[valueField] ?? i} value={r[valueField] ?? ''}>
          {r[labelField] ?? r[valueField] ?? '—'}
        </option>
      ))}
    </select>
  );
}
window.RefSelect = RefSelect;
