// cwr-caf-formats.jsx — JSON / XML compatibility layer for CWR + CAF
// ─────────────────────────────────────────────────────────────────
// Three responsibilities:
//   1. CWR flat-file ↔ JSON sibling (CWR 3.x official spec)
//   2. CWR flat-file ↔ XML
//   3. CAF flat-file ↔ JSON / ↔ XML (CAF v3.0 spec uses JSON envelope)
//
// All exporters take builder output (line[] for CWR, {lines, recordObjects}
// for CAF) and emit text. Importers do the reverse.
//
// EXPORT: window.CwrCafFormats = { ... }
// ─────────────────────────────────────────────────────────────────
(function () {
  'use strict';
  if (typeof window === 'undefined') return;

  // ─── Shared XML helpers ─────────────────────────────────────────
  function xmlEsc(s) {
    if (s == null) return '';
    return String(s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
  }
  function xmlAttr(k, v) { return v == null || v === '' ? '' : ` ${k}="${xmlEsc(v)}"`; }
  function indent(n) { return '  '.repeat(n); }

  // Generic { @attr: ... , child: ... } → XML emitter.
  function objToXml(obj, tagName, level) {
    level = level || 0;
    const pad = indent(level);
    if (obj == null) return `${pad}<${tagName}/>`;
    if (typeof obj !== 'object') return `${pad}<${tagName}>${xmlEsc(obj)}</${tagName}>`;
    if (Array.isArray(obj)) return obj.map(x => objToXml(x, tagName, level)).join('\n');

    const attrs = [];
    const children = [];
    for (const [k, v] of Object.entries(obj)) {
      if (k.startsWith('@')) attrs.push(`${k.slice(1)}="${xmlEsc(v)}"`);
      else if (v == null || v === '') continue;
      else children.push([k, v]);
    }
    const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
    if (children.length === 0) return `${pad}<${tagName}${attrStr}/>`;
    const inner = children.map(([k, v]) => objToXml(v, k, level + 1)).join('\n');
    return `${pad}<${tagName}${attrStr}>\n${inner}\n${pad}</${tagName}>`;
  }

  // Tiny XML → plain-object parser (good enough for our well-formed exports).
  // Uses DOMParser — available in browsers.
  function xmlToObj(text) {
    const doc = new DOMParser().parseFromString(text, 'application/xml');
    const err = doc.querySelector('parsererror');
    if (err) throw new Error('XML parse error: ' + err.textContent.slice(0, 120));
    return elemToObj(doc.documentElement);
  }
  function elemToObj(el) {
    const obj = {};
    for (const a of Array.from(el.attributes)) obj['@' + a.name] = a.value;
    const children = Array.from(el.children);
    if (children.length === 0) {
      const text = el.textContent.trim();
      if (Object.keys(obj).length === 0) return text;
      if (text) obj['#text'] = text;
      return obj;
    }
    for (const c of children) {
      const childObj = elemToObj(c);
      if (obj[c.tagName] == null) obj[c.tagName] = childObj;
      else if (Array.isArray(obj[c.tagName])) obj[c.tagName].push(childObj);
      else obj[c.tagName] = [obj[c.tagName], childObj];
    }
    return obj;
  }

  // ─── CWR: lines → structured JSON tree ──────────────────────────
  // Walks decoded lines and groups them into transmission → groups → transactions.
  function cwrLinesToTree(lines, version) {
    if (!window.CwrDecode) throw new Error('CwrDecode not loaded');
    const decoded = lines.map(l => window.CwrDecode.decodeLine(l, version)).filter(Boolean);
    const tree = {
      '@version': version || '2.1',
      header: null,
      groups: [],
      trailer: null,
    };
    let curGroup = null;
    let curTxn = null;
    for (const r of decoded) {
      const rt = r.recordType;
      if (rt === 'HDR') tree.header = stripFields(r);
      else if (rt === 'TRL') tree.trailer = stripFields(r);
      else if (rt === 'GRH') { curGroup = { ...stripFields(r), transactions: [] }; tree.groups.push(curGroup); curTxn = null; }
      else if (rt === 'GRT') { if (curGroup) curGroup.trailer = stripFields(r); curTxn = null; }
      else if (['NWR','REV','ISW','EXC','AGR'].includes(rt)) {
        curTxn = { '@type': rt, ...stripFields(r), records: [] };
        if (curGroup) curGroup.transactions.push(curTxn);
      } else {
        // Detail record — attach to current transaction
        if (curTxn) curTxn.records.push({ '@type': rt, ...stripFields(r) });
      }
    }
    return tree;
  }
  function stripFields(rec) {
    const out = {};
    for (const [k, v] of Object.entries(rec.fields || {})) {
      if (v == null || v === '') continue;
      out[k] = v;
    }
    return out;
  }

  // ─── CWR exporters ──────────────────────────────────────────────
  function cwrToJson(lines, version) {
    const tree = cwrLinesToTree(lines, version);
    return JSON.stringify({ cwr: tree }, null, 2);
  }

  function cwrToXml(lines, version) {
    const tree = cwrLinesToTree(lines, version);
    const out = ['<?xml version="1.0" encoding="UTF-8"?>'];
    out.push(`<cwr version="${xmlEsc(tree['@version'])}">`);
    if (tree.header) out.push(objToXml(tree.header, 'header', 1));
    for (const g of tree.groups) {
      const groupAttrs = ` type="${xmlEsc(g.transactionType || '')}" id="${xmlEsc(g.groupId || '')}"`;
      out.push(`${indent(1)}<group${groupAttrs}>`);
      for (const t of g.transactions) {
        const tType = t['@type'];
        const txnCopy = { ...t }; delete txnCopy['@type']; delete txnCopy.records;
        out.push(`${indent(2)}<transaction type="${tType}">`);
        out.push(objToXml(txnCopy, 'header', 3));
        for (const r of (t.records || [])) {
          const rType = r['@type'];
          const rCopy = { ...r }; delete rCopy['@type'];
          out.push(objToXml(rCopy, rType.toLowerCase(), 3));
        }
        out.push(`${indent(2)}</transaction>`);
      }
      if (g.trailer) out.push(objToXml(g.trailer, 'groupTrailer', 2));
      out.push(`${indent(1)}</group>`);
    }
    if (tree.trailer) out.push(objToXml(tree.trailer, 'trailer', 1));
    out.push('</cwr>');
    return out.join('\n');
  }

  // ─── CAF exporters ──────────────────────────────────────────────
  // CafBuild already exposes buildJsonSibling. We add an XML-style sibling.
  function cafToJson(opts) {
    if (!window.CafBuild) throw new Error('CafBuild not loaded');
    return JSON.stringify(window.CafBuild.buildJsonSibling(opts), null, 2);
  }
  function cafToXml(opts) {
    if (!window.CafBuild) throw new Error('CafBuild not loaded');
    const env = window.CafBuild.buildJsonSibling(opts);
    const caf = env.caf;
    const out = ['<?xml version="1.0" encoding="UTF-8"?>'];
    const ver = caf['@version'] || '3.0';
    out.push(`<caf version="${xmlEsc(ver)}" charset="${xmlEsc(caf['@charset'] || 'UTF-8')}">`);
    if (caf.header)      out.push(objToXml(caf.header, 'header', 1));
    if (caf.groupHeader) out.push(objToXml(caf.groupHeader, 'groupHeader', 1));
    out.push(`${indent(1)}<agreements>`);
    for (const a of (caf.agreements || [])) {
      out.push(`${indent(2)}<agreement>`);
      out.push(objToXml(a.agreement || {}, 'fields', 3));
      if (a.territories?.length) {
        out.push(`${indent(3)}<territories>`);
        a.territories.forEach(t => out.push(objToXml(t, 'territory', 4)));
        out.push(`${indent(3)}</territories>`);
      }
      if (a.parties?.length) {
        out.push(`${indent(3)}<parties>`);
        a.parties.forEach(p => out.push(objToXml(p, 'party', 4)));
        out.push(`${indent(3)}</parties>`);
      }
      if (a.usages?.length) {
        out.push(`${indent(3)}<usages>`);
        a.usages.forEach(u => out.push(objToXml(u, 'usage', 4)));
        out.push(`${indent(3)}</usages>`);
      }
      if (a.messages?.length) {
        out.push(`${indent(3)}<messages>`);
        a.messages.forEach(m => out.push(objToXml(m, 'message', 4)));
        out.push(`${indent(3)}</messages>`);
      }
      out.push(`${indent(2)}</agreement>`);
    }
    out.push(`${indent(1)}</agreements>`);
    if (caf.groupTrailer) out.push(objToXml(caf.groupTrailer, 'groupTrailer', 1));
    if (caf.trailer)      out.push(objToXml(caf.trailer, 'trailer', 1));
    out.push('</caf>');
    return out.join('\n');
  }

  // ─── Importers (round-trip support) ─────────────────────────────
  function jsonToCwrTree(text)  { return JSON.parse(text).cwr || JSON.parse(text); }
  function jsonToCafTree(text)  { return JSON.parse(text).caf || JSON.parse(text); }
  function xmlToCwrTree(text)   { return xmlToObj(text); }
  function xmlToCafTree(text)   { return xmlToObj(text); }

  // Round-trip detection — does parsed JSON/XML look like a valid CWR/CAF tree?
  function detectFormat(text) {
    const t = (text || '').trim();
    if (!t) return null;
    if (t.startsWith('<?xml') || t.startsWith('<')) {
      const m = t.match(/<(cwr|caf)[\s>]/);
      if (m) return { kind: m[1], format: 'xml' };
      return { kind: 'unknown', format: 'xml' };
    }
    if (t.startsWith('{') || t.startsWith('[')) {
      try {
        const o = JSON.parse(t);
        if (o.cwr) return { kind: 'cwr', format: 'json' };
        if (o.caf) return { kind: 'caf', format: 'json' };
        return { kind: 'unknown', format: 'json' };
      } catch (_) { return null; }
    }
    // Heuristic for flat file: lines starting with HDR/CAH
    const firstLine = t.split('\n')[0] || '';
    if (firstLine.startsWith('HDR')) return { kind: 'cwr', format: 'flat' };
    if (firstLine.startsWith('CAH')) return { kind: 'caf', format: 'flat' };
    return null;
  }

  // ─── Public API ────────────────────────────────────────────────
  window.CwrCafFormats = {
    cwrToJson, cwrToXml, cwrLinesToTree,
    cafToJson, cafToXml,
    jsonToCwrTree, jsonToCafTree,
    xmlToCwrTree, xmlToCafTree,
    detectFormat,
    objToXml, xmlToObj,
  };
  console.log('[CwrCafFormats] loaded');
})();
