// ============================================================================
// ID-CODES — Unified validator + auto-fix library for all music-industry
// identifier codes. Covers UPC / EAN-13 / GRid / DPID / LC / CISAC Society /
// TIS Territory / ISCC / ISMN / MusicBrainz / ISAN, and re-exports the
// existing CwrIdValidate helpers for ISWC / IPI Name / IPI Base / ISRC / ISNI.
//
// Every validator returns a consistent shape:
//   { valid: true|false|'warn',
//     reason?: string,            // human-readable problem
//     normalized?: string,        // canonical-format echo
//     suggestion?: string,        // a *suggested fix* if the body is right
//                                 // but the check digit / format is wrong
//     suggestionFix?: 'check'|'format'|'pad'|'case' }
//
// EXPORT: window.IdCodes = { validate, validators, formats, all, autoFormat }
// ============================================================================
(function () {

  // ─── Helpers ────────────────────────────────────────────────────────────
  const onlyDigits = (s) => String(s || '').replace(/\D+/g, '');
  const onlyAlnum  = (s) => String(s || '').replace(/[^A-Za-z0-9]/g, '');
  const ok    = (normalized) => ({ valid: true,  normalized });
  const skip  = (reason)     => ({ valid: true,  skipped: true, reason });
  const bad   = (reason, extras) => Object.assign({ valid: false, reason }, extras || {});
  const warn  = (reason, extras) => Object.assign({ valid: 'warn', reason }, extras || {});

  // mod-10 (Luhn-like, but using GS1 weights 3,1,3,1… right-to-left)
  function gs1Mod10Check(digits) {
    // digits is a string of N-1 digits; return the check digit
    const arr = digits.split('').map(Number).reverse();
    let sum = 0;
    for (let i = 0; i < arr.length; i++) sum += arr[i] * (i % 2 === 0 ? 3 : 1);
    return (10 - (sum % 10)) % 10;
  }

  // ─── UPC-A: 12 digits, GS1 mod-10 ───────────────────────────────────────
  function validateUpc(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = onlyDigits(value);
    if (cleaned.length !== 12) {
      // If 11 digits, suggest computed check
      if (cleaned.length === 11) {
        const fix = gs1Mod10Check(cleaned);
        return bad('UPC must be 12 digits, got ' + cleaned.length, { suggestion: cleaned + fix, suggestionFix: 'pad' });
      }
      return bad('UPC must be 12 digits, got ' + cleaned.length);
    }
    const body = cleaned.slice(0, 11);
    const given = parseInt(cleaned[11], 10);
    const computed = gs1Mod10Check(body);
    if (computed !== given) {
      return bad('UPC check digit mismatch: expected ' + computed + ', got ' + given,
        { suggestion: body + computed, suggestionFix: 'check' });
    }
    return ok(cleaned);
  }

  // ─── EAN-13: 13 digits, GS1 mod-10 ──────────────────────────────────────
  function validateEan13(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = onlyDigits(value);
    if (cleaned.length !== 13) {
      if (cleaned.length === 12) {
        const fix = gs1Mod10Check(cleaned);
        return bad('EAN-13 must be 13 digits, got ' + cleaned.length, { suggestion: cleaned + fix, suggestionFix: 'pad' });
      }
      return bad('EAN-13 must be 13 digits, got ' + cleaned.length);
    }
    const body = cleaned.slice(0, 12);
    const given = parseInt(cleaned[12], 10);
    const computed = gs1Mod10Check(body);
    if (computed !== given) {
      return bad('EAN-13 check digit mismatch: expected ' + computed + ', got ' + given,
        { suggestion: body + computed, suggestionFix: 'check' });
    }
    return ok(cleaned);
  }

  // ─── GRid (Global Release Identifier): 18 chars
  // Format: A1-XXXXX-NNNNNNNNNN-C  (issuer-code(5) + release-num(10) + check(1))
  // Mod 37,36 check digit (ISO/IEC 7064)
  function validateGrid(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = onlyAlnum(value).toUpperCase();
    if (cleaned.length !== 18) {
      return bad('GRid must be 18 alphanumeric chars (got ' + cleaned.length + ')');
    }
    // Mod 37,36 — ISO 7064
    const map = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    let p = 36;
    for (let i = 0; i < 17; i++) {
      const v = map.indexOf(cleaned[i]);
      if (v < 0) return bad('GRid: invalid character "' + cleaned[i] + '"');
      let s = (p + v) % 36;
      if (s === 0) s = 36;
      p = (s * 2) % 37;
    }
    const computed = (37 - p) % 36;
    const givenIdx = map.indexOf(cleaned[17]);
    if (computed !== givenIdx) {
      return bad('GRid check char mismatch: expected ' + map[computed] + ', got ' + cleaned[17],
        { suggestion: cleaned.slice(0, 17) + map[computed], suggestionFix: 'check' });
    }
    return ok(cleaned.slice(0, 2) + '-' + cleaned.slice(2, 7) + '-' + cleaned.slice(7, 17) + '-' + cleaned[17]);
  }

  // ─── DDEX Party ID (DPID): "PADPIDA" + 14 alphanumeric chars (21 total)
  // Reference: DDEX DPID v3
  function validateDpid(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = onlyAlnum(value).toUpperCase();
    if (!cleaned.startsWith('PADPIDA')) {
      return bad('DPID must start with "PADPIDA", got "' + cleaned.slice(0, 7) + '"');
    }
    if (cleaned.length !== 21) {
      return bad('DPID must be 21 chars (PADPIDA + 14), got ' + cleaned.length);
    }
    if (!/^[A-Z0-9]{14}$/.test(cleaned.slice(7))) {
      return bad('DPID body must be 14 alphanumeric, got "' + cleaned.slice(7) + '"');
    }
    return ok(cleaned);
  }

  // ─── Label Code: LC + 4 or 5 digits
  function validateLabelCode(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const raw = String(value).trim().toUpperCase();
    const m = raw.match(/^LC[\s\-]?(\d{4,5})$/);
    if (!m) {
      // try bare digits
      const d = onlyDigits(raw);
      if (d.length === 4 || d.length === 5) {
        return bad('Label Code missing "LC" prefix', { suggestion: 'LC-' + d, suggestionFix: 'format' });
      }
      return bad('Label Code must be LC- followed by 4 or 5 digits, got "' + value + '"');
    }
    return ok('LC-' + m[1]);
  }

  // ─── CISAC Society Code: 3 digits, must exist in ref table ──────────────
  function validateSocietyCode(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const d = onlyDigits(value).padStart(3, '0');
    if (d.length !== 3) return bad('Society code must be 3 digits, got "' + value + '"');
    // Try ref-data lookup if available
    const tbl = (window.RS && window.RS.refData && window.RS.refData.societies)
      || window.SOCIETY_CODES
      || null;
    if (tbl) {
      const found = Array.isArray(tbl)
        ? tbl.find((s) => String(s.code || s.id).padStart(3, '0') === d)
        : tbl[d];
      if (!found) return bad('Society code ' + d + ' not in CISAC reference list');
      return ok(d);
    }
    // No table — accept syntactic match with a warn
    return warn('Society code ' + d + ' (no reference table loaded — syntax-only check)', { normalized: d });
  }

  // ─── TIS Territory Code: 4 digits, must be in TIS table ────────────────
  function validateTisCode(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const d = onlyDigits(value).padStart(4, '0');
    if (d.length !== 4) return bad('TIS territory must be 4 digits, got "' + value + '"');
    const tbl = (window.RS && window.RS.refData && window.RS.refData.tis)
      || window.TIS_CODES
      || null;
    if (tbl) {
      const found = Array.isArray(tbl)
        ? tbl.find((t) => String(t.code || t.id).padStart(4, '0') === d)
        : tbl[d];
      if (!found) return bad('TIS code ' + d + ' not in territory reference list');
      return ok(d);
    }
    return warn('TIS code ' + d + ' (no reference table loaded — syntax-only check)', { normalized: d });
  }

  // ─── ISCC (International Standard Content Code, ISO 24138)
  // Spec: ISCC:XXXXXXXXXXXXXX… base32, version-prefixed. Length varies by
  // unit; common composite: 64-byte → ~13 base32 chars per unit, 4 units.
  // Practical sanity: starts with "ISCC:" and is base32 with 10..52 body chars.
  function validateIscc(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const raw = String(value).trim().toUpperCase();
    const m = raw.match(/^ISCC:([A-Z2-7]{10,52})$/);
    if (!m) {
      // try without prefix
      const m2 = raw.match(/^[A-Z2-7]{10,52}$/);
      if (m2) return bad('ISCC missing "ISCC:" prefix', { suggestion: 'ISCC:' + raw, suggestionFix: 'format' });
      return bad('ISCC must be "ISCC:" + base32 body (10-52 chars from A-Z, 2-7), got "' + value + '"');
    }
    return ok('ISCC:' + m[1]);
  }

  // ─── ISMN (International Standard Music Number — printed music)
  // Format: M-NNN-NNNNN-C or 979-0-NNN-NNNNN-C (13 digits when full).
  // Body length 10 (M-form) or 13 (979-0 prefix). Mod-10 (EAN-13 weights for 13-digit form).
  function validateIsmn(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const raw = String(value).trim().toUpperCase();
    const cleaned = raw.replace(/[\s\-]/g, '');
    // 13-digit form: 9790NNNNNNNNN
    if (/^9790\d{9}$/.test(cleaned)) {
      const body = cleaned.slice(0, 12);
      const given = parseInt(cleaned[12], 10);
      const computed = gs1Mod10Check(body);
      if (computed !== given) {
        return bad('ISMN-13 check digit mismatch: expected ' + computed,
          { suggestion: body + computed, suggestionFix: 'check' });
      }
      return ok('979-0-' + cleaned.slice(4, 7) + '-' + cleaned.slice(7, 12) + '-' + cleaned[12]);
    }
    // M-form: M + 9 digits
    if (/^M\d{9}$/.test(cleaned)) {
      // M-form check: weights 3,1,3,1… on M=3
      const arr = cleaned.split('').map((c) => c === 'M' ? 3 : Number(c));
      let sum = 0;
      for (let i = 0; i < 9; i++) sum += arr[i] * (i % 2 === 0 ? 1 : 3);
      const computed = (10 - (sum % 10)) % 10;
      const given = arr[9];
      if (computed !== given) {
        return bad('ISMN-M check digit mismatch: expected ' + computed,
          { suggestion: 'M' + cleaned.slice(1, 9) + computed, suggestionFix: 'check' });
      }
      return ok('M-' + cleaned.slice(1, 4) + '-' + cleaned.slice(4, 9) + '-' + cleaned[9]);
    }
    return bad('ISMN must be M+9 digits or 979-0+9 digits, got "' + value + '"');
  }

  // ─── MusicBrainz ID — UUID v4-ish (8-4-4-4-12 hex) ─────────────────────
  function validateMbid(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = String(value).trim().toLowerCase();
    if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(cleaned)) {
      // Maybe just hex without dashes
      const hex = cleaned.replace(/-/g, '');
      if (/^[0-9a-f]{32}$/.test(hex)) {
        const formatted = hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20, 32);
        return bad('MBID needs dashes', { suggestion: formatted, suggestionFix: 'format' });
      }
      return bad('MBID must be UUID format (8-4-4-4-12 hex), got "' + value + '"');
    }
    return ok(cleaned);
  }

  // ─── ISAN (Audiovisual). Root: 12 hex + 4 hex episode + check(1) + 8 hex version + check(1)
  // Common short form: 12 hex (root). Full ISAN: 24 hex + 2 check chars (mod-37,36).
  function validateIsan(value) {
    if (!value || String(value).trim() === '') return skip('empty (optional)');
    const cleaned = String(value).replace(/[\s\-]/g, '').toUpperCase();
    // Short root form (12 hex)
    if (/^[0-9A-F]{12}$/.test(cleaned)) {
      return ok('ISAN ' + cleaned.slice(0, 4) + '-' + cleaned.slice(4, 8) + '-' + cleaned.slice(8, 12));
    }
    // Full form (16 + check + 8 + check = 26 chars)
    if (/^[0-9A-F]{16}[0-9A-Z][0-9A-F]{8}[0-9A-Z]$/.test(cleaned)) {
      // We can't reliably compute ISAN check here without a full mod-37,36 impl over hex+alpha,
      // but ISO 7064 mod 37-2 is what's specified — we accept structurally valid input.
      return ok('ISAN ' + cleaned.slice(0, 4) + '-' + cleaned.slice(4, 8) + '-' + cleaned.slice(8, 12) + '-' + cleaned.slice(12, 16) + '-' + cleaned[16] + '-' + cleaned.slice(17, 25) + '-' + cleaned[25]);
    }
    return bad('ISAN must be 12 hex (root) or 26 chars (full with check), got ' + cleaned.length);
  }

  // ─── Bring in CWR-side validators via window.CwrIdValidate ──────────────
  function fwdCwr(method) {
    return function (value) {
      const fn = window.CwrIdValidate && window.CwrIdValidate[method];
      if (!fn) return warn('CwrIdValidate not loaded', { normalized: String(value || '').trim() });
      return fn(value);
    };
  }

  // ─── Format catalog ─────────────────────────────────────────────────────
  const formats = {
    iswc:    { label: 'ISWC',          example: 'T-345.246.800-1',                pattern: 'T + 9 digits + check', body: 'ISO 15707 · mod-10 check digit',         scope: 'Works' },
    isrc:    { label: 'ISRC',          example: 'US-RC1-23-45678',                pattern: 'CC + 3 alphanum + 2 + 5 digits',          body: 'ISO 3901 · no check digit, format-strict',  scope: 'Recordings' },
    ipiName: { label: 'IPI Name #',    example: '00073112823',                    pattern: '11 digits',                                body: 'CISAC · mod-101 (last 2 = check)',          scope: 'Writers / Publishers' },
    ipiBase: { label: 'IPI Base #',    example: 'I-001234567-8',                  pattern: 'I + 9 digits + check',                     body: 'CISAC · mod-101',                           scope: 'Works (originator)' },
    isni:    { label: 'ISNI',          example: '0000 0001 2103 2683',            pattern: '15 digits + check (digit or X)',          body: 'ISO 27729 · mod-11-2',                       scope: 'Public identities' },
    upc:     { label: 'UPC-A',         example: '012345678905',                   pattern: '12 digits',                                body: 'GS1 · mod-10 (weights 3,1,3,1…)',           scope: 'Releases (US)' },
    ean13:   { label: 'EAN-13',        example: '4006381333931',                  pattern: '13 digits',                                body: 'GS1 · mod-10',                              scope: 'Releases (intl)' },
    grid:    { label: 'GRid',          example: 'A1-2425G-ABC1234002-M',          pattern: 'A1 + 5 issuer + 10 release + 1 check',    body: 'IFPI · ISO 7064 mod 37,36',                 scope: 'Releases (digital)' },
    dpid:    { label: 'DDEX Party ID', example: 'PADPIDA2014011001I',             pattern: 'PADPIDA + 14 alphanum',                   body: 'DDEX · uniqueness-only',                    scope: 'Parties (DDEX)' },
    labelCode: { label: 'Label Code',  example: 'LC-12345',                        pattern: 'LC + 4 or 5 digits',                      body: 'IFPI · uniqueness-only',                    scope: 'Labels' },
    society: { label: 'Society Code',  example: '052 (BMI)',                       pattern: '3 digits',                                body: 'CISAC reference list',                      scope: 'Societies' },
    tis:     { label: 'TIS Code',      example: '0840 (USA)',                      pattern: '4 digits',                                body: 'CISAC TIS reference list',                  scope: 'Territories' },
    iscc:    { label: 'ISCC',          example: 'ISCC:KACYPXW557HTFJBY',           pattern: '"ISCC:" + base32 body',                   body: 'ISO 24138 · content-derived',               scope: 'Content' },
    ismn:    { label: 'ISMN',          example: 'M-2306-7118-7',                   pattern: 'M + 9 digits OR 979-0 + 9 digits',        body: 'ISO 10957 · mod-10',                        scope: 'Printed music' },
    mbid:    { label: 'MusicBrainz ID',example: '5b11f4ce-a62d-471e-81fc-a69a8278c7da', pattern: 'UUID (8-4-4-4-12 hex)',               body: 'UUID v4 (uniqueness-only)',                 scope: 'MusicBrainz refs' },
    isan:    { label: 'ISAN',          example: 'ISAN 0000-0001-A7A2-0000-K',      pattern: '12 hex root or 26-char full',             body: 'ISO 15706 · mod 37,36',                     scope: 'Audiovisual' },
  };

  // ─── Validators map ─────────────────────────────────────────────────────
  const validators = {
    iswc:      fwdCwr('validateIswc'),
    isrc:      fwdCwr('validateIsrc'),
    ipiName:   fwdCwr('validateIpiName'),
    ipiBase:   fwdCwr('validateIpiBase'),
    isni:      fwdCwr('validateIsni'),
    upc:       validateUpc,
    ean13:     validateEan13,
    grid:      validateGrid,
    dpid:      validateDpid,
    labelCode: validateLabelCode,
    society:   validateSocietyCode,
    tis:       validateTisCode,
    iscc:      validateIscc,
    ismn:      validateIsmn,
    mbid:      validateMbid,
    isan:      validateIsan,
  };

  function validate(kind, value) {
    const fn = validators[kind];
    if (!fn) return bad('unknown id-kind: ' + kind);
    return fn(value);
  }

  // Auto-detect: try every validator and return ranked guesses
  function detect(value) {
    if (!value || String(value).trim() === '') return [];
    const out = [];
    for (const k of Object.keys(validators)) {
      const r = validators[k](value);
      if (r.valid === true) out.push({ kind: k, confidence: 1.0, result: r });
      else if (r.valid === 'warn') out.push({ kind: k, confidence: 0.6, result: r });
    }
    return out.sort((a, b) => b.confidence - a.confidence);
  }

  // Auto-format: just return canonical form when valid; otherwise echo input
  function autoFormat(kind, value) {
    const r = validate(kind, value);
    return (r.valid === true && r.normalized) ? r.normalized : value;
  }

  // Run-all-on-row (for catalog audit)
  function all(values) {
    // values: { iswc, isrc, ipiName, ... }
    const out = {};
    for (const k of Object.keys(values || {})) {
      if (validators[k]) out[k] = validators[k](values[k]);
    }
    return out;
  }

  // Self-test (extends existing CwrIdValidate.selfTest)
  function selfTest() {
    const cases = [
      { fn: validateUpc,       in: '012345678905',                 expect: true,  label: 'UPC valid' },
      { fn: validateUpc,       in: '012345678900',                 expect: false, label: 'UPC bad-check' },
      { fn: validateEan13,     in: '4006381333931',                expect: true,  label: 'EAN-13 valid' },
      { fn: validateEan13,     in: '4006381333930',                expect: false, label: 'EAN-13 bad-check' },
      { fn: validateGrid,      in: 'A1-2425G-ABC1234002-M',        expect: true,  label: 'GRid syntactic' },
      { fn: validateDpid,      in: 'PADPIDA2014011001I',           expect: true,  label: 'DPID well-formed' },
      { fn: validateDpid,      in: 'BADPIDA2014011001I',           expect: false, label: 'DPID wrong prefix' },
      { fn: validateLabelCode, in: 'LC-12345',                     expect: true,  label: 'Label Code 5-digit' },
      { fn: validateLabelCode, in: 'LC-1234',                      expect: true,  label: 'Label Code 4-digit' },
      { fn: validateLabelCode, in: '12345',                        expect: false, label: 'LC missing prefix' },
      { fn: validateMbid,      in: '5b11f4ce-a62d-471e-81fc-a69a8278c7da', expect: true, label: 'MBID UUID' },
      { fn: validateMbid,      in: '5b11f4cea62d471e81fca69a8278c7da', expect: false, label: 'MBID needs dashes' },
      { fn: validateIscc,      in: 'ISCC:KACYPXW557HTFJBY',        expect: true,  label: 'ISCC well-formed' },
      { fn: validateIsmn,      in: 'M230671187',                    expect: true,  label: 'ISMN-M' },
    ];
    let pass = 0, fail = 0;
    const results = cases.map((c) => {
      const r = c.fn(c.in);
      const got = r.valid === true;
      const okk = got === c.expect;
      if (okk) pass++; else fail++;
      return { label: c.label, input: c.in, expected: c.expect, got, ok: okk, reason: r.reason || r.normalized || '' };
    });
    return { pass, fail, total: cases.length, results };
  }

  // Audit a catalog row (Work / Recording / Release / Party)
  // entity = { kind: 'work'|'recording'|'release'|'party', data: {...} }
  function auditEntity(entity) {
    const issues = [];
    const d = entity.data || {};
    const checks = [];
    if (entity.kind === 'work') {
      checks.push(['iswc', d.iswc]);
      if (d.iswcAlt) checks.push(['iswc', d.iswcAlt]);
    } else if (entity.kind === 'recording') {
      checks.push(['isrc', d.isrc]);
    } else if (entity.kind === 'release') {
      checks.push(['upc', d.upc], ['ean13', d.ean13], ['grid', d.grid], ['labelCode', d.lc]);
    } else if (entity.kind === 'party') {
      checks.push(['ipiName', d.ipi], ['ipiBase', d.ipiBase], ['isni', d.isni], ['dpid', d.dpid]);
    } else if (entity.kind === 'agreement') {
      // Agreements link parties; territories/societies validated here too
      if (d.tis) checks.push(['tis', d.tis]);
      if (d.society) checks.push(['society', d.society]);
    }
    for (const [kind, val] of checks) {
      if (!val) continue;
      const r = validate(kind, val);
      if (r.valid === false) issues.push({ kind, value: val, severity: 'high', reason: r.reason, suggestion: r.suggestion });
      else if (r.valid === 'warn') issues.push({ kind, value: val, severity: 'medium', reason: r.reason });
    }
    return { entity: entity.kind, id: d.id || d.code || '', issues };
  }

  window.IdCodes = {
    formats,
    validators,
    validate,
    autoFormat,
    detect,
    all,
    selfTest,
    auditEntity,
    // exposed helpers for callers building forms
    gs1Mod10Check,
  };
})();
