// ============================================================================
// CAF VALIDATE · spec-driven CISAC schema + business-rule conformance
// ----------------------------------------------------------------------------
// Reads window.CafSpec to drive every field-level check.
// Outputs detailed per-line issues + summary + per-record-type stats so the
// compliance dashboard can show certification readiness at a glance.
//
// EXPORT: window.CafValidate = { validate, validateBuilt, ipiCheckDigit, ... }
// ============================================================================
(function () {
  'use strict';
  const Spec = window.CafSpec;
  if (!Spec) { console.error('caf-validate: CafSpec missing'); return; }

  // ─── ID check digit math ────────────────────────────────────────────────
  // IPI Name #: 11 digits = 9 base digits + 2-digit mod-101 check.
  // CISAC formula: cd = (Σ digit_i * (10 - i)) mod 101, where i = 1..9.
  function ipiNameCheckDigit(base9) {
    if (!/^\d{9}$/.test(base9)) return null;
    let sum = 0;
    for (let i = 0; i < 9; i++) sum += parseInt(base9[i], 10) * (10 - i);
    return sum % 101;
  }
  function ipiNameValid(eleven) {
    if (!/^\d{11}$/.test(eleven)) return false;
    const expect = ipiNameCheckDigit(eleven.slice(0, 9));
    return expect === parseInt(eleven.slice(9, 11), 10);
  }
  // IPI Base #: I-NNNNNNNNN-C — 1 letter + 9 digits + check digit
  // Reference impl: weighted sum mod 10 (weights 2..10 cyclic).
  function ipiBaseCheckDigit(base9) {
    if (!/^\d{9}$/.test(base9)) return null;
    let sum = 0;
    for (let i = 0; i < 9; i++) sum += parseInt(base9[i], 10) * ((i % 9) + 2);
    return (10 - (sum % 10)) % 10;
  }
  function ipiBaseValid(s) {
    if (!s) return false;
    const m = s.replace(/[\s-]/g, '');
    if (!/^I\d{10}$/.test(m)) return false;
    const expect = ipiBaseCheckDigit(m.slice(1, 10));
    return expect === parseInt(m.slice(10, 11), 10);
  }

  // ─── Per-line, per-field validation against spec ────────────────────────
  function validateLine(version, line, idx) {
    const issues = [];
    const t = line.slice(0, 3);
    const rec = Spec.getRecord(version, t);
    if (!rec) {
      issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA',
        code: 'REC-001', message: `Unknown record type "${t}"`, fix: 'Check spec table for permitted record codes' });
      return issues;
    }
    if (line.length !== rec.length) {
      issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA',
        code: 'LEN-001', message: `${t} is ${line.length} chars; spec requires ${rec.length}`,
        fix: 'Check field offsets — re-build with up-to-date spec' });
    }
    // Walk fields by offset
    let off = 0;
    for (const f of rec.fields) {
      const raw = line.slice(off, off + f.len);
      const trimmed = raw.replace(/\s+$/, '');
      // Required-field presence
      if (f.required && trimmed === '' && f.default == null) {
        issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA',
          code: 'REQ-001', message: `Missing required field "${f.name}" at @${off}+${f.len}`,
          fix: `Populate ${rec.label} field "${f.name}"` });
      }
      // Type-specific validators
      if (trimmed !== '' && typeof f.validate === 'function') {
        const err = f.validate(raw, { type: t, version });
        if (err) {
          issues.push({ line: idx, recordType: t, severity: 'error', category: 'FIELD',
            code: 'VAL-' + f.name.toUpperCase().slice(0, 8), message: `${f.name}: ${err}`,
            fix: `Field @${off}+${f.len}` });
        }
      }
      off += f.len;
    }
    return issues;
  }

  // ─── Whole-transmission validation ──────────────────────────────────────
  function validate({ version = '1.2', lines, agreements }) {
    const issues = [];
    const isV3 = String(version).startsWith('3');
    const pushAll = (xs) => xs.forEach(i => issues.push(i));

    // 1. Per-line spec checks (lengths, required fields, field-level validators)
    lines.forEach((l, idx) => pushAll(validateLine(version, l, idx)));

    // 2. SCHEMA: ordering & envelope
    let sawHdr = false, sawTrl = false, openGroup = false, groupCount = 0;
    lines.forEach((l, idx) => {
      const t = l.slice(0, 3);
      if (t === 'HDR') {
        if (sawHdr) issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA', code: 'ORD-001', message: 'Duplicate HDR record', fix: 'Only one HDR per transmission' });
        if (idx !== 0) issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA', code: 'ORD-002', message: 'HDR must be the first record', fix: 'Move HDR to line 1' });
        sawHdr = true;
      }
      if (t === 'GRH') {
        if (openGroup) issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA', code: 'ORD-003', message: 'GRH inside open group', fix: 'Close prior group with GRT before opening another' });
        openGroup = true; groupCount += 1;
      }
      if (t === 'GRT') {
        if (!openGroup) issues.push({ line: idx, recordType: t, severity: 'error', category: 'SCHEMA', code: 'ORD-004', message: 'GRT without matching GRH', fix: 'Each GRT must follow a GRH' });
        openGroup = false;
      }
      if (t === 'TRL') sawTrl = true;
    });
    if (!sawHdr) issues.push({ line: 0, recordType: 'HDR', severity: 'error', category: 'SCHEMA', code: 'ORD-005', message: 'Transmission missing HDR' });
    if (!sawTrl) issues.push({ line: lines.length - 1, recordType: 'TRL', severity: 'error', category: 'SCHEMA', code: 'ORD-006', message: 'Transmission missing TRL' });
    if (openGroup) issues.push({ line: lines.length - 1, recordType: 'GRH', severity: 'error', category: 'SCHEMA', code: 'ORD-007', message: 'Group opened but never closed (missing GRT)' });

    // 3. SCHEMA: trailer counts
    const grhIdx = lines.findIndex(l => l.startsWith('GRH'));
    const grtIdx = lines.findIndex(l => l.startsWith('GRT'));
    const trlIdx = lines.findIndex(l => l.startsWith('TRL'));
    if (grhIdx >= 0 && grtIdx > grhIdx) {
      const grtRec = Spec.getRecord(version, 'GRT');
      const grtLine = lines[grtIdx];
      if (grtRec) {
        // recordCount field offset = 3 + 5 + 8 = 16, length 8
        const txnDeclared = parseInt(grtLine.slice(8, 16), 10) || 0;
        const recDeclared = parseInt(grtLine.slice(16, 24), 10) || 0;
        const groupRecords = (grtIdx - grhIdx) + 1; // GRH … GRT inclusive
        const txnActual = lines.slice(grhIdx + 1, grtIdx).filter(x => x.startsWith('AGR')).length;
        if (txnDeclared !== txnActual) issues.push({ line: grtIdx, recordType: 'GRT', severity: 'error', category: 'SCHEMA', code: 'TRL-001', message: `GRT transactionCount ${txnDeclared} ≠ actual ${txnActual}`, fix: 'Rebuild trailer counts' });
        if (recDeclared !== groupRecords) issues.push({ line: grtIdx, recordType: 'GRT', severity: 'warning', category: 'SCHEMA', code: 'TRL-002', message: `GRT recordCount ${recDeclared} ≠ actual ${groupRecords}`, fix: 'Rebuild trailer counts' });
      }
    }
    if (trlIdx >= 0) {
      const trlLine = lines[trlIdx];
      const grpDeclared = parseInt(trlLine.slice(3, 8), 10) || 0;
      if (grpDeclared !== groupCount) issues.push({ line: trlIdx, recordType: 'TRL', severity: 'error', category: 'SCHEMA', code: 'TRL-003', message: `TRL groupCount ${grpDeclared} ≠ actual ${groupCount}` });
    }

    // 4. Per-agreement business rules
    (agreements || []).forEach((ag, i) => {
      const txnLines = lines
        .map((l, idx) => ({ l, idx, t: l.slice(0, 3) }))
        .filter(x => /^(AGR|TER|IPA|NPA|USA|AGM)$/.test(x.t) && parseInt(x.l.slice(3, 11), 10) === i);
      const agrLine = txnLines.find(x => x.t === 'AGR');
      const lineIdx = agrLine ? agrLine.idx : 0;

      // 4a. DATE LOGIC
      const start = ag.start && new Date(ag.start);
      const end = ag.end && new Date(ag.end);
      const ret = ag.retentionEnd && new Date(ag.retentionEnd);
      const sig = ag.dateOfSignature && new Date(ag.dateOfSignature);
      if (start && end && end < start) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'DTE-001', message: `Agreement end (${ag.end}) precedes start (${ag.start})`, fix: 'Correct AGR fields 7/8' });
      }
      if (end && ret && ret < end) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'warning', category: 'BIZ', code: 'DTE-002', message: 'Retention end precedes agreement end — collection right would expire mid-term', fix: 'Set retention ≥ agreement end' });
      }
      if (start && sig && sig > start) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'warning', category: 'BIZ', code: 'DTE-003', message: `Date of signature (${ag.dateOfSignature}) is after agreement start (${ag.start})`, fix: 'Verify chronology of execution vs effective date' });
      }
      if (ag.priorRoyaltyStatus === 'D' && !ag.priorRoyaltyStartDate) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'DTE-004', message: 'priorRoyaltyStatus=D requires priorRoyaltyStartDate', fix: 'Provide AGR field 11' });
      }
      if (ag.postTermCollectionStatus === 'D' && !ag.postTermCollectionEndDate) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'DTE-005', message: 'postTermCollectionStatus=D requires postTermCollectionEndDate', fix: 'Provide AGR field 13' });
      }

      // 4b. TER: at least one inclusion; no overlap I/E within same code
      const ters = (ag.territories || []);
      const inclusions = ters.filter(t => (t.inclusion || 'I') === 'I');
      const exclusions = ters.filter(t => t.inclusion === 'E');
      if (ters.length === 0) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'TER-001', message: 'No TER records — territory mandatory', fix: 'Add at least one territory' });
      } else if (inclusions.length === 0) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'TER-002', message: 'Only excluded territories — must include at least one', fix: 'Set inclusion=I on at least one TER' });
      }
      // TIS validity (numeric 4-digit)
      ters.forEach((t, k) => {
        const code = String(t.tisCode || '');
        if (!/^\d{4}$/.test(code)) {
          issues.push({ line: lineIdx, recordType: 'TER', severity: 'error', category: 'ID', code: 'TIS-001', message: `TER #${k+1}: TIS code "${code}" is not 4 digits`, fix: 'Use CISAC TIS numeric codes (e.g. 0840 = USA, 2136 = WORLD ex US)' });
        } else if (!Spec.TIS_TOP[code]) {
          issues.push({ line: lineIdx, recordType: 'TER', severity: 'info', category: 'ID', code: 'TIS-002', message: `TER #${k+1}: TIS code ${code} not in top-list (still valid; ref data only)`, fix: 'Confirm against full TIS table if rejected' });
        }
      });

      // 4c. IPA role matrix per agreement type
      const parties = ag.parties || [];
      const assignors = parties.filter(p => p.role === 'AS');
      const acquirers = parties.filter(p => p.role === 'AC');
      if (assignors.length === 0) issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'IPA-001', message: 'Agreement has no Assignor (AS)', fix: 'Add AS party (writer side)' });
      if (acquirers.length === 0) issues.push({ line: lineIdx, recordType: 'AGR', severity: 'error', category: 'BIZ', code: 'IPA-002', message: 'Agreement has no Acquirer (AC)', fix: 'Add AC party (publisher side)' });
      // Original-Specific & Original-General: exactly one AS, one AC by default
      if ((ag.agreementType === 'OS' || ag.agreementType === 'OG') && (assignors.length > 1)) {
        issues.push({ line: lineIdx, recordType: 'IPA', severity: 'warning', category: 'BIZ', code: 'ROL-001', message: `${ag.agreementType} typically has 1 Assignor; got ${assignors.length}`, fix: 'Verify role assignment' });
      }
      if (ag.agreementType === 'PG' && acquirers.length < 1) {
        issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'BIZ', code: 'ROL-002', message: 'Packaged-General must list each acquirer', fix: 'Add AC parties for each constituent' });
      }

      // 4d. SHARE CONSISTENCY per right per territory
      // For each right (PR/MR/SR), assignor share + acquirer share ≤ 100% per territory.
      ['pr', 'mr', 'sr'].forEach(rt => {
        const sumAS = assignors.reduce((s, p) => s + (Number(p[rt + 'Share']) || 0), 0);
        const sumAC = acquirers.reduce((s, p) => s + (Number(p[rt + 'Share']) || 0), 0);
        if (sumAS + sumAC > 100.01) {
          issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'BIZ', code: 'SHR-001', message: `${rt.toUpperCase()} shares total ${(sumAS+sumAC).toFixed(2)}% (>100%)`, fix: 'Recompute splits — assignor + acquirer must not exceed 100% per right' });
        } else if (sumAS > 0 && sumAS + sumAC < 99.99 && sumAC > 0) {
          issues.push({ line: lineIdx, recordType: 'IPA', severity: 'warning', category: 'BIZ', code: 'SHR-002', message: `${rt.toUpperCase()} shares total ${(sumAS+sumAC).toFixed(2)}% (<100%) — residual unassigned`, fix: 'Verify intended residual or correct shares' });
        }
        // Per-side bounds (basis points)
        [...assignors, ...acquirers].forEach(p => {
          const v = Number(p[rt + 'Share']) || 0;
          if (v < 0 || v > 100) {
            issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'BIZ', code: 'SHR-003', message: `${rt.toUpperCase()} share for ${p.lastName || p.partyId} is ${v}% (out of 0-100)`, fix: 'Constrain share to 0-100%' });
          }
        });
      });

      // 4e. IPI MOD-101 check digits
      [...assignors, ...acquirers].forEach((p, k) => {
        if (p.ipiNameNumber) {
          const ipi = String(p.ipiNameNumber).replace(/\s/g, '');
          if (!/^\d{11}$/.test(ipi)) {
            issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'ID', code: 'IPI-001', message: `IPI Name # "${ipi}" is not 11 digits`, fix: 'IPI Name # = 11 digits (9 base + 2 mod-101 check)' });
          } else if (!ipiNameValid(ipi)) {
            issues.push({ line: lineIdx, recordType: 'IPA', severity: 'warning', category: 'ID', code: 'IPI-002', message: `IPI Name # ${ipi} fails mod-101 check digit`, fix: 'Recompute check digit on first 9 base digits' });
          }
        }
        if (p.ipiBaseNumber && String(p.ipiBaseNumber).trim()) {
          if (!ipiBaseValid(p.ipiBaseNumber)) {
            issues.push({ line: lineIdx, recordType: 'IPA', severity: 'warning', category: 'ID', code: 'IPI-003', message: `IPI Base # "${p.ipiBaseNumber}" failed format/check`, fix: 'IPI Base # is I-9digit-checkDigit' });
          }
        }
        // Society code (3-digit lookup)
        ['prSoc', 'mrSoc', 'srSoc'].forEach(s => {
          const v = p[s];
          if (v != null && v !== '' && v !== '000') {
            const code = String(v).padStart(3, '0');
            if (!/^\d{3}$/.test(code)) {
              issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'ID', code: 'SOC-001', message: `${s} "${code}" is not a 3-digit numeric society code`, fix: 'Use CISAC society codes' });
            } else if (!Spec.SOCIETY_CODES[code]) {
              issues.push({ line: lineIdx, recordType: 'IPA', severity: 'warning', category: 'ID', code: 'SOC-002', message: `${s} ${code} not in CISAC society lookup table`, fix: 'Verify against CISAC member directory' });
            }
          }
        });
      });

      // 4f. CHARACTER-SET enforcement
      if (!isV3) {
        const checkAscii = (val, where) => {
          if (val && /[^\x00-\x7F]/.test(val)) {
            issues.push({ line: lineIdx, recordType: 'IPA', severity: 'error', category: 'CHARSET', code: 'CHR-001', message: `${where} contains non-ASCII chars (CAF v1.2 is ASCII-only)`, fix: 'Use NPA non-Roman party record for the original script' });
          }
        };
        parties.forEach(p => {
          if (!p.nonRomanName) {
            checkAscii(p.lastName, 'IPA lastName');
            checkAscii(p.firstName, 'IPA firstName');
          }
        });
      }

      // 4g. USA — at least one usage right
      const usas = ag.usages || [];
      if (usas.length === 0) {
        issues.push({ line: lineIdx, recordType: 'AGR', severity: 'warning', category: 'BIZ', code: 'USA-001', message: 'Agreement declares no usage rights (USA records)', fix: 'Add at least one USA record (commonly PR + MR)' });
      }
      usas.forEach((u, k) => {
        if (!Spec.RIGHT_CODES[u.rightCode]) {
          issues.push({ line: lineIdx, recordType: 'USA', severity: 'error', category: 'FIELD', code: 'USA-002', message: `USA #${k+1}: rightCode "${u.rightCode}" not in spec`, fix: `Allowed: ${Object.keys(Spec.RIGHT_CODES).join(', ')}` });
        }
      });
    });

    // 5. Summary + per-record-type stats (for dashboard)
    const errors = issues.filter(i => i.severity === 'error').length;
    const warnings = issues.filter(i => i.severity === 'warning').length;
    const infos = issues.filter(i => i.severity === 'info').length;
    const byRecord = {};
    for (const t of ['HDR','GRH','AGR','TER','IPA','NPA','USA','AGM','GRT','TRL','ACK','MSG','VER']) {
      byRecord[t] = { errors: 0, warnings: 0, total: 0 };
    }
    issues.forEach(i => {
      const b = byRecord[i.recordType] = byRecord[i.recordType] || { errors:0, warnings:0, total:0 };
      b.total += 1;
      if (i.severity === 'error') b.errors += 1;
      if (i.severity === 'warning') b.warnings += 1;
    });
    const byCategory = {};
    issues.forEach(i => {
      byCategory[i.category] = byCategory[i.category] || { errors:0, warnings:0, total:0 };
      byCategory[i.category].total += 1;
      if (i.severity === 'error') byCategory[i.category].errors += 1;
      if (i.severity === 'warning') byCategory[i.category].warnings += 1;
    });
    return {
      issues,
      summary: { errors, warnings, infos, total: issues.length, ok: errors === 0 },
      byRecord, byCategory,
    };
  }

  window.CafValidate = { validate, validateLine, ipiNameCheckDigit, ipiNameValid, ipiBaseCheckDigit, ipiBaseValid };
})();
