/* Report Builder — exporters: PPTX (native/editable), PDF (16:9 snapshot),
   XLSX (multi-sheet) + optional ZIP-of-CSVs. All three ship together (v1).
   Heavy libraries lazy-load on first export so the dashboard stays light.
   Everything derives from (reportDef, dataset, brandKit) — same source as
   the live preview. PDF page = 13.333×7.5in landscape, never A4.
======================================================================== */

/* ---------- Lazy CDN loader ------------------------------------------- */
const R_LIBS = {
  pptx:        { url:'https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js', test:()=>window.PptxGenJS },
  jspdf:       { url:'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js',       test:()=>window.jspdf },
  html2canvas: { url:'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',test:()=>window.html2canvas },
  jszip:       { url:'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js',           test:()=>window.JSZip },
};
const _rLibP = {};
function rLoadLib(name){
  const lib = R_LIBS[name];
  if (lib.test()) return Promise.resolve();
  if (_rLibP[name]) return _rLibP[name];
  _rLibP[name] = new Promise((res, rej)=>{
    const s = document.createElement('script');
    s.src = lib.url; s.async = true;
    s.onload = ()=> lib.test() ? res() : rej(new Error(name+' failed to initialise'));
    s.onerror = ()=> rej(new Error('Could not load '+name+' (network)'));
    document.head.appendChild(s);
  });
  return _rLibP[name];
}

