/**
 * pres/edit/fill — token replacement in an existing .pptx.
 *
 * Strategy:
 *   1. Unzip the source pptx in memory.
 *   2. For each slide XML, scan for `{{tokenName}}` placeholders.
 *      A simple text-substitution pass is enough because PPTX wraps
 *      runs in `<a:t>` and the tokens are typically inside a single
 *      run when authored carefully.
 *   3. PowerPoint sometimes splits a token across runs when the user
 *      edits mid-token, leaving `<a:t>{{na</a:t><a:t>me}}</a:t>`. We
 *      collapse adjacent runs inside the same `<a:r>` before the scan
 *      to recover the original token. (This won't recover splits
 *      across run-property changes — we surface those as `unresolved`.)
 *   4. Re-zip and write to the destination path.
 *
 * The agent's call:
 *   { sourcePath, outputPath, values: { customerName: 'Acme', ... } }
 *
 * Returns { path, size, replaced: {token:count}, unresolved: [...] }.
 */
import fs from 'fs/promises';
import { createRequire } from 'module';

const localRequire = createRequire(import.meta.url);
let JSZip = null;
function loadZip() {
  if (JSZip) return JSZip;
  JSZip = localRequire('jszip');
  return JSZip;
}

function escXml(s) {
  return String(s)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

/**
 * Best-effort heal of split tokens. Looks for `<a:t>...</a:t><a:t>...</a:t>`
 * pairs inside the same `<a:r>` block and merges them, on the
 * assumption that adjacent text runs without intervening run-prop
 * changes are safe to collapse.
 *
 * Does not touch runs that have different rPr — those would lose
 * formatting if merged.
 */
function healSplitTokens(xml) {
  // Inside a single <a:r>: <a:rPr.../><a:t>foo</a:t> can't precede
  // another <a:t> without a closing </a:r>. So if we see two <a:t>'s
  // in a row INSIDE the same <a:r>, that's actually an error PowerPoint
  // sometimes produces. Merge them.
  return xml.replace(
    /(<a:t[^>]*>)([\s\S]*?)<\/a:t>\s*<a:t[^>]*>([\s\S]*?)<\/a:t>/g,
    (match, openTag, a, b) => {
      // Only heal when at least one side has token-fragment markers.
      if (/\{\{|\}\}/.test(a + b)) {
        return `${openTag}${a}${b}</a:t>`;
      }
      return match;
    }
  );
}

function applyTokens(xml, values, counters) {
  const unresolved = new Set();
  const replaced = xml.replace(
    /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g,
    (full, name) => {
      if (Object.prototype.hasOwnProperty.call(values, name)) {
        counters[name] = (counters[name] || 0) + 1;
        return escXml(values[name]);
      }
      unresolved.add(name);
      return full;
    }
  );
  return { xml: replaced, unresolved: [...unresolved] };
}

/**
 * Fill placeholders in a .pptx with provided values.
 *
 *   { sourcePath, outputPath, values }
 *   → { path, size, replaced: {token: count}, unresolved: [tokens never matched] }
 */
export async function fillPres({ sourcePath, outputPath, values = {} }) {
  if (!sourcePath) throw new Error('fillPres: sourcePath is required');
  if (!outputPath) throw new Error('fillPres: outputPath is required');

  const Zip = loadZip();
  const buf = await fs.readFile(sourcePath);
  const zip = await Zip.loadAsync(buf);

  const slidePaths = Object.keys(zip.files)
    .filter(p => /^ppt\/slides\/slide\d+\.xml$/.test(p));

  const replaced = {};
  const unresolvedAll = new Set();

  for (const p of slidePaths) {
    let xml = await zip.file(p).async('string');
    xml = healSplitTokens(xml);
    const { xml: out, unresolved } = applyTokens(xml, values, replaced);
    unresolved.forEach(u => unresolvedAll.add(u));
    zip.file(p, out);
  }

  const outBuf = await zip.generateAsync({ type: 'nodebuffer' });
  await fs.writeFile(outputPath, outBuf);
  const stat = await fs.stat(outputPath);

  return {
    path: outputPath,
    size: stat.size,
    replaced,
    unresolved: [...unresolvedAll],
  };
}