/* ---------- Shared helpers --------------------------------------------- */
function rExportFilename(def, ds, ext){
  const brand = (def.brandName || (ds.account && ds.account.name) || 'Report').trim();
  const period = def.scope.start ? (def.scope.start + ' to ' + def.scope.end) : '';
  const base = `${brand} — ${period} — Performance Report`.replace(/[\\/:*?"<>|]/g,'-');
  return base + '.' + ext;
}
function rDownloadBlob(blob, filename){
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  document.body.appendChild(a); a.click();
  setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 2000);
}
const _imgCache = {};
async function rImgData(path){
  if (!path) return null;
  if (_imgCache[path]) return _imgCache[path];
  try {
    const res = await fetch(path, { credentials:'include' });
    if (!res.ok) return null;
    const blob = await res.blob();
    const data = await new Promise((r)=>{ const fr = new FileReader(); fr.onload=()=>r(fr.result); fr.readAsDataURL(blob); });
    const dims = await new Promise((r)=>{ const im = new Image(); im.onload=()=>r({w:im.naturalWidth,h:im.naturalHeight}); im.onerror=()=>r(null); im.src=data; });
    _imgCache[path] = { data, dims };
    return _imgCache[path];
  } catch(e){ return null; }
}
/* Platform brand icons for PPTX: render the React SVG offscreen, rasterize
   to PNG. color resolves `currentColor` (use #fff on branded slides). */
const _iconPngCache = {};
async function rBrandIconPng(icon, color){
  const key = icon + '|' + color;
  if (_iconPngCache[key] !== undefined) return _iconPngCache[key];
  let out = null;
  const host = document.createElement('div');
  host.style.cssText = 'position:fixed;left:-9999px;top:0;color:' + color + ';';
  document.body.appendChild(host);
  const root = ReactDOM.createRoot(host);
  try {
    await new Promise(res=>{ root.render(React.createElement(BrandIcon, { name:icon, size:256 })); setTimeout(res, 80); });
    const svg = host.querySelector('svg');
    if (svg){
      svg.setAttribute('xmlns','http://www.w3.org/2000/svg');
      const ser = new XMLSerializer().serializeToString(svg).replace(/currentColor/g, color);
      out = await new Promise(res=>{
        const im = new Image();
        im.onload = ()=>{
          try {
            const c = document.createElement('canvas'); c.width = 256; c.height = 256;
            c.getContext('2d').drawImage(im, 0, 0, 256, 256);
            res(c.toDataURL('image/png'));
          } catch(e){ res(null); }
        };
        im.onerror = ()=>res(null);
        im.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(ser);
      });
    }
  } catch(e){ out = null; }
  root.unmount(); host.remove();
  _iconPngCache[key] = out;
  return out;
}

/* Platform mark for exports: official PNG first, rasterized SVG fallback. */
async function rPlatformImg(platform, fallbackColor){
  const p = typeof rPlatformLogo==='function' ? rPlatformLogo(platform) : null;
  if (p){ const im = await rImgData(p); if (im) return im.data; }
  return await rBrandIconPng(rPlatform(platform).icon, fallbackColor || '#1A1A17');
}

/* Preflight: slides referencing platforms with no rows in range. */
function rPreflight(def, ds){
  const have = new Set(rDatasetPlatforms(ds));
  const warned = new Set();
  const out = [];
  def.slides.forEach((s,i)=>{
    const plats = new Set();
    if (s.platform) plats.add(s.platform);
    (s.blocks||[]).forEach(b=>{ if (b.binding && b.binding.platform) plats.add(b.binding.platform); });
    plats.forEach(p=>{
      if (!have.has(p) && !warned.has(p+':'+i)){
        warned.add(p+':'+i);
        out.push({ slide:i+1, title:s.title, platform:p,
          msg:`Slide ${i+1} “${s.title}” references ${rPlatform(p).label}, which has no data in this range.` });
      }
    });
  });
  return out;
}

/* ============================ XLSX / CSV ============================ */
const R_XLSX_METRICS = ['spend','impressions','reach','clicks','link_clicks','ctr','cpc','cpm',
  'conversions','cpa','cvr','revenue','roas','video_views','engagements','leads'];
function _sheetName(s){ return s.replace(/[\\/*?:\[\]]/g,'').slice(0,31); }
function _entityTable(ds, level, platform, currency){
  const XM = R_XLSX_METRICS.filter(k=>!rMetricBlockedFor(ds.account.id, k));
  const { rows } = rEntityRows(ds, level, { platform, sortKey:'spend', sortDir:'desc' });
  const head = [ level==='campaign'?'Campaign':level==='adset'?'Ad Set':'Ad',
    ...(level!=='campaign' ? ['Parent'] : []), 'Status',
    ...XM.map(k=>rMetric(k).label + (rMetric(k).fmt.startsWith('currency')?` (${currency})`:'')) ];
  const campaignsById = Object.fromEntries((ds.entities.campaigns||[]).map(c=>[c.id,c.name]));
  const adsetsById = Object.fromEntries((ds.entities.adsets||[]).map(a=>[a.id,a.name]));
  const body = rows.map(r=>[
    r.name,
    ...(level==='adset' ? [campaignsById[r.campaign_id]||''] :
        level==='ad' ? [adsetsById[r.adset_id]||''] : []),
    r.status||'',
    ...XM.map(k=>{
      const v = rMetricValue(r.cur,k);
      return rMetric(k).fmt==='pct' || rMetric(k).fmt==='x' || rMetric(k).fmt==='currency2'
        ? +v.toFixed(3) : Math.round(v*100)/100;
    }),
  ]);
  return [head, ...body];
}
function rBuildWorkbookSheets(def, ds){
  const currency = ds.account.currency || 'USD';
  /* per-account blocklist (e.g. Al Munawarah: no ROAS in any export) */
  const XM = R_XLSX_METRICS.filter(k=>!rMetricBlockedFor(ds.account.id, k));
  const sheets = [];   // [name, aoa]
  /* Summary */
  const plats = rByPlatform(ds);
  const tot = rTotals(ds);
  const summary = [
    ['Account', ds.account.name],
    ['Brand', def.brandName || ds.account.name],
    ['Currency', currency],
    ['Period', ds.range.start + ' → ' + ds.range.end],
    ['Comparison period', ds.compare.start + ' → ' + ds.compare.end],
    ['Generated', new Date().toISOString().slice(0,16).replace('T',' ')],
    [],
    ['Platform', ...XM.map(k=>rMetric(k).label), 'Δ Spend %', 'Δ Results %'],
    ...plats.map(p=>{
      const dS = rDelta(rMetricValue(p.cur,'spend'), rMetricValue(p.prev,'spend'),'spend');
      const dC = rDelta(rMetricValue(p.cur,'conversions'), rMetricValue(p.prev,'conversions'),'conversions');
      return [ rPlatform(p.platform).label,
        ...XM.map(k=>+rMetricValue(p.cur,k).toFixed(3)),
        dS?+dS.pct.toFixed(1):'', dC?+dC.pct.toFixed(1):'' ];
    }),
    [ 'TOTAL', ...XM.map(k=>+rMetricValue(tot.cur,k).toFixed(3)),
      (()=>{ const d=rDelta(rMetricValue(tot.cur,'spend'),rMetricValue(tot.prev,'spend'),'spend'); return d?+d.pct.toFixed(1):''; })(),
      (()=>{ const d=rDelta(rMetricValue(tot.cur,'conversions'),rMetricValue(tot.prev,'conversions'),'conversions'); return d?+d.pct.toFixed(1):''; })() ],
  ];
  const brand = rBrand(ds);
  if (brand.hasData){
    summary.push([], ['Followers gained (period)']);
    brand.platforms.forEach(p=>summary.push([rPlatform(p).label, brand.gained[p]||0,
      brand.latest[p]!=null?('total '+brand.latest[p]):'' ]));
    summary.push(['Combined', brand.totalGained, brand.totalFollowers||'']);
  }
  sheets.push(['Summary', summary]);
  /* Per-platform entity sheets in locked order */
  rDatasetPlatforms(ds).forEach(p=>{
    const P = rPlatform(p).label;
    [['campaign','Campaigns'],['adset','Ad Sets'],['ad','Ads']].forEach(([lvl,lbl])=>{
      const table = _entityTable(ds, lvl, p, currency);
      if (table.length > 1) sheets.push([_sheetName(`${P} ${lbl}`), table]);
    });
  });
  /* Daily data */
  const daily = [ ['Date','Platform',...XM.filter(k=>rMetric(k).agg==='sum').map(k=>rMetric(k).label)],
    ...(ds.daily||[]).map(r=>[ r.date, rPlatform(r.platform).label,
      ...XM.filter(k=>rMetric(k).agg==='sum').map(k=>Number(r[k])||0) ]) ];
  sheets.push(['Daily Data', daily]);
  /* Definitions */
  const defs = [ ['Metric','Definition / formula'],
    ...rAllMetrics().map(m=>{
      let f = 'Reported by platform (summed daily)';
      if (m.agg && m.agg.num) f = `${rMetric(m.agg.num).label} ÷ ${rMetric(m.agg.den).label}` + (m.agg.mult?` × ${m.agg.mult}`:'');
      if (m.agg && m.agg.formula) f = 'Custom: ' + m.agg.formula;
      if (m.agg === 'last') f = 'Latest reported value (not summed)';
      return [m.label, f + (m.note ? ' — ' + m.note : '')];
    }) ];
  sheets.push(['Definitions', defs]);
  return sheets;
}
async function rExportXLSX(def, ds, opts){
  const sheets = rBuildWorkbookSheets(def, ds);
  if (opts && opts.csvZip){
    await rLoadLib('jszip');
    const zip = new JSZip();
    sheets.forEach(([name, aoa])=>{
      const ws = XLSX.utils.aoa_to_sheet(aoa);
      zip.file(name.replace(/[^\w؀-ۿ -]/g,'') + '.csv', '﻿' + XLSX.utils.sheet_to_csv(ws));
    });
    const blob = await zip.generateAsync({ type:'blob' });
    rDownloadBlob(blob, rExportFilename(def, ds, 'zip'));
    return;
  }
  const wb = XLSX.utils.book_new();
  sheets.forEach(([name, aoa])=>{
    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!cols'] = (aoa[0]||[]).map((_,i)=>({ wch: i===0?34:14 }));
    XLSX.utils.book_append_sheet(wb, ws, name);
  });
  const out = XLSX.write(wb, { bookType:'xlsx', type:'array' });
  rDownloadBlob(new Blob([out], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),
    rExportFilename(def, ds, 'xlsx'));
}

/* ============================ PDF ============================ */
/* Mounts selected slides offscreen at full 1280×720, snapshots each at 2x. */
async function rExportPDF(def, ds, kit, slideIds, onProgress){
  await Promise.all([rLoadLib('jspdf'), rLoadLib('html2canvas')]);
  const theme = rTheme(kit, def.lang);
  const currency = ds.account.currency || 'USD';
  const slides = def.slides.filter(s=>slideIds.includes(s.id));
  const host = document.createElement('div');
  host.style.cssText = 'position:fixed;left:-99999px;top:0;z-index:-1;';
  document.body.appendChild(host);
  const root = ReactDOM.createRoot(host);
  const renderAll = ()=> new Promise(res=>{
    root.render(
      <div>
        {slides.map(s=>(
          <div key={s.id} data-export-slide={s.id}>
            <RSlide def={def} slide={s} ds={ds} theme={theme} currency={currency} editable={false}/>
          </div>
        ))}
      </div>
    );
    setTimeout(res, 650);   // charts render synchronously (animations off); settle fonts/images
  });
  try {
    await renderAll();
    try { await (document.fonts && document.fonts.ready); } catch(e){}
    const { jsPDF } = window.jspdf;
    const pdf = new jsPDF({ orientation:'landscape', unit:'in', format:[13.333, 7.5] });
    for (let i=0; i<slides.length; i++){
      onProgress && onProgress(`Rendering slide ${i+1}/${slides.length}…`);
      const node = host.querySelector(`[data-export-slide="${slides[i].id}"] .r-slide`);
      const canvas = await html2canvas(node, { scale:2, backgroundColor:'#ffffff',
        useCORS:true, logging:false, windowWidth:R_SLIDE_W,
        /* ARABIC FIX: html2canvas draws text glyph-by-glyph whenever
           letter-spacing ≠ normal, which destroys Arabic ligatures/shaping
           ("disconnected characters"). Normalize tracking in the export clone
           only — on-screen styling is untouched. */
        onclone: (doc)=>{
          doc.querySelectorAll('.r-slide, .r-slide *').forEach(el=>{
            el.style.letterSpacing = 'normal';
          });
        } });
      const img = canvas.toDataURL('image/jpeg', 0.92);
      if (i > 0) pdf.addPage([13.333,7.5], 'landscape');
      pdf.addImage(img, 'JPEG', 0, 0, 13.333, 7.5);
    }
    pdf.save(rExportFilename(def, ds, 'pdf'));
  } finally {
    root.unmount();
    host.remove();
  }
}

/* ============================ PPTX ============================ */
const IN_W = 13.333, IN_H = 7.5, MARGIN = 0.55;
const PX = (px)=> px / R_SLIDE_W * IN_W;   // logical px → inches
function _hex(c){ return String(c||'#000000').replace('#',''); }

async function rExportPPTX(def, ds, kit, slideIds, onProgress){
  await rLoadLib('pptx');
  const theme = rTheme(kit, def.lang);
  const lang = def.lang, rtl = theme.rtl;
  const currency = ds.account.currency || 'USD';
  const pptx = new PptxGenJS();
  pptx.defineLayout({ name:'WIDE', width:IN_W, height:IN_H });
  pptx.layout = 'WIDE';
  pptx.author = 'Relay';
  pptx.title = def.name;
  const T = (o)=> Object.assign({ fontFace: rtl ? 'Sakkal Majalla' : 'Calibri',
    lang: rtl ? 'ar-SA' : 'en-US', rtlMode: rtl }, o);
  const alignStart = rtl ? 'right' : 'left';
  const bgImg   = rBrandedBg(theme) ? await rImgData(rBrandedBg(theme)) : null;
  const logo    = theme.logo ? await rImgData(theme.logo) : null;
  const logoDark= theme.logoDark ? await rImgData(theme.logoDark) : null;
  const logoRatio = (img)=> img && img.dims ? img.dims.w/img.dims.h : 3;
  const periodStr = def.scope.start ? rDateLabel(def.scope.start,lang)+' – '+rDateLabel(def.scope.end,lang) : '';
  const slides = def.slides.filter(s=>slideIds.includes(s.id));
  const middle = def.slides.filter(s=>!['cover','agenda','thank_you'].includes(s.type));

  const brandedBase = (s)=>{
    if (bgImg){ s.background = { data: bgImg.data }; return; }
    s.background = { color:_hex(theme.main) };
    /* brand-derived geometry when the kit has no cover artwork */
    const ex = rtl ? -2.2 : IN_W - 4.3;
    s.addShape(pptx.ShapeType.ellipse, { x:ex+1.5, y:-2.4, w:6.5, h:6.5,
      fill:{ color:_hex(theme.secondary), transparency:86 }, line:{ type:'none' } });
    s.addShape(pptx.ShapeType.ellipse, { x:ex+2.2, y:4.6, w:4.5, h:4.5,
      fill:{ color:_hex(theme.main), transparency:100 }, line:{ color:_hex(rMix(theme.tertiary, theme.main, 0.35)), width:2 } });
    s.addShape(pptx.ShapeType.ellipse, { x:ex, y:1.15, w:1.9, h:1.9,
      fill:{ color:_hex(theme.tertiary), transparency:82 }, line:{ type:'none' } });
  };
  const onBrand = _hex(theme.onMain);
  /* agency credit ("Prepared by FLNT") — separate from the client brand */
  const agency = def.agency && def.agency.enabled ? def.agency : null;
  const agencyLogo = agency && agency.logo ? await rImgData(agency.logo) : null;
  const addAgency = (s, x, y, align)=>{
    if (!agency) return;
    let ax = x;
    if (agencyLogo){
      const w = Math.min(0.24 * (agencyLogo.dims ? agencyLogo.dims.w/agencyLogo.dims.h : 3), 0.9);
      s.addImage({ data:agencyLogo.data, x: align==='center' ? ax - w/2 - 0.9 : ax + 1.05, y:y-0.015, h:0.24, w });
    }
    s.addText([
      { text:'Prepared by  ', options:{ transparency:30 } },
      { text: agency.name || 'FLNT', options:{ bold:true } },
    ], T({ x: align==='center' ? ax-2 : ax, y, w:4, h:0.3, fontSize:10.5, color:onBrand,
      align: align || 'left', rtlMode:false, lang:'en-US' }));
  };

  for (let i=0; i<slides.length; i++){
    const sl = slides[i];
    onProgress && onProgress(`Building slide ${i+1}/${slides.length}…`);
    const s = pptx.addSlide();

    if (sl.type === 'cover'){
      brandedBase(s);
      const lg = logoDark || logo;
      if (lg) s.addImage({ data:lg.data, x: rtl ? IN_W-MARGIN-1.9 : MARGIN, y:0.95,
        h:1.15, w:1.15*logoRatio(lg) > 3.8 ? 3.8 : 1.15*logoRatio(lg) });
      if (def.brandName) s.addText(def.brandName.toUpperCase(), T({ x:MARGIN, y:2.6, w:IN_W-MARGIN*2, h:0.45,
        fontSize:15, color:onBrand, transparency:20, charSpacing:4, bold:true, align:alignStart }));
      s.addText(sl.title || rStr('performance_report',lang), T({ x:MARGIN, y:3.0, w:IN_W-MARGIN*2, h:1.7,
        fontSize:40, bold:true, color:onBrand, align:alignStart }));
      if (periodStr) s.addText(rStr('exec_summary',lang)+' — '+periodStr, T({ x:MARGIN, y:4.75, w:IN_W-MARGIN*2, h:0.55,
        fontSize:16, color:onBrand, transparency:10, align:alignStart }));
      s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.0 : MARGIN, y:5.45, w:1.0, h:0.08,
        fill:{ color:_hex(theme.secondary) }, line:{ type:'none' }, rectRadius:0.04 });
      addAgency(s, MARGIN, IN_H-0.62, 'left');

    } else if (sl.type === 'thank_you'){
      brandedBase(s);
      const lg = logoDark || logo;
      if (lg){ const w = Math.min(1.55*logoRatio(lg), 4.2);
        s.addImage({ data:lg.data, x:(IN_W-w)/2, y:1.75, h:1.55, w }); }
      s.addText(sl.title || rStr('thank_you',lang), T({ x:1, y:3.65, w:IN_W-2, h:1.1,
        fontSize:40, bold:true, color:onBrand, align:'center' }));
      if (def.brandName) s.addText(def.brandName, T({ x:1, y:4.85, w:IN_W-2, h:0.55,
        fontSize:16, color:onBrand, transparency:15, align:'center' }));
      addAgency(s, IN_W/2, 5.55, 'center');

    } else if (sl.type === 'divider'){
      brandedBase(s);
      const dividers = def.slides.filter(x=>x.type==='divider');
      const secNum = String(dividers.indexOf(sl)+1).padStart(2,'0');
      /* oversized section-number watermark */
      s.addText(secNum, T({ x: rtl ? 0.1 : IN_W-4.7, y:IN_H-3.3, w:4.6, h:3.3,
        fontSize:190, bold:true, color:onBrand, transparency:91,
        align: rtl?'left':'right', rtlMode:false, lang:'en-US' }));
      /* platform logo (official PNG in a white chip) */
      if (sl.platform){
        const ic = await rPlatformImg(sl.platform, '#ffffff');
        if (ic){
          s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.05 : MARGIN, y:1.5, w:1.05, h:1.05,
            fill:{ color:'FFFFFF', transparency:8 }, line:{ type:'none' }, rectRadius:0.18,
            shadow:{ type:'outer', blur:8, offset:2, angle:90, color:'000000', opacity:0.3 } });
          s.addImage({ data:ic, x: (rtl ? IN_W-MARGIN-1.05 : MARGIN)+0.17, y:1.67, w:0.71, h:0.71 });
        }
      }
      s.addText((rtl ? '' : 'SECTION ') + secNum, T({ x:MARGIN, y:2.72, w:IN_W-MARGIN*2, h:0.35,
        fontSize:12, bold:true, color:onBrand, transparency:35, charSpacing:3, align:alignStart, rtlMode:false, lang:'en-US' }));
      s.addText(sl.title, T({ x:MARGIN, y:3.05, w:IN_W-MARGIN*2, h:1.4,
        fontSize:40, bold:true, color:onBrand, align:alignStart }));
      s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-1.2 : MARGIN, y:4.6, w:1.2, h:0.1,
        fill:{ color:_hex(theme.secondary) }, line:{ type:'none' }, rectRadius:0.05 });

    } else if (sl.type === 'agenda'){
      await _pptxHeader(pptx, s, sl.title || rStr('agenda',lang), theme, T, alignStart, periodStr, logo, null);
      const rowsPerCol = Math.ceil(middle.length/2);
      const rowH = Math.min(0.72, 5.4/Math.max(rowsPerCol,1));   // spread down the slide
      middle.forEach((m, idx)=>{
        const col = Math.floor(idx/rowsPerCol), row = idx % rowsPerCol;
        let x = MARGIN + col * ((IN_W-MARGIN*2)/2 + 0.15);
        const y = 1.7 + row * rowH;
        s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W - x - 0.42 : x, y:y+0.03, w:0.42, h:0.42,
          fill:{ color:_hex(theme.main) }, line:{ type:'none' }, rectRadius:0.08 });
        s.addText(String(idx+1).padStart(2,'0'), T({ x: rtl ? IN_W - x - 0.42 : x, y:y+0.03, w:0.42, h:0.42,
          fontSize:12, bold:true, color:onBrand, align:'center', valign:'middle' }));
        s.addText((m.type==='divider' ? (lang==='ar'?'قسم: ':'Section: ') : '') + m.title,
          T({ x: rtl ? IN_W - x - ((IN_W-MARGIN*2)/2 - 0.3) : x+0.58, y, w:(IN_W-MARGIN*2)/2 - 0.65, h:0.48,
            fontSize:15, bold:true, color:'33332E', align:alignStart, valign:'middle' }));
      });

    } else {
      await _pptxHeader(pptx, s, sl.title, theme, T, alignStart, periodStr, logo, sl.platform || null);
      /* block layout cursor mirroring the DOM 12-col grid;
         a single block gets the whole content area (tall) */
      let y = 1.6;
      const blocks = sl.blocks || [];
      const tall = blocks.length === 1;
      let bi = 0;
      while (bi < blocks.length && y < IN_H - 0.6){
        const blk = blocks[bi];
        const meta = R_BLOCKS[blk.type] || { span:12 };
        const span = (blk.options && blk.options.span) || meta.span;
        const next = blocks[bi+1];
        const nextSpan = next ? ((next.options && next.options.span) || (R_BLOCKS[next.type]||{span:12}).span) : 12;
        if (span <= 6 && nextSpan <= 6 && next){
          const wHalf = (IN_W - MARGIN*2 - 0.3)/2;
          const h1 = await _pptxBlock(pptx, s, blk,  def, ds, theme, T, { x:MARGIN, y, w:wHalf }, currency, alignStart);
          const h2 = await _pptxBlock(pptx, s, next, def, ds, theme, T, { x:MARGIN+wHalf+0.3, y, w:wHalf }, currency, alignStart);
          y += Math.max(h1, h2) + 0.25;
          bi += 2;
        } else {
          const h = await _pptxBlock(pptx, s, blk, def, ds, theme, T, { x:MARGIN, y, w:IN_W-MARGIN*2, tall }, currency, alignStart);
          y += h + 0.25;
          bi += 1;
        }
      }
    }
    /* light-slide chrome: top brand strip + footer (brand · agency · page) */
    if (!['cover','thank_you','divider'].includes(sl.type)){
      s.addShape(pptx.ShapeType.rect, { x:0, y:0, w:IN_W, h:0.055,
        fill:{ color:_hex(theme.main) }, line:{ type:'none' } });
      s.addShape(pptx.ShapeType.rect, { x:IN_W*0.55, y:0, w:IN_W*0.45, h:0.055,
        fill:{ color:_hex(theme.tertiary) }, line:{ type:'none' } });
      s.addShape(pptx.ShapeType.rect, { x:MARGIN, y:IN_H-0.42, w:IN_W-MARGIN*2, h:0.008,
        fill:{ color:'ECECE6' }, line:{ type:'none' } });
      s.addText(def.brandName || '', T({ x:MARGIN, y:IN_H-0.38, w:4, h:0.28,
        fontSize:9.5, bold:true, color:'A3A39A', align:'left', rtlMode:false }));
      const pageTxt = agency
        ? [{ text:'Prepared by ', options:{ color:'A3A39A' } },
           { text:(agency.name||'FLNT'), options:{ bold:true, color:'8B8B80' } },
           { text:'   ·   ' + (def.slides.indexOf(sl)+1) + ' / ' + def.slides.length, options:{ color:'A3A39A' } }]
        : [{ text:(def.slides.indexOf(sl)+1) + ' / ' + def.slides.length, options:{ color:'A3A39A' } }];
      s.addText(pageTxt, T({ x:IN_W-5-MARGIN, y:IN_H-0.38, w:5, h:0.28,
        fontSize:9.5, align:'right', rtlMode:false, lang:'en-US' }));
    }
  }
  onProgress && onProgress('Writing file…');
  await pptx.writeFile({ fileName: rExportFilename(def, ds, 'pptx') });
}

async function _pptxHeader(pptx, s, title, theme, T, alignStart, periodStr, logo, platform){
  s.background = { color:'FBFBF9' };
  const rtl = theme.rtl;
  s.addShape(pptx.ShapeType.roundRect, { x: rtl ? IN_W-MARGIN-0.07 : MARGIN, y:0.45, w:0.07, h:0.55,
    fill:{ color:_hex(theme.main) }, line:{ type:'none' }, rectRadius:0.035 });
  s.addText(title, T({ x: rtl ? MARGIN : MARGIN+0.2, y:0.38, w:IN_W-MARGIN*2-1.8, h:0.6,
    fontSize:24, bold:true, color:'22221E', align:alignStart }));
  if (periodStr) s.addText(periodStr, T({ x: rtl ? MARGIN : MARGIN+0.2, y:0.95, w:IN_W-MARGIN*2-1.8, h:0.32,
    fontSize:11, color:'9A9A90', align:alignStart }));
  /* platform logo (before the kit logo, on the header's end side) */
  let endX = rtl ? MARGIN : IN_W - MARGIN;
  if (logo){
    const w = Math.min(0.5 * (logo.dims ? logo.dims.w/logo.dims.h : 3), 1.8);
    s.addImage({ data:logo.data, x: rtl ? endX : endX - w, y:0.42, h:0.5, w });
    endX = rtl ? endX + w + 0.18 : endX - w - 0.18;
  }
  if (platform){
    const ic = await rPlatformImg(platform, '#1A1A17');
    if (ic) s.addImage({ data:ic, x: rtl ? endX : endX - 0.46, y:0.44, w:0.46, h:0.46 });
  }
}

/* Render one block natively; returns consumed height in inches. */
async function _pptxBlock(pptx, s, blk, def, ds, theme, T, box, currency, alignStart){
  const lang = def.lang;
  const b = blk.binding || {};
  const type = blk.type;

  if (type === 'scorecard' || type === 'metric_grid'){
    const t = b.platform ? rTotals(ds, b.platform) : rTotals(ds);
    const metrics = (b.metrics && b.metrics.length ? b.metrics : ['spend','conversions','cpa','roas']);
    const cols = metrics.length <= 4 ? metrics.length : Math.ceil(metrics.length/2);
    const rows = Math.ceil(metrics.length/cols);
    const big = box.tall && metrics.length <= 4;      // solo scorecards fill the slide
    const gap = big ? 0.26 : 0.2;
    const cardW = (box.w - gap*(cols-1))/cols;
    const cardH = big ? 3.4 : 1.45;
    const cy0 = big ? box.y + 0.6 : box.y;
    metrics.forEach((k, i)=>{
      const m = rMetric(k); if (!m) return;
      const cx = box.x + (i%cols)*(cardW+gap), cy = cy0 + Math.floor(i/cols)*(cardH+gap);
      /* Total Followers = brand metric (brand_metrics_daily), not ad sums */
      let cur, prev, d, followersSub = null;
      if (k === 'followers'){
        const br = rBrand(ds);
        cur = br.totalFollowers || br.totalGained || 0;
        prev = null; d = null;
        followersSub = br.totalGained ? '+' + br.totalGained.toLocaleString('en-US') + '  ' + rStr('followers_by_platform',lang) : null;
      } else {
        cur = rMetricValue(t.cur,k); prev = rMetricValue(t.prev,k);
        d = rDelta(cur, prev, k);
      }
      const padTop = big ? 0.85 : 0.1;
      s.addShape(pptx.ShapeType.roundRect, { x:cx, y:cy, w:cardW, h:cardH,
        fill:{ color:'FFFFFF' }, line:{ color:'E7E7E2', width:1 }, rectRadius:0.1 });
      s.addShape(pptx.ShapeType.roundRect, { x:cx+0.16, y:cy+(big?0.55:0.09), w:big?0.55:0.42, h:0.05,
        fill:{ color:_hex(theme.ramp[i % theme.ramp.length]) }, line:{ type:'none' }, rectRadius:0.025 });
      s.addText(rMetricLabel(k,lang), T({ x:cx+0.16, y:cy+padTop, w:cardW-0.32, h:0.34,
        fontSize: big?13:11, bold:true, color:'6D6D64', align:alignStart }));
      s.addText(rCompact(cur, k==='followers' ? 'int' : m.fmt, currency, lang),
        T({ x:cx+0.16, y:cy+padTop+0.32, w:cardW-0.32, h:big?0.7:0.5,
        fontSize: big?30:23, bold:true, color:_hex(theme.main), align:alignStart }));
      if (d) s.addText((d.up?'▲ ':'▼ ') + Math.abs(d.pct).toFixed(1) + '%  ' + rStr('vs_prev',lang),
        T({ x:cx+0.16, y:cy+padTop+(big?1.1:0.86), w:cardW-0.32, h:0.3, fontSize: big?11:9.5, bold:true,
          color: d.tone==='good' ? '0C7A43' : d.tone==='bad' ? 'B42323' : '5E5E55', align:alignStart }));
      else if (followersSub) s.addText(followersSub,
        T({ x:cx+0.16, y:cy+padTop+(big?1.1:0.86), w:cardW-0.32, h:0.3, fontSize: big?11:9.5, bold:true,
          color:'0C7A43', align:alignStart }));
    });
    return big ? 5.2 : rows*(cardH+gap);
  }

  if (type === 'trend'){
    const keys = (b.metrics && b.metrics.length ? b.metrics : ['spend']);
    const data = rSeries(ds, keys, b.platform);
    if (!data.length) return _pptxEmpty(pptx, s, box, T, lang);
    const labels = data.map(r=>r.label);
    const series = keys.map((k,i)=>({ name: rMetricLabel(k,lang), labels, values: data.map(r=>+(r[k]||0).toFixed(3)) }));
    const showCompare = blk.options && blk.options.showCompare !== false;
    if (showCompare) keys.forEach((k)=>{
      series.push({ name: rMetricLabel(k,lang)+' (prev)', labels, values: data.map(r=>+(r['_cmp_'+k]||0).toFixed(3)) });
    });
    const colors = keys.map((k,i)=>_hex(theme.ramp[i%theme.ramp.length]));
    const h = box.tall ? 5.3 : 3.2;
    s.addChart(pptx.ChartType.line, series, { x:box.x, y:box.y, w:box.w, h,
      chartColors: showCompare ? [...colors, ...colors] : colors,
      lineSize: 2.25, lineSmooth: true, lineDataSymbol:'none',
      catAxisLabelFontSize: 10, valAxisLabelFontSize: 10, legendFontSize: 11,
      showLegend: true, legendPos:'b',
      valGridLine:{ color:'E5E5E0', style:'solid', size:1 }, catGridLine:{ style:'none' } });
    return h;
  }

  if (type === 'bar'){
    const key = b.metric || 'spend';
    let items;
    if ((b.dimension||'platform')==='platform'){
      items = rByPlatform(ds).map(p=>({ name:(lang==='ar'?rPlatform(p.platform).ar:rPlatform(p.platform).label),
        v:rMetricValue(p.cur,key) }));
    } else {
      const { rows } = rEntityRows(ds, b.level||'campaign', { platform:b.platform, sortKey:key, topN:b.topN||8 });
      items = rows.map(r=>({ name:r.name, v:rMetricValue(r.cur,key) }));
    }
    items = items.filter(d=>d.v>0);
    if (!items.length) return _pptxEmpty(pptx, s, box, T, lang);
    const h = box.tall ? 5.3 : 3.0;
    s.addChart(pptx.ChartType.bar, [{ name:rMetricLabel(key,lang), labels:items.map(d=>d.name),
      values:items.map(d=>+d.v.toFixed(2)) }], { x:box.x, y:box.y, w:box.w, h,
      chartColors: items.map((_,i)=>_hex(theme.ramp[i%theme.ramp.length])),
      barDir:'col', showLegend:false, catAxisLabelFontSize:10, valAxisLabelFontSize:10,
      valGridLine:{ color:'E5E5E0', style:'solid', size:1 } });
    return h;
  }

  if (type === 'donut'){
    const key = b.metric || 'spend';
    const items = rByPlatform(ds).map(p=>({ name:(lang==='ar'?rPlatform(p.platform).ar:rPlatform(p.platform).label),
      v:rMetricValue(p.cur,key) })).filter(d=>d.v>0);
    if (!items.length) return _pptxEmpty(pptx, s, box, T, lang);
    const h = box.tall ? 5.2 : 3.0;
    s.addText(rStr('spend_share',lang), T({ x:box.x, y:box.y-0.05, w:box.w, h:0.32, fontSize:12.5, bold:true, color:'4B4B44', align:alignStart }));
    s.addChart(pptx.ChartType.doughnut, [{ name:rMetricLabel(key,lang), labels:items.map(d=>d.name),
      values:items.map(d=>+d.v.toFixed(2)) }], { x:box.x, y:box.y+0.3, w:box.w, h:h-0.35,
      chartColors: items.map((_,i)=>_hex(theme.ramp[i%theme.ramp.length])),
      holeSize: 60, showLegend:true, legendPos: theme.rtl?'l':'r', legendFontSize:11,
      showPercent:true, dataLabelFontSize:10 });
    return h;
  }

  if (type === 'table'){
    const metrics = b.metrics && b.metrics.length ? b.metrics : ['spend','impressions','clicks','ctr','conversions','cpa'];
    const res = rEntityRows(ds, b.level||'campaign', { platform:b.platform,
      sortKey:b.sortKey||'spend', sortDir:b.sortDir||'desc', topN:Math.min(b.topN||10, 12), filters:b.filters||[] });
    const rows = res.others ? [...res.rows, res.others] : res.rows;
    if (!rows.length) return _pptxEmpty(pptx, s, box, T, lang);
    const heat = (blk.options && blk.options.heatmap) || [];
    const maxOf = {}; heat.forEach(k=>{ maxOf[k] = Math.max(...rows.map(r=>rMetricValue(r.cur,k)), 0.0001); });
    const nameHead = b.level==='ad'?rStr('ads',lang):b.level==='adset'?rStr('adsets',lang):rStr('campaigns',lang);
    const headFill = _hex(rMix('#FFFFFF', theme.main, 0.08));
    const headRow = [ { text:nameHead, options:{ bold:true, color:'55554E', fill:{color:headFill}, align:alignStart } },
      ...metrics.map(k=>({ text:rMetricLabel(k,lang), options:{ bold:true, color:'55554E', fill:{color:headFill}, align: theme.rtl?'left':'right' } })) ];
    const bodyRows = rows.map((r,ri)=>[
      { text:String(r.name).slice(0,48), options:{ bold:true, color:'33332E', align:alignStart, italic:!!r._others,
        fill: ri%2===1 ? { color:'FAFAF8' } : undefined } },
      ...metrics.map(k=>{
        const m = rMetric(k), v = rMetricValue(r.cur,k);
        let fill = ri%2===1 ? { color:'FAFAF8' } : undefined;
        if (heat.includes(k) && !r._others){
          fill = { color:_hex(rMix('#FFFFFF', theme.tertiary, 0.14 + 0.5*Math.min(1, v/maxOf[k]))) };
        }
        return { text:rFmt(v, m?m.fmt:'int', currency, lang), options:{ align: theme.rtl?'left':'right', fill, color:'3D3D37' } };
      }),
    ]);
    const rowH = box.tall ? Math.min(0.44, 5.3/(rows.length+1)) : 0.32;
    const h = (rows.length+1)*rowH + 0.12;
    s.addTable([headRow, ...bodyRows], { x:box.x, y:box.y, w:box.w,
      colW: [box.w*0.3, ...metrics.map(()=> box.w*0.7/metrics.length)],
      fontSize: box.tall ? 11 : 10, fontFace: T({}).fontFace, border:{ type:'solid', color:'EFEFE9', pt:0.5 },
      rowH, valign:'middle', margin:0.05 });
    return h;
  }

  if (type === 'ranked_list'){
    const key = b.metric || 'impressions', sec = b.secondary || 'ctr';
    const mk = rMetric(key), ms = rMetric(sec);
    const { rows } = rEntityRows(ds, b.level||'campaign', { platform:b.platform, sortKey:key, sortDir:'desc', topN:b.topN||5 });
    if (!rows.length) return _pptxEmpty(pptx, s, box, T, lang);
    const total = rows.reduce((a,r)=>a+rMetricValue(r.cur,key),0);
    const rowH = box.tall ? Math.min(0.95, (5.3 - 0.14*rows.length)/rows.length) : 0.72;
    rows.forEach((r,i)=>{
      const y = box.y + i*(rowH+0.14);
      const v = rMetricValue(r.cur,key), share = total>0? v/total*100 : 0;
      s.addShape(pptx.ShapeType.roundRect, { x:box.x, y, w:box.w, h:rowH,
        fill:{color:'FFFFFF'}, line:{color:'E7E7E2', width:1}, rectRadius:0.08 });
      const badge = Math.min(0.52, rowH-0.2);
      const bx = theme.rtl ? box.x + box.w - badge - 0.16 : box.x + 0.16;
      s.addShape(pptx.ShapeType.ellipse, { x:bx, y:y+(rowH-badge)/2, w:badge, h:badge, fill:{color:_hex(theme.secondary)}, line:{type:'none'} });
      s.addText(String(i+1), T({ x:bx, y:y+(rowH-badge)/2, w:badge, h:badge, fontSize:15, bold:true,
        color:_hex(rContrastText(theme.secondary)), align:'center', valign:'middle' }));
      const tx = theme.rtl ? box.x + 0.18 : box.x + badge + 0.34;
      const tw = box.w - badge - 0.6;
      s.addText(String(r.name).slice(0,60), T({ x:tx, y:y+0.07, w:tw, h:rowH*0.45, fontSize:14, bold:true, color:'2B2B26', align:alignStart }));
      s.addText(`${rCompact(v, mk?mk.fmt:'int', currency, lang)} — ${share.toFixed(1)}% • ${rMetricLabel(sec,lang)} ${rFmt(rMetricValue(r.cur,sec), ms?ms.fmt:'pct', currency, lang)}`,
        T({ x:tx, y:y+rowH*0.5, w:tw, h:rowH*0.4, fontSize:10.5, color:'7C7C72', align:alignStart }));
    });
    return rows.length*(rowH+0.14);
  }

  if (type === 'brand'){
    const br = rBrand(ds);
    if (!br.hasData) return _pptxEmpty(pptx, s, box, T, lang);
    s.addText(rStr('followers_by_platform',lang), T({ x:box.x, y:box.y, w:box.w, h:0.28, fontSize:10.5, bold:true, color:'4B4B44', align:alignStart }));
    let y = box.y + 0.32;
    br.platforms.forEach(p=>{
      s.addText((lang==='ar'?rPlatform(p).ar:rPlatform(p).label), T({ x:box.x, y, w:box.w*0.55, h:0.26, fontSize:10, bold:true, color:'3D3D37', align:alignStart }));
      s.addText((br.gained[p]?'+'+br.gained[p].toLocaleString('en-US'):'—') +
        (br.latest[p]!=null ? `  (${Number(br.latest[p]).toLocaleString('en-US')})` : ''),
        T({ x: theme.rtl ? box.x : box.x+box.w*0.5, y, w:box.w*0.5, h:0.26, fontSize:10, bold:true,
          color:_hex(theme.main), align: theme.rtl?'left':'right' }));
      y += 0.28;
    });
    s.addText(rStr('total_followers',lang) + '  ' +
      (br.totalFollowers ? br.totalFollowers.toLocaleString('en-US') : '+'+br.totalGained.toLocaleString('en-US')),
      T({ x:box.x, y:y+0.05, w:box.w, h:0.28, fontSize:10.5, bold:true, color:_hex(theme.main), align:alignStart }));
    return (y + 0.4) - box.y;
  }

  if (type === 'heatmap'){
    /* approximate: compact table of weeks × days with fills */
    const key = b.metric || 'spend';
    const series = rSeries(ds, [key], b.platform);
    if (!series.length) return _pptxEmpty(pptx, s, box, T, lang);
    const weeks = {};
    series.forEach(r=>{
      const d = new Date(r.date+'T00:00:00'); const dow = d.getDay();
      const ws = new Date(d); ws.setDate(d.getDate()-dow);
      const wk = ws.toISOString().slice(0,10);
      (weeks[wk] = weeks[wk] || Array(7).fill(null))[dow] = r[key];
    });
    const wkeys = Object.keys(weeks).sort();
    const max = Math.max(...series.map(r=>r[key]||0), 0.0001);
    const days = lang==='ar' ? ['أحد','إثن','ثلا','أرب','خمي','جمع','سبت'] : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
    const tRows = [
      [ {text:'', options:{fill:{color:'FFFFFF'}}}, ...wkeys.map(w=>({ text:new Date(w+'T00:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'}),
        options:{ fontSize:7, color:'9A9A90', align:'center' } })) ],
      ...days.map((dl,dow)=>[
        { text:dl, options:{ fontSize:7.5, color:'8B8B80' } },
        ...wkeys.map(w=>{
          const v = weeks[w][dow];
          const fill = v==null ? 'F4F4F0' : _hex(rMix('#FFFFFF', theme.main, 0.12+0.72*Math.min(1,v/max)));
          return { text: v!=null && v/max>0.02 ? rCompact(v,'int',currency,'en') : '',
            options:{ fill:{color:fill}, fontSize:6.5, align:'center',
              color:_hex(rContrastText('#'+fill)) } };
        }),
      ]),
    ];
    const hmRowH = box.tall ? 0.6 : 0.3;
    const h = 8*hmRowH + 0.1;
    s.addTable(tRows, { x:box.x, y:box.y, w:box.w, rowH:hmRowH, border:{ type:'solid', color:'FFFFFF', pt:1 },
      fontFace:T({}).fontFace, margin:0.02, valign:'middle' });
    return h;
  }

  if (type === 'perf_flags'){
    const perf = rPerfInsights(ds, b.level||'ad', { platform:b.platform, lang, currency });
    if (!perf) return _pptxEmpty(pptx, s, box, T, lang);
    const half = (box.w - 0.25) / 2;
    const card = (x, tone, label, name, reasons)=>{
      s.addShape(pptx.ShapeType.roundRect, { x, y:box.y, w:half, h:1.55,
        fill:{ color:tone.bg }, line:{ color:tone.border, width:1 }, rectRadius:0.1 });
      s.addText(label, T({ x:x+0.15, y:box.y+0.08, w:half-0.3, h:0.3,
        fontSize:10, bold:true, color:tone.fg, charSpacing:2, align:alignStart }));
      s.addText(String(name).slice(0,60), T({ x:x+0.15, y:box.y+0.38, w:half-0.3, h:0.34,
        fontSize:13, bold:true, color:'26261F', align:alignStart }));
      s.addText(reasons.slice(0,3).join('   •   '), T({ x:x+0.15, y:box.y+0.74, w:half-0.3, h:0.72,
        fontSize:9.5, color:tone.fg, align:alignStart, valign:'top' }));
    };
    const bestLbl  = lang==='ar' ? '👑 الأفضل أداءً' : (lang==='both' ? '👑 الأفضل أداءً · TOP PERFORMER' : '👑 TOP PERFORMER');
    const worstLbl = lang==='ar' ? '⚠️ يحتاج معالجة' : (lang==='both' ? '⚠️ يحتاج معالجة · NEEDS ATTENTION' : '⚠️ NEEDS ATTENTION');
    card(box.x, { bg:'F2F9F2', border:'BFE3BF', fg:'1D7A34' }, bestLbl, perf.best.name, perf.best.reasons);
    card(box.x+half+0.25, { bg:'FDF5F0', border:'F0CDB8', fg:'B4560F' }, worstLbl, perf.worst.name, perf.worst.reasons);
    return 1.7;
  }

  if (type === 'priorities'){
    const items = (blk.options && blk.options.items && blk.options.items.length)
      ? blk.options.items : rRecommendations(ds, lang, currency);
    if (!items.length) return _pptxEmpty(pptx, s, box, T, lang);
    const PRI = { high:{ bar:'D9482B', label: lang==='ar'?'عالية':'HIGH' },
                  med: { bar:'E2A615', label: lang==='ar'?'متوسطة':'MED' },
                  low: { bar:'4F9A67', label: lang==='ar'?'منخفضة':'LOW' } };
    const cols = 2, gap = 0.22;
    const cardW = (box.w - gap)/cols;
    const rowsN = Math.ceil(items.length/cols);
    const cardH = Math.min(1.65, (5.3 - gap*(rowsN-1)) / rowsN);
    items.forEach((it, i)=>{
      const p = PRI[it.pri] || PRI.med;
      const cx = box.x + (i%cols)*(cardW+gap), cy = box.y + Math.floor(i/cols)*(cardH+gap);
      s.addShape(pptx.ShapeType.roundRect, { x:cx, y:cy, w:cardW, h:cardH,
        fill:{ color:'FFFFFF' }, line:{ color:'E7E7E2', width:1 }, rectRadius:0.09 });
      s.addShape(pptx.ShapeType.rect, { x: theme.rtl ? cx+cardW-0.07 : cx, y:cy, w:0.07, h:cardH,
        fill:{ color:p.bar }, line:{ type:'none' } });
      s.addText(String(it.title), T({ x:cx+0.18, y:cy+0.06, w:cardW-1.0, h:0.3,
        fontSize:12, bold:true, color:'26261F', align:alignStart }));
      s.addText(p.label, T({ x: theme.rtl ? cx+0.14 : cx+cardW-0.85, y:cy+0.08, w:0.68, h:0.24,
        fontSize:8, bold:true, color:p.bar, align:'center', rtlMode:false }));
      s.addText([
        { text: String(it.why).replace(/\n/g,' '), options:{ color:'55554E', breakLine:true } },
        { text: '→ ' + String(it.action).replace(/\n/g,' '), options:{ bold:true, color:_hex(theme.main) } },
      ], T({ x:cx+0.18, y:cy+0.38, w:cardW-0.36, h:cardH-0.46, fontSize:9, align:alignStart, valign:'top' }));
    });
    return rowsN*(cardH+gap);
  }

  if (type === 'text'){
    const o = blk.options || {};
    let bullets = o.bullets && o.bullets.length ? o.bullets : (o.autoInsights ? rAutoInsights(ds, lang, currency) : []);
    const items = [];
    if (o.text) items.push({ text:o.text, options:T({ fontSize:15.5, color:'3A3A34', align:alignStart, breakLine:true }) });
    bullets.forEach(t=>items.push({ text:t, options:T({ fontSize:15, color:'3A3A34', align:alignStart,
      bullet:{ code:'2022', indent:16 }, breakLine:true, paraSpaceAfter:12 }) }));
    if (!items.length) return 0.1;
    const h = Math.min(0.6 + items.length*(box.tall?0.9:0.65), 5.3);
    s.addText(items, T({ x:box.x, y:box.y, w:box.w, h, valign:'top' }));
    return h;
  }

  if (type === 'image'){
    const o = blk.options || {};
    if (!o.src) return 0.1;
    const img = await rImgData(o.src);
    if (!img) return 0.1;
    const ratio = img.dims ? img.dims.w/img.dims.h : 1.6;
    const h = Math.min(3.4, box.w/ratio);
    s.addImage({ data:img.data, x:box.x + (box.w - h*ratio)/2, y:box.y, w:h*ratio, h });
    return h;
  }

  return 0.1;
}
function _pptxEmpty(pptx, s, box, T, lang){
  s.addShape(pptx.ShapeType.roundRect, { x:box.x, y:box.y, w:box.w, h:0.8,
    fill:{color:'FAFAF7'}, line:{ color:'D9D9D4', width:1, dashType:'dash' }, rectRadius:0.09 });
  s.addText(rStr('no_data',lang), T({ x:box.x, y:box.y, w:box.w, h:0.8, fontSize:10.5,
    color:'8B8B80', align:'center', valign:'middle' }));
  return 0.9;
}

/* ============================ Export modal ============================ */
function ReportExportModal({ open, onClose, def, ds, kit }){
  const lang = def ? def.lang : 'en';
  const [format, setFormat] = useState('pptx');
  const [csvZip, setCsvZip] = useState(false);
  const [sel, setSel] = useState({});
  const [busy, setBusy] = useState(false);
  const [progress, setProgress] = useState('');
  const [error, setError] = useState('');
  useEffect(()=>{
    if (open && def) setSel(Object.fromEntries(def.slides.map(s=>[s.id, true])));
    setError(''); setProgress('');
  }, [open]);
  if (!open || !def || !ds) return null;
  const warnings = rPreflight(def, ds);
  const selIds = def.slides.filter(s=>sel[s.id]).map(s=>s.id);
  const allOn = selIds.length === def.slides.length;
  const typeIcon = { cover:'file', agenda:'columns', exec_summary:'sparkles', divider:'chev-right',
    content:'columns', thank_you:'check' };
  const doExport = async ()=>{
    setBusy(true); setError('');
    try {
      if (format==='xlsx') await rExportXLSX(def, ds, { csvZip });
      else if (format==='pdf') await rExportPDF(def, ds, kit, selIds, setProgress);
      else await rExportPPTX(def, ds, kit, selIds, setProgress);
      onClose();
    } catch(e){
      console.error('[report export]', e);
      setError(e.message || 'Export failed');
    }
    setBusy(false); setProgress('');
  };
  const fmts = [
    { id:'pptx', label:'PowerPoint (.pptx)', desc:'Native editable slides & charts' },
    { id:'pdf',  label:'PDF (16:9)',         desc:'Pixel-identical to the preview' },
    { id:'xlsx', label:'Excel (.xlsx)',      desc:'Multi-sheet data workbook' },
  ];
  return (
    <Modal open={open} onClose={busy?()=>{}:onClose} width={640}>
      <div className="p-5">
        <div className="text-[16px] font-bold mb-1">Export report</div>
        <div className="text-[12px] text-ink-500 dark:text-ink-300 mb-4">{def.name}</div>

        <div className="grid grid-cols-3 gap-2 mb-4">
          {fmts.map(f=>(
            <button key={f.id} onClick={()=>setFormat(f.id)}
              className={`text-left p-3 rounded-xl border transition ${format===f.id
                ? 'border-accent-400 bg-accent-50 dark:bg-accent-500/10'
                : 'border-ink-200 dark:border-white/10 hover:border-ink-300'}`}>
              <div className="text-[12.5px] font-bold">{f.label}</div>
              <div className="text-[10.5px] text-ink-500 dark:text-ink-300 mt-0.5">{f.desc}</div>
            </button>
          ))}
        </div>
        {format==='xlsx' && (
          <label className="flex items-center gap-2 mb-4 text-[12.5px] cursor-pointer">
            <Toggle checked={csvZip} onChange={setCsvZip} size="sm"/>
            Download as ZIP of CSV files instead (one file per sheet)
          </label>
        )}

        {format!=='xlsx' && (
          <div className="mb-4">
            <div className="flex items-center justify-between mb-2">
              <div className="text-[12.5px] font-semibold">Slides ({selIds.length}/{def.slides.length})</div>
              <button className="text-[11.5px] text-accent-600 dark:text-accent-300 font-semibold"
                onClick={()=>setSel(Object.fromEntries(def.slides.map(s=>[s.id, !allOn])))}>
                {allOn ? 'Deselect all' : 'Select all'}
              </button>
            </div>
            <div className="max-h-[220px] overflow-y-auto scroll-soft grid grid-cols-2 gap-1.5">
              {def.slides.map((s,i)=>(
                <label key={s.id} className={`flex items-center gap-2 px-2.5 py-2 rounded-lg border cursor-pointer transition
                  ${sel[s.id] ? 'border-accent-300 bg-accent-50/50 dark:bg-accent-500/10' : 'border-ink-200 dark:border-white/10 opacity-60'}`}>
                  <input type="checkbox" checked={!!sel[s.id]} onChange={e=>setSel(v=>({ ...v, [s.id]:e.target.checked }))}
                    className="accent-current"/>
                  <Icon name={typeIcon[s.type]||'file'} size={13} className="text-ink-400 flex-shrink-0"/>
                  <span className="text-[11.5px] font-medium truncate" dir="auto">{i+1}. {s.title}</span>
                </label>
              ))}
            </div>
          </div>
        )}

        {warnings.length > 0 && (
          <div className="mb-4 px-3 py-2.5 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/25">
            {warnings.slice(0,3).map((w,i)=>(
              <div key={i} className="text-[11.5px] text-amber-700 dark:text-amber-300">{w.msg}</div>
            ))}
          </div>
        )}
        {error && <div className="mb-3 text-[12px] text-rose-600 dark:text-rose-300">{error}</div>}

        <div className="flex items-center gap-3">
          <div className="text-[11px] text-ink-400 font-mono truncate flex-1">
            {busy ? progress || 'Working…' : rExportFilename(def, ds, format==='xlsx' ? (csvZip?'zip':'xlsx') : format)}
          </div>
          <Btn variant="ghost" onClick={onClose} className={busy?'opacity-40 pointer-events-none':''}>Cancel</Btn>
          <Btn variant="accent" icon={busy?undefined:'chev-right'} onClick={doExport}
            className={busy?'opacity-60 pointer-events-none':''}>
            {busy ? 'Exporting…' : 'Export'}
          </Btn>
        </div>
      </div>
    </Modal>
  );
}

Object.assign(window, {
  rExportXLSX, rExportPDF, rExportPPTX, rPreflight, rExportFilename, rLoadLib,
  ReportExportModal,
});
