/* global docx */
// ============================================================================
// EX-ANTE — Native .docx export (Office Open XML, via the docx library)
// ============================================================================
// Generates a proper Word document with:
//   - Title page (centered, large)
//   - Decision banner (coloured paragraph)
//   - Auto-numbered headings (H1/H2/H3)
//   - Real Word tables (not HTML-faked)
//   - Page breaks between major sections
//   - Header on every page after the cover
// The `docx` UMD bundle loaded in MELR.html exposes `window.docx`.
//
// Exposes window.exporteExanteDocx(payload) — same payload shape as
// ExanteReport / ExanteReportModal. Triggers a browser download.
// ============================================================================

(function () {
  // Guard: docx might not be loaded yet (offline, blocked CDN, etc.). Fall
  // back to a no-op that the UI can detect via the existence of the function.
  if (typeof docx === "undefined" || !docx) {
    window.exportExanteDocx = function () {
      window.alert("La librairie docx n'a pas pu être chargée (CDN inaccessible). Utilisez l'export Word (.doc) ou PDF à la place.");
    };
    window.exportExanteDocxAvailable = false;
    return;
  }
  window.exportExanteDocxAvailable = true;

  const {
    Document, Packer,
    Paragraph, TextRun, HeadingLevel, AlignmentType,
    Table, TableRow, TableCell, WidthType,
    BorderStyle, ShadingType,
    Header, Footer, PageBreak,
    LevelFormat,
    ImageRun,
  } = docx;

  // ---------- formatters ----------
  function fmtNum(v) { if (v == null || !isFinite(v)) return "—"; return Math.round(v).toLocaleString(); }
  function fmtPct(v) { if (v == null || !isFinite(v)) return "—"; return (v * 100).toFixed(1) + "%"; }
  function fmtRatio(v) { if (v == null || !isFinite(v)) return "—"; if (v >= 999) return "∞"; return Number(v).toFixed(2); }
  function fmtCcy(v, ccy) { if (v == null || !isFinite(v)) return "—"; return Math.round(v).toLocaleString() + " " + (ccy || "EUR"); }
  function safe(v) { return v == null || v === "" ? "—" : String(v); }

  // ---------- threshold-aware shading colours ----------
  // Tile shading colours — match the HTML report's pastel palette so the
  // rendered Word file looks like a screenshot of the live dashboard.
  const SHADE = {
    ok:      { fill: "DCFCE7", fg: "15803D" },
    bad:     { fill: "FEE2E2", fg: "B91C1C" },
    amber:   { fill: "FEF3C7", fg: "A16207" },
    neutral: { fill: "F3F4F6", fg: "374151" },
    accent:  { fill: "E0E7FF", fg: "1E3A8A" },
  };
  function statusFor(value, threshold, dir) {
    if (value == null || !isFinite(value)) return "neutral";
    if (dir === "<") return value <= threshold ? "ok" : "bad";
    return value >= threshold ? "ok" : "bad";
  }
  function pctStatus(value) {
    if (value == null || !isFinite(value)) return "neutral";
    if (value >= 0.75) return "ok";
    if (value >= 0.60) return "amber";
    return "bad";
  }
  // RGBA-like fill for the sensitivity heatmap — converts a fraction
  // around the threshold into a soft green/red shading hex code.
  function heatmapFill(value, threshold, dir) {
    if (value == null || !isFinite(value)) return "F3F4F6";
    let delta;
    if (dir === "<") delta = (threshold - value) / Math.max(Math.abs(threshold), 1);
    else delta = (value - threshold) / Math.max(Math.abs(threshold), 1);
    delta = Math.max(-1, Math.min(1, delta));
    if (delta >= 0) {
      // green: from D1FAE5 (very pale) to 86EFAC (medium)
      const t = Math.min(1, delta * 1.2);
      const g = Math.round(0xFA - t * (0xFA - 0xEF));     // 250 -> 239
      const r = Math.round(0xD1 - t * (0xD1 - 0x86));     // 209 -> 134
      const b = Math.round(0xE5 - t * (0xE5 - 0xAC));     // 229 -> 172
      return r.toString(16).padStart(2, "0").toUpperCase()
           + g.toString(16).padStart(2, "0").toUpperCase()
           + b.toString(16).padStart(2, "0").toUpperCase();
    } else {
      // red: from FEE2E2 to FCA5A5
      const t = Math.min(1, -delta * 1.2);
      const r = Math.round(0xFE - t * (0xFE - 0xFC));
      const g = Math.round(0xE2 - t * (0xE2 - 0xA5));
      const b = Math.round(0xE2 - t * (0xE2 - 0xA5));
      return r.toString(16).padStart(2, "0").toUpperCase()
           + g.toString(16).padStart(2, "0").toUpperCase()
           + b.toString(16).padStart(2, "0").toUpperCase();
    }
  }

  // ---------- low-level helpers ----------
  function P(text, opts) {
    opts = opts || {};
    const runs = Array.isArray(text)
      ? text.map((t) => (t instanceof TextRun ? t : new TextRun(typeof t === "string" ? t : t)))
      : [new TextRun({ text: text || "", bold: opts.bold, italics: opts.italics, size: opts.size, color: opts.color })];
    return new Paragraph({
      children: runs,
      heading: opts.heading,
      alignment: opts.alignment,
      spacing: opts.spacing || { before: 0, after: 80 },
      pageBreakBefore: opts.pageBreakBefore,
      shading: opts.shading,
    });
  }

  function H1(text) { return P(text, { heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 }, pageBreakBefore: true }); }
  function H1NoBreak(text) { return P(text, { heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 } }); }
  function H2(text) { return P(text, { heading: HeadingLevel.HEADING_2, spacing: { before: 160, after: 80 } }); }
  function H3(text) { return P(text, { heading: HeadingLevel.HEADING_3, spacing: { before: 120, after: 60 } }); }

  // Make a quick table with header row + data rows.
  // columns = [{ label, width:number (pct of 100), align: "left"|"right"|"center" }]
  // rows = array of cell arrays (string | { text, bold?, color? })
  function buildTable(columns, rows) {
    const headerCells = columns.map((c) => new TableCell({
      width: { size: c.width || (100 / columns.length), type: WidthType.PERCENTAGE },
      shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
      children: [new Paragraph({
        children: [new TextRun({ text: c.label, bold: true, size: 18 })],
        alignment: c.align === "right" ? AlignmentType.RIGHT : c.align === "center" ? AlignmentType.CENTER : AlignmentType.LEFT,
        spacing: { before: 40, after: 40 },
      })],
    }));
    const dataRows = rows.map((cells) => new TableRow({
      children: columns.map((c, i) => {
        const cell = cells[i];
        const text = cell == null ? "—" : (typeof cell === "string" || typeof cell === "number" ? String(cell) : (cell.text != null ? String(cell.text) : "—"));
        const bold = typeof cell === "object" && cell !== null && cell.bold;
        const color = typeof cell === "object" && cell !== null ? cell.color : undefined;
        const shading = typeof cell === "object" && cell !== null && cell.shading
          ? { type: ShadingType.CLEAR, fill: cell.shading }
          : undefined;
        return new TableCell({
          width: { size: c.width || (100 / columns.length), type: WidthType.PERCENTAGE },
          shading,
          children: [new Paragraph({
            children: [new TextRun({ text, bold, color, size: 18 })],
            alignment: c.align === "right" ? AlignmentType.RIGHT : c.align === "center" ? AlignmentType.CENTER : AlignmentType.LEFT,
            spacing: { before: 30, after: 30 },
          })],
        });
      }),
    }));
    return new Table({
      width: { size: 100, type: WidthType.PERCENTAGE },
      rows: [new TableRow({ tableHeader: true, children: headerCells }), ...dataRows],
    });
  }

  // Build a row of "KPI tiles" as a borderless Word table — one cell per
  // tile with header-fill colour matching the live dashboard.
  // tiles = [{ label, value, status: "ok"|"bad"|"amber"|"neutral", sub }]
  function buildKpiTileRow(tiles, perRow) {
    perRow = perRow || 4;
    const rows = [];
    for (let i = 0; i < tiles.length; i += perRow) {
      const slice = tiles.slice(i, i + perRow);
      const cells = slice.map((tile) => {
        const c = SHADE[tile.status || "neutral"];
        return new TableCell({
          width: { size: 100 / perRow, type: WidthType.PERCENTAGE },
          shading: { type: ShadingType.CLEAR, fill: c.fill },
          children: [
            new Paragraph({
              children: [new TextRun({ text: tile.label, bold: true, color: c.fg, size: 16 })],
              spacing: { before: 60, after: 30 },
            }),
            new Paragraph({
              children: [new TextRun({ text: tile.value, bold: true, color: c.fg, size: 28 })],
              spacing: { before: 0, after: 30 },
            }),
            tile.sub ? new Paragraph({
              children: [new TextRun({ text: tile.sub, color: c.fg, size: 14 })],
              spacing: { before: 0, after: 60 },
            }) : new Paragraph({ children: [], spacing: { after: 60 } }),
          ],
        });
      });
      // Pad if last row not full
      while (cells.length < perRow) {
        cells.push(new TableCell({ width: { size: 100 / perRow, type: WidthType.PERCENTAGE }, children: [new Paragraph("")] }));
      }
      rows.push(new TableRow({ children: cells }));
    }
    return new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, rows });
  }

  // Word decision pill — a small shaded paragraph rendered inline-ish via a
  // single-cell table.
  function decisionPill(decision, lang) {
    const s = decision === "consistent" ? SHADE.ok
            : decision === "with_observations" ? SHADE.amber
            : SHADE.bad;
    const label = decision === "consistent" ? "CONSISTENT"
                : decision === "with_observations" ? (lang === "fr" ? "AVEC OBSERVATIONS" : "WITH OBSERVATIONS")
                : (lang === "fr" ? "REJET" : "REJECT");
    return new Paragraph({
      shading: { type: ShadingType.CLEAR, fill: s.fill },
      alignment: AlignmentType.LEFT,
      spacing: { before: 60, after: 60 },
      children: [new TextRun({ text: " " + label + " ", bold: true, color: s.fg, size: 18 })],
    });
  }

  // Helper: wrap an ArrayBuffer (PNG) as a centered paragraph w/ ImageRun.
  function imgParagraph(buf, w, h) {
    if (!buf) return null;
    return new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { before: 120, after: 120 },
      children: [new ImageRun({ data: buf, transformation: { width: w, height: h } })],
    });
  }

  // ---------- the big builder ----------
  async function build(payload) {
    const {
      lang, dossier, ident, inputs, scen, calendar,
      capexLines, opexLines, revenueLines, finSources,
      pubTransfers, mprTransfers, externalities,
      qualityItems, mcItems,
      // Phase 7 additions
      stakeholders, stakeholderAccounts, conversionFactors, institutional,
      exanteComputed, mprResult, qualityResult, mcResult, finalDecision,
    } = payload;
    const t = (fr, en) => lang === "en" ? en : fr;
    const ccy = (inputs && inputs.currency) || "EUR";
    const projectCode = (dossier && dossier.projects && dossier.projects.code) || (ident && ident.intitule) || "Project";
    const today = new Date().toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US", { year: "numeric", month: "long", day: "numeric" });

    // ===== Precompute chart PNG buffers (canvas → ArrayBuffer) =====
    let chartBufs = {};
    if (window.exanteCharts && window.exanteEngine) {
      try {
        const E = window.exanteEngine;
        const cf = exanteComputed && exanteComputed.cashFlow;
        const cashflowOpts = cf ? {
          fcfProject: cf.fcfProject, cumProject: cf.cumProject,
          fcfEquity: cf.fcfEquity, cumEquity: cf.cumEquity, ccy,
        } : null;
        const plOpts = (exanteComputed && exanteComputed.pl && exanteComputed.pl.length) ? exanteComputed.pl : null;
        // Donut from positive externalities by category
        let donutOpts = null;
        if (externalities && externalities.length > 0) {
          const buckets = {};
          externalities.filter((e) => e.kind === "positive").forEach((e) => {
            const k = e.category || "other";
            buckets[k] = (buckets[k] || 0) + (Number(e.amount_year1) || 0);
          });
          const arr = Object.entries(buckets).map(([name, value]) => ({ name, value }));
          if (arr.length > 0) donutOpts = { stakeholders: arr };
        }
        // Sensitivity matrix
        let sensitivityOpts = null;
        if (cf) {
          const baseOpts = {
            inputs, scen,
            capexLines: capexLines || [], opexLines: opexLines || [],
            revenueLines: revenueLines || [], financingSources: finSources || [],
          };
          const mprBaseOpts = {
            inputs, capexLines: capexLines || [],
            opexByYear: exanteComputed.opexByYear,
            revenueByYear: exanteComputed.revenueByYear,
            debtSchedule: exanteComputed.debt,
            mprTransfers: mprTransfers || [], externalities: externalities || [],
            conversionFactors: null,
          };
          const variables = [
            { v: "capex",    fr: "CAPEX",                en: "CAPEX" },
            { v: "volumes",  fr: "Volumes",              en: "Volumes" },
            { v: "tariffs",  fr: "Tarifs",               en: "Tariffs" },
            { v: "opex",     fr: "OPEX",                 en: "OPEX" },
            { v: "discount", fr: "Taux d'actualisation", en: "Discount rate" },
          ];
          const deltas = [-0.20, -0.10, 0, 0.10, 0.20];
          const matrix = variables.map((v) => ({
            v,
            cells: deltas.map((d) => E.economicSensitivityShock(baseOpts, mprBaseOpts, v.v, d)),
          }));
          sensitivityOpts = { matrix, variables, deltas };
        }
        chartBufs = await window.exanteCharts.arrayBuffers({
          cashflow: cashflowOpts, pl: plOpts, donut: donutOpts, sensitivity: sensitivityOpts, ccy,
        });
      } catch (e) {
        console.error("[exante-docx] chart prep failed:", e);
        chartBufs = {};
      }
    }

    // ===== Build all children =====
    const children = [];

    // --- Cover page ---
    children.push(
      P([new TextRun({ text: t("ÉVALUATION EX ANTE", "EX-ANTE APPRAISAL"), bold: true, size: 56 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 2400, after: 240 } }),
      P([new TextRun({ text: safe(ident && ident.intitule), size: 28 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 120, after: 480 } }),
      P([new TextRun({ text: t("Code projet : ", "Project code: ") + projectCode, size: 22 })],
        { alignment: AlignmentType.CENTER }),
      P([new TextRun({ text: t("Tutelle : ", "Authority: ") + safe(ident && ident.ministry), size: 22 })],
        { alignment: AlignmentType.CENTER }),
      P([new TextRun({ text: t("Document généré le : ", "Generated on: ") + today, size: 22 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 60, after: 800 } }),
      P([new TextRun({ text: t("Dossier d'évaluation ex ante", "Ex-ante appraisal dossier"), italics: true, size: 18, color: "6B7280" })],
        { alignment: AlignmentType.CENTER })
    );

    // --- Decision banner ---
    if (finalDecision) {
      const decUi = {
        go:          { label: t("GO — APPROUVÉ POUR FINANCEMENT", "GO — APPROVED FOR FUNDING"), color: "16A34A", fill: "DCFCE7" },
        conditional: { label: t("CONDITIONNEL — AVEC OBSERVATIONS", "CONDITIONAL — WITH OBSERVATIONS"), color: "D97706", fill: "FEF3C7" },
        no_go:       { label: t("NO-GO — À RÉVISER OU REJETER", "NO-GO — TO REVISE OR REJECT"), color: "DC2626", fill: "FEE2E2" },
      }[finalDecision.decision];
      children.push(
        H1NoBreak(t("Décision du comité", "Investment committee decision")),
        new Paragraph({
          shading: { type: ShadingType.CLEAR, fill: decUi.fill },
          alignment: AlignmentType.CENTER,
          spacing: { before: 100, after: 100 },
          children: [new TextRun({ text: decUi.label, bold: true, color: decUi.color, size: 28 })],
        }),
        P(finalDecision.reasons.join(" · "), { italics: true, color: "374151", spacing: { after: 200 } })
      );
    }

    // --- Executive summary ---
    children.push(H1NoBreak(t("I. Synthèse exécutive", "I. Executive summary")));
    if (ident) {
      children.push(
        P([new TextRun({ text: t("Objectif général : ", "General objective: "), bold: true }), new TextRun({ text: safe(ident.general_objective) })]),
        P([new TextRun({ text: t("Coût global : ", "Global cost: "), bold: true }), new TextRun({ text: fmtCcy(ident.global_cost_native, ident.currency) })]),
        P([new TextRun({ text: t("Horizon : ", "Horizon: "), bold: true }), new TextRun({ text: (ident.total_horizon_years || "—") + " " + t("ans", "yrs") })]),
        P([new TextRun({ text: t("Cible directe : ", "Direct target: "), bold: true }), new TextRun({ text: safe(ident.direct_targets) + " (" + (ident.direct_target_size ? Number(ident.direct_target_size).toLocaleString() : "—") + ")" })])
      );
    }

    // Indicators as colored KPI tiles
    if (exanteComputed && exanteComputed.indicators) {
      const ind = exanteComputed.indicators;
      const discFin = (inputs && inputs.discount_rate_financial) || 0.09;
      const discEco = (inputs && inputs.discount_rate_economic)  || 0.09;
      const tiles = [
        { label: "VAN " + t("projet", "project"), value: fmtCcy(ind.npvProject, ccy), status: statusFor(ind.npvProject, 0, ">"), sub: "≥ 0" },
        { label: "TRI " + t("projet", "project"), value: fmtPct(ind.irrProject), status: statusFor(ind.irrProject, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("DSCR min", "Min DSCR"), value: fmtRatio(ind.dscrMin), status: statusFor(ind.dscrMin, 1.2, ">"), sub: "≥ 1,2" },
        { label: t("Marge EBITDA", "EBITDA margin"), value: fmtPct(ind.ebitdaMarginAvg), status: statusFor(ind.ebitdaMarginAvg, 0, ">"), sub: t("Moyenne", "Average") },
      ];
      if (mprResult && mprResult.indicators) {
        tiles.push(
          { label: "ENPV", value: fmtCcy(mprResult.indicators.enpv, ccy), status: statusFor(mprResult.indicators.enpv, 0, ">"), sub: "≥ 0" },
          { label: "EIRR", value: fmtPct(mprResult.indicators.eirr), status: statusFor(mprResult.indicators.eirr, discEco, ">"), sub: "> " + fmtPct(discEco) },
          { label: t("Ratio B/C", "B/C ratio"), value: fmtRatio(mprResult.indicators.bcRatio), status: statusFor(mprResult.indicators.bcRatio, 1, ">"), sub: "≥ 1" }
        );
      }
      if (qualityResult) tiles.push({ label: t("Qualité", "Quality"), value: fmtPct(qualityResult.pct), status: statusFor(qualityResult.pct, 0.6, ">"), sub: "≥ 60%" });
      if (mcResult)      tiles.push({ label: t("Multicritère", "Multi-criteria"), value: fmtPct(mcResult.globalPct), status: statusFor(mcResult.globalPct, 0.75, ">"), sub: "≥ 75%" });
      children.push(buildKpiTileRow(tiles, 4));
    }

    // Cashflow chart at end of exec summary
    if (chartBufs.cashflow) {
      const img = imgParagraph(chartBufs.cashflow, 600, 274);
      if (img) children.push(img);
    }

    // --- II. Identification ---
    children.push(H1(t("II. Identification du projet (Tableau 8)", "II. Project identification (Table 8)")));
    if (ident) {
      children.push(H2(t("1. Identification générale", "1. General identification")));
      children.push(
        P([new TextRun({ text: t("Intitulé : ", "Title: "), bold: true }), new TextRun({ text: safe(ident.intitule) })]),
        P([new TextRun({ text: t("Sous-secteur : ", "Sub-sector: "), bold: true }), new TextRun({ text: safe(ident.sub_sector) })]),
        P([new TextRun({ text: t("Ministère : ", "Ministry: "), bold: true }), new TextRun({ text: safe(ident.ministry) })])
      );
      children.push(H2(t("2. Situation initiale", "2. Initial situation")));
      children.push(
        P([new TextRun({ text: t("Situation initiale : ", "Initial situation: "), bold: true }), new TextRun({ text: safe(ident.initial_situation) })]),
        P([new TextRun({ text: t("Situation sans projet : ", "Without project: "), bold: true }), new TextRun({ text: safe(ident.without_project_situation) })]),
        P([new TextRun({ text: t("Problème central : ", "Central problem: "), bold: true }), new TextRun({ text: safe(ident.central_problem) })])
      );
      children.push(H2(t("3. Objectifs", "3. Objectives")));
      children.push(P([new TextRun({ text: t("Objectif général : ", "General objective: "), bold: true }), new TextRun({ text: safe(ident.general_objective) })]));
      if (ident.specific_objective_1) children.push(P("OS 1 : " + ident.specific_objective_1));
      if (ident.specific_objective_2) children.push(P("OS 2 : " + ident.specific_objective_2));
      if (ident.specific_objective_3) children.push(P("OS 3 : " + ident.specific_objective_3));

      children.push(H2(t("4. Composantes", "4. Components")));
      const compRows = [];
      for (let i = 1; i <= 6; i++) {
        if (ident["component_" + i + "_label"]) {
          compRows.push([ident["component_" + i + "_label"], fmtCcy(ident["component_" + i + "_amount"], ident.currency)]);
        }
      }
      if (compRows.length) children.push(buildTable(
        [{ label: t("Composante", "Component"), width: 75 }, { label: t("Budget", "Budget"), width: 25, align: "right" }],
        compRows
      ));

      children.push(H2(t("5. Risques", "5. Risks")));
      children.push(
        P([new TextRun({ text: t("Risques : ", "Risks: "), bold: true }), new TextRun({ text: safe(ident.main_risks) })]),
        P([new TextRun({ text: t("Conditions préalables : ", "Preconditions: "), bold: true }), new TextRun({ text: safe(ident.preconditions) })])
      );
    }

    // --- III. Parameters & scenarios ---
    children.push(H1(t("III. Paramètres et scénarios", "III. Parameters and scenarios")));
    if (inputs) {
      children.push(buildTable(
        [{ label: t("Paramètre", "Parameter"), width: 60 }, { label: t("Valeur", "Value"), width: 40, align: "right" }],
        [
          [t("Scénario actif", "Active scenario"), safe(inputs.scenario)],
          [t("Horizon", "Horizon"), (inputs.horizon_years || "—") + " " + t("ans", "yrs")],
          [t("Taux actu. financier", "Financial discount"), fmtPct(inputs.discount_rate_financial)],
          [t("Taux actu. économique", "Economic discount"), fmtPct(inputs.discount_rate_economic)],
          [t("Inflation annuelle", "Annual inflation"), fmtPct(inputs.inflation_annual)],
          [t("Taux d'impôt", "Tax rate"), fmtPct(inputs.tax_rate_effective)],
          [t("Part dette / CAPEX", "Debt / CAPEX share"), fmtPct(inputs.debt_share_capex)],
          [t("Taux d'intérêt dette", "Debt rate"), fmtPct(inputs.debt_interest_rate)],
          [t("Imprévus CAPEX", "Contingencies"), fmtPct(inputs.contingencies_capex)],
        ]
      ));
    }

    // Calendar
    if (calendar && calendar.activities && calendar.activities.length > 0) {
      children.push(H2(t("Calendrier", "Schedule")));
      children.push(buildTable(
        [
          { label: t("Phase", "Phase"), width: 18 },
          { label: t("Action", "Action"), width: 42 },
          { label: t("Début", "Start"), width: 14 },
          { label: t("Fin", "End"), width: 14 },
          { label: "%", width: 12, align: "right" },
        ],
        calendar.activities.map((a) => {
          const ph = calendar.phases.find((p) => p.id === a.phase_id);
          return [ph ? ph.name_fr : "—", (a.milestone ? "◆ " : "") + a.name_fr, a.start_date || "—", a.end_date || "—", (a.progress || 0) + "%"];
        })
      ));
    }

    // --- IV. Financial ---
    children.push(H1(t("IV. Analyse financière", "IV. Financial analysis")));
    children.push(H2("CAPEX (" + t("Tableau", "Table") + " 13)"));
    if (capexLines && capexLines.length > 0) {
      const capexRows = capexLines.map((l) => [
        l.rubric,
        l.category || "—",
        fmtNum(l.quantity || 1),
        fmtNum(l.unit_cost),
        { text: fmtNum((l.quantity || 1) * (l.unit_cost || 0)), bold: true },
        String(l.amort_years || "—"),
      ]);
      if (exanteComputed && exanteComputed.capex) {
        capexRows.push(
          [{ text: t("Sous-total", "Subtotal"), bold: true, shading: "F3F4F6" }, "", "", "", { text: fmtNum(exanteComputed.capex.subtotal), bold: true, shading: "F3F4F6" }, { text: "", shading: "F3F4F6" }],
          [t("Imprévus", "Contingencies"), "", "", "", fmtNum(exanteComputed.capex.contingencies), ""],
          [{ text: t("CAPEX TOTAL", "TOTAL CAPEX"), bold: true, shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: fmtNum(exanteComputed.capex.total), bold: true, shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }],
        );
      }
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item"), width: 30 },
          { label: t("Catégorie", "Category"), width: 17 },
          { label: t("Qté", "Qty"), width: 8, align: "right" },
          { label: t("Coût unit.", "Unit cost"), width: 15, align: "right" },
          { label: t("Total", "Total"), width: 18, align: "right" },
          { label: t("Amort.", "Amort."), width: 12, align: "right" },
        ],
        capexRows
      ));
    }

    children.push(H2("OPEX (" + t("Tableau", "Table") + " 14)"));
    if (opexLines && opexLines.length > 0 && window.exanteEngine) {
      const opexRows = opexLines.map((l) => {
        const series = window.exanteEngine.projectOpexLine(l, 25, inputs, scen);
        return [l.rubric, l.inflation_category, fmtNum(l.year1_amount), fmtNum(series[4]), fmtNum(series[9])];
      });
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item"), width: 40 },
          { label: t("Catégorie", "Category"), width: 20 },
          { label: t("An 1", "Y1"), width: 13, align: "right" },
          { label: t("An 5", "Y5"), width: 13, align: "right" },
          { label: t("An 10", "Y10"), width: 14, align: "right" },
        ],
        opexRows
      ));
    }

    children.push(H2(t("Revenus", "Revenue") + " (" + t("Tableau", "Table") + " 15)"));
    if (revenueLines && revenueLines.length > 0) {
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item"), width: 40 },
          { label: t("Volume An 1", "Volume Y1"), width: 18, align: "right" },
          { label: t("Tarif An 1", "Tariff Y1"), width: 18, align: "right" },
          { label: t("Revenu An 1", "Revenue Y1"), width: 24, align: "right" },
        ],
        revenueLines.map((l) => [
          l.rubric,
          l.is_fixed ? "—" : fmtNum(l.year1_volume),
          fmtNum(l.year1_tariff),
          { text: fmtNum(l.is_fixed ? l.year1_tariff : (l.year1_volume || 0) * (l.year1_tariff || 0)), bold: true },
        ])
      ));
    }

    children.push(H2(t("Plan de financement", "Financing plan") + " (" + t("Tableau", "Table") + " 17)"));
    if (finSources && finSources.length > 0) {
      children.push(buildTable(
        [
          { label: t("Source", "Source"), width: 50 },
          { label: t("Nature", "Kind"), width: 25 },
          { label: t("Montant", "Amount"), width: 25, align: "right" },
        ],
        finSources.map((f) => [f.source, f.kind, fmtCcy(f.amount, f.currency)])
      ));
    }

    if (exanteComputed && exanteComputed.indicators) {
      children.push(H2(t("Indicateurs financiers consolidés", "Consolidated financial indicators")));
      const ind = exanteComputed.indicators;
      const discFin = (inputs && inputs.discount_rate_financial) || 0.09;
      children.push(buildKpiTileRow([
        { label: t("VAN projet", "Project NPV"),       value: fmtCcy(ind.npvProject, ccy), status: statusFor(ind.npvProject, 0, ">"), sub: "≥ 0" },
        { label: t("TRI projet", "Project IRR"),       value: fmtPct(ind.irrProject),      status: statusFor(ind.irrProject, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("DSCR min", "Min DSCR"),            value: fmtRatio(ind.dscrMin),       status: statusFor(ind.dscrMin, 1.2, ">"), sub: "≥ 1,2" },
        { label: t("VAN actionnaire", "Equity NPV"),   value: fmtCcy(ind.npvEquity, ccy),  status: statusFor(ind.npvEquity, 0, ">"), sub: "≥ 0" },
        { label: t("TRI actionnaire", "Equity IRR"),   value: fmtPct(ind.irrEquity),       status: statusFor(ind.irrEquity, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("Marge EBITDA", "EBITDA margin"),   value: fmtPct(ind.ebitdaMarginAvg), status: statusFor(ind.ebitdaMarginAvg, 0, ">"), sub: t("Moyenne", "Average") },
      ], 3));
      // P&L chart right after the financial indicators tiles
      if (chartBufs.pl) {
        const img = imgParagraph(chartBufs.pl, 600, 292);
        if (img) children.push(img);
      }
    }

    // Stakeholder accounts (Phase 7.1)
    if (stakeholderAccounts && stakeholderAccounts.rows && stakeholderAccounts.rows.length > 0) {
      children.push(H2(t("Comptes par acteur (Tableau 22)", "Stakeholder accounts (Table 22)")));
      children.push(P(t(
        "Revenus, coûts et impact net par acteur (avec vs sans projet), cumulés sur l'horizon.",
        "Revenue, costs and net impact per actor (with vs without project), cumulated over the horizon."
      )));
      const rows = stakeholderAccounts.rows.map((r) => [
        { text: r.name, bold: true },
        String(r.beneficiary_count || 0),
        fmtNum(r.cumWithRevenue),
        { text: "(" + fmtNum(r.cumWithCost) + ")", color: "DC2626" },
        { text: fmtNum(r.cumWithNet), bold: true },
        { text: fmtNum(r.cumBaselineNet), color: "6B7280" },
        {
          text: (r.cumImpact >= 0 ? "+" : "") + fmtNum(r.cumImpact),
          bold: true,
          color: r.cumImpact >= 0 ? "15803D" : "B91C1C",
          shading: r.cumImpact >= 0 ? "DCFCE7" : "FEE2E2",
        },
      ]);
      // TOTAL row
      rows.push([
        { text: "TOTAL", bold: true, shading: "F3F4F6" }, { text: "", shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumWithRevenue), bold: true, shading: "F3F4F6" },
        { text: "(" + fmtNum(stakeholderAccounts.total.cumWithCost) + ")", bold: true, shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumWithNet), bold: true, shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumBaselineNet), bold: true, shading: "F3F4F6" },
        {
          text: (stakeholderAccounts.total.cumImpact >= 0 ? "+" : "") + fmtNum(stakeholderAccounts.total.cumImpact),
          bold: true, shading: "F3F4F6",
          color: stakeholderAccounts.total.cumImpact >= 0 ? "15803D" : "B91C1C",
        },
      ]);
      children.push(buildTable(
        [
          { label: t("Acteur", "Stakeholder"), width: 22 },
          { label: t("Bénéf.", "Benef."), width: 8, align: "right" },
          { label: t("Revenu cum.", "Cumul. rev."), width: 14, align: "right" },
          { label: t("Coût cum.", "Cumul. cost"), width: 14, align: "right" },
          { label: t("Net (avec)", "Net (with)"), width: 14, align: "right" },
          { label: t("Net (sans)", "Net (without)"), width: 14, align: "right" },
          { label: t("Impact", "Impact"), width: 14, align: "right" },
        ],
        rows
      ));
    }

    // --- V. Economic (MPR) ---
    children.push(H1(t("V. Analyse économique (MPR)", "V. Economic analysis (MPR)")));

    children.push(H2(t("Phase 1 — Élimination des transferts", "Phase 1 — Transfer elimination")));
    if (mprTransfers && mprTransfers.length > 0) {
      children.push(buildTable(
        [
          { label: t("Libellé", "Label"), width: 50 },
          { label: t("Catégorie", "Category"), width: 25 },
          { label: t("Montant/an", "Amount/yr"), width: 25, align: "right" },
        ],
        mprTransfers.map((t2) => [t2.label, t2.category, fmtNum(t2.amount_per_year)])
      ));
    } else {
      children.push(P(t("Les intérêts de la dette sont automatiquement déduits depuis le plan de financement.", "Debt interest is automatically deducted from the financing plan.")));
    }

    children.push(H2(t("Phase 2 — Externalités", "Phase 2 — Externalities")));
    if (externalities && externalities.length > 0) {
      children.push(buildTable(
        [
          { label: t("Libellé", "Label"), width: 38 },
          { label: t("Nature", "Kind"), width: 16 },
          { label: t("Catégorie", "Category"), width: 22 },
          { label: t("An 1", "Y1"), width: 24, align: "right" },
        ],
        externalities.map((e) => [
          e.label,
          e.kind,
          e.category || "—",
          { text: (e.kind === "negative" ? "−" : "+") + fmtNum(e.amount_year1), color: e.kind === "negative" ? "DC2626" : "16A34A" },
        ])
      ));
    }

    if (mprResult && mprResult.indicators) {
      children.push(H2(t("Indicateurs économiques", "Economic indicators")));
      const eind = mprResult.indicators;
      const discEco = (inputs && inputs.discount_rate_economic) || 0.09;
      children.push(buildKpiTileRow([
        { label: "ENPV",                              value: fmtCcy(eind.enpv, ccy),       status: statusFor(eind.enpv, 0, ">"),  sub: "≥ 0" },
        { label: "EIRR",                              value: fmtPct(eind.eirr),            status: statusFor(eind.eirr, discEco, ">"), sub: "> " + fmtPct(discEco) },
        { label: t("Ratio B/C", "B/C ratio"),         value: fmtRatio(eind.bcRatio),       status: statusFor(eind.bcRatio, 1, ">"), sub: "≥ 1" },
        { label: t("NPV bénéfices", "NPV benefits"),  value: fmtCcy(eind.npvBenefits, ccy), status: "neutral", sub: t("Actualisé", "Discounted") },
        { label: t("NPV coûts", "NPV costs"),         value: fmtCcy(eind.npvCosts, ccy),    status: "neutral", sub: t("Actualisé", "Discounted") },
      ], 3));
      if (chartBufs.donut) {
        const img = imgParagraph(chartBufs.donut, 460, 260);
        if (img) children.push(img);
      }
    }

    // Conversion factors — MPR Phase 3 (Phase 7.2)
    if (conversionFactors && conversionFactors.length > 0) {
      children.push(H2(t("Phase 3 — Facteurs de conversion", "Phase 3 — Conversion factors")));
      children.push(P(t(
        "Coefficients de prix fictifs appliqués automatiquement par le moteur dans le calcul de l'ENPV/EIRR/B/C (1.0 = prix de marché).",
        "Shadow-price coefficients automatically applied to ENPV/EIRR/B/C (1.0 = market price)."
      )));
      const labels = {
        capex_local: t("CAPEX local", "Local CAPEX"),
        capex_imported: t("CAPEX importé", "Imported CAPEX"),
        opex_personnel: t("OPEX — Personnel", "OPEX — Personnel"),
        opex_materials: t("OPEX — Matières", "OPEX — Materials"),
        revenue: t("Revenus", "Revenue"),
        other: t("Autres", "Other"),
      };
      children.push(buildTable(
        [
          { label: t("Input", "Input"), width: 30 },
          { label: t("Facteur", "Factor"), width: 15, align: "right" },
          { label: "Δ vs 1.0", width: 15, align: "right" },
          { label: t("Justification", "Justification"), width: 40 },
        ],
        conversionFactors.map((f) => {
          const v = Number(f.factor) || 1;
          const delta = (v - 1) * 100;
          return [
            labels[f.input_kind] || f.input_kind,
            { text: v.toFixed(2), bold: true, color: Math.abs(v - 1) < 0.02 ? "374151" : v < 1 ? "A16207" : "15803D" },
            (delta >= 0 ? "+" : "") + delta.toFixed(1) + "%",
            f.notes || "—",
          ];
        })
      ));
    }

    // --- VI. Quality + Multi-criteria ---
    children.push(H1(t("VI. Contrôle qualité & analyse multicritère", "VI. Quality grid & multi-criteria")));

    if (qualityResult) {
      children.push(
        H2(t("Grille de contrôle qualité (Tableau 9)", "Quality grid (Table 9)")),
        decisionPill(qualityResult.decision, lang),
        P([
          new TextRun({ text: t("Score global : ", "Global score: "), bold: true }),
          new TextRun({ text: fmtPct(qualityResult.pct), bold: true, color: SHADE[pctStatus(qualityResult.pct)].fg, size: 24 }),
          new TextRun({ text: " · " + qualityResult.scoredItems + "/" + qualityResult.totalItems + " " + t("critères évalués", "criteria scored"), color: "6B7280" }),
        ])
      );
      const qRows = Object.entries(qualityResult.byDim).map(([dim, v]) => {
        const s = pctStatus(v.pct);
        return [dim, v.avg.toFixed(2) + "/3", { text: fmtPct(v.pct), bold: true, color: SHADE[s].fg, shading: SHADE[s].fill }];
      });
      children.push(buildTable(
        [
          { label: t("Dimension", "Dimension"), width: 60 },
          { label: t("Moyenne", "Average"), width: 20, align: "right" },
          { label: "%", width: 20, align: "right" },
        ],
        qRows
      ));
    }

    if (mcResult) {
      children.push(
        H2(t("Analyse multicritère (Tableau 21)", "Multi-criteria analysis (Table 21)")),
        decisionPill(mcResult.decision, lang),
        P([
          new TextRun({ text: t("Score pondéré global : ", "Weighted global score: "), bold: true }),
          new TextRun({ text: fmtPct(mcResult.globalPct), bold: true, color: SHADE[pctStatus(mcResult.globalPct)].fg, size: 24 }),
          new TextRun({ text: " · " + mcResult.scoredItems + "/" + mcResult.totalItems + " " + t("sous-critères évalués", "sub-criteria scored"), color: "6B7280" }),
        ])
      );
      const mcRows = Object.entries(mcResult.byDim).map(([dim, v]) => {
        const s = pctStatus(v.pct);
        return [dim, v.weightedAvg.toFixed(2) + "/3", { text: fmtPct(v.pct), bold: true, color: SHADE[s].fg, shading: SHADE[s].fill }];
      });
      children.push(buildTable(
        [
          { label: t("Dimension", "Dimension"), width: 60 },
          { label: t("Moy. pondérée", "Weighted avg"), width: 20, align: "right" },
          { label: "%", width: 20, align: "right" },
        ],
        mcRows
      ));
    }

    // --- VII. Institutional analysis (Phase 7.3) ---
    if (institutional && (institutional.legal_framework || institutional.management_model || institutional.carrier_organizations || institutional.sustainability_mechanisms)) {
      const inst = institutional;
      const mgmtModelLabels = {
        regie: t("Régie publique directe", "Direct public management"),
        concession: t("Concession", "Concession"),
        ppp: t("Partenariat Public-Privé", "Public-Private Partnership"),
        delegation: t("Délégation de service public", "Public service delegation"),
        community: t("Gestion communautaire", "Community-based"),
        mixed: t("Modèle mixte", "Mixed model"),
        other: t("Autre", "Other"),
      };
      children.push(H1(t("VII. Analyse institutionnelle", "VII. Institutional analysis")));

      const addBlock = (title, fields) => {
        const nonEmpty = fields.filter(([_, v]) => v && String(v).trim());
        if (!nonEmpty.length) return;
        children.push(H2(title));
        nonEmpty.forEach(([label, v]) => {
          children.push(P([new TextRun({ text: label + " : ", bold: true }), new TextRun({ text: String(v) })]));
        });
      };

      addBlock(t("1. Cadre juridique & réglementaire", "1. Legal & regulatory framework"), [
        [t("Cadre légal applicable", "Applicable legal framework"), inst.legal_framework],
        [t("Contraintes / autorisations", "Constraints / permits"), inst.juridical_constraints],
        [t("Normes techniques applicables", "Applicable technical norms"), inst.applicable_norms],
      ]);
      addBlock(t("2. Modèle de gestion", "2. Management model"), [
        [t("Modèle retenu", "Selected model"), mgmtModelLabels[inst.management_model] || inst.management_model],
        [t("Détails du modèle", "Model details"), inst.management_model_details],
        [t("Opérateur identifié", "Identified operator"), inst.operator_identification],
        [t("Arrangements contractuels", "Contractual arrangements"), inst.contractual_arrangements],
      ]);
      addBlock(t("3. Capacités institutionnelles des porteurs", "3. Carrier institutional capacities"), [
        [t("Organisations porteuses", "Carrier organisations"), inst.carrier_organizations],
        [t("Capacités actuelles", "Existing capacities"), inst.existing_capacities],
        [t("Écarts identifiés", "Capacity gaps"), inst.capacity_gaps],
        [t("Plan de renforcement", "Capacity-building plan"), inst.capacity_building_plan],
      ]);
      addBlock(t("4. Coordination & gouvernance", "4. Coordination & governance"), [
        [t("Mécanismes de coordination", "Coordination mechanisms"), inst.coordination_mechanisms],
        [t("Instances de décision", "Decision-making bodies"), inst.decision_making_bodies],
        [t("Dispositif institutionnel de S&E", "Institutional M&E arrangements"), inst.monitoring_arrangements],
      ]);
      addBlock(t("5. Durabilité institutionnelle", "5. Institutional sustainability"), [
        [t("Mécanismes de durabilité", "Sustainability mechanisms"), inst.sustainability_mechanisms],
        [t("Transfert de propriété post-projet", "Post-project ownership transfer"), inst.ownership_transfer],
        [t("Ressources O&M futures", "Future O&M resources"), inst.post_project_resources],
      ]);
      addBlock(t("6. Risques institutionnels", "6. Institutional risks"), [
        [t("Risques identifiés", "Identified risks"), inst.institutional_risks],
        [t("Mesures de mitigation", "Mitigation measures"), inst.risk_mitigation_measures],
      ]);
    }

    // --- VIII. Sensitivity heatmap ---
    if (exanteComputed && mprResult && window.exanteEngine) {
      const E = window.exanteEngine;
      const baseOpts = {
        inputs, scen,
        capexLines: capexLines || [], opexLines: opexLines || [],
        revenueLines: revenueLines || [], financingSources: finSources || [],
      };
      const mprBaseOpts = {
        inputs, capexLines: capexLines || [],
        opexByYear: exanteComputed.opexByYear,
        revenueByYear: exanteComputed.revenueByYear,
        debtSchedule: exanteComputed.debt,
        mprTransfers: mprTransfers || [], externalities: externalities || [],
        conversionFactors: null,
      };
      const vars = [
        { v: "capex",    fr: "CAPEX",                en: "CAPEX" },
        { v: "volumes",  fr: "Volumes",              en: "Volumes" },
        { v: "tariffs",  fr: "Tarifs",               en: "Tariffs" },
        { v: "opex",     fr: "OPEX",                 en: "OPEX" },
        { v: "discount", fr: "Taux d'actualisation", en: "Discount rate" },
      ];
      const deltas = [-0.20, -0.10, 0, 0.10, 0.20];
      let matrix = null;
      try {
        matrix = vars.map((v) => ({
          v,
          cells: deltas.map((d) => E.economicSensitivityShock(baseOpts, mprBaseOpts, v.v, d)),
        }));
      } catch (e) { /* fall through */ }

      if (matrix) {
        children.push(H1(t("VIII. Analyse de sensibilité (VAN projet)", "VIII. Sensitivity analysis (Project NPV)")));
        children.push(P(t("Heatmap de la VAN projet pour 5 variables × 5 chocs (−20% à +20%). Vert = VAN ≥ 0 (viable), rouge = VAN < 0.",
                          "Project NPV heatmap for 5 variables × 5 shocks (−20% to +20%). Green = NPV ≥ 0 (viable), red = NPV < 0.")));
        // Header row
        const headerCells = [
          new TableCell({
            width: { size: 30, type: WidthType.PERCENTAGE },
            shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
            children: [new Paragraph({ children: [new TextRun({ text: t("Variable", "Variable"), bold: true, size: 18 })] })],
          }),
          ...deltas.map((d) => new TableCell({
            width: { size: 14, type: WidthType.PERCENTAGE },
            shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
            children: [new Paragraph({
              alignment: AlignmentType.RIGHT,
              children: [new TextRun({ text: d === 0 ? "Base" : ((d > 0 ? "+" : "") + Math.round(d * 100) + "%"), bold: true, size: 18 })],
            })],
          })),
        ];
        const dataRows = matrix.map((row) => new TableRow({
          children: [
            new TableCell({
              width: { size: 30, type: WidthType.PERCENTAGE },
              children: [new Paragraph({ children: [new TextRun({ text: t(row.v.fr, row.v.en), bold: true, size: 18 })] })],
            }),
            ...row.cells.map((res, i) => {
              const value = res.fin && res.fin.npvProject;
              const fill = heatmapFill(value, 0, ">");
              return new TableCell({
                width: { size: 14, type: WidthType.PERCENTAGE },
                shading: { type: ShadingType.CLEAR, fill },
                children: [new Paragraph({
                  alignment: AlignmentType.RIGHT,
                  children: [new TextRun({ text: fmtNum(value), bold: i === 2, size: 18 })],
                })],
              });
            }),
          ],
        }));
        children.push(new Table({
          width: { size: 100, type: WidthType.PERCENTAGE },
          rows: [new TableRow({ tableHeader: true, children: headerCells }), ...dataRows],
        }));
        if (chartBufs.sensitivity) {
          const img = imgParagraph(chartBufs.sensitivity, 600, 240);
          if (img) children.push(img);
        }
      }
    }

    // --- Footer note ---
    children.push(
      P("", { spacing: { before: 320 } }),
      P([new TextRun({ text: t("Document généré automatiquement par MELR — ", "Generated automatically by MELR — ") + today, italics: true, size: 16, color: "6B7280" })],
        { alignment: AlignmentType.CENTER })
    );

    // ===== Build the document =====
    const doc = new Document({
      creator: "MELR",
      title: t("Évaluation ex-ante", "Ex-ante appraisal") + " — " + projectCode,
      description: "Generated by the MELR app",
      styles: {
        default: {
          document: { run: { font: "Calibri", size: 22 } },
          heading1: { run: { size: 32, bold: true, color: "1F2937" }, paragraph: { spacing: { before: 240, after: 120 }, border: { bottom: { color: "1F2937", style: BorderStyle.SINGLE, size: 12, space: 1 } } } },
          heading2: { run: { size: 26, bold: true, color: "374151" }, paragraph: { spacing: { before: 160, after: 80 } } },
          heading3: { run: { size: 22, bold: true, color: "4B5563" }, paragraph: { spacing: { before: 120, after: 60 } } },
        },
      },
      sections: [{
        headers: {
          default: new Header({
            children: [P([
              new TextRun({ text: t("Évaluation ex-ante — ", "Ex-ante appraisal — "), color: "6B7280", size: 18 }),
              new TextRun({ text: projectCode, color: "6B7280", size: 18, bold: true }),
            ], { alignment: AlignmentType.RIGHT })],
          }),
        },
        footers: {
          default: new Footer({
            children: [P([new TextRun({ text: "MELR · " + today, color: "9CA3AF", size: 16 })], { alignment: AlignmentType.CENTER })],
          }),
        },
        children,
      }],
    });

    return doc;
  }

  // ---------- Public entry point ----------
  window.exportExanteDocx = async function (payload) {
    try {
      const doc = await build(payload);
      const blob = await Packer.toBlob(doc);
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      const code = (payload.dossier && payload.dossier.projects && payload.dossier.projects.code) || "exante";
      const date = new Date().toISOString().slice(0, 10);
      a.href = url;
      a.download = "exante-" + code + "-" + date + ".docx";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (e) {
      console.error("[exante-docx] Export failed:", e);
      window.alert("Erreur de génération .docx : " + e.message + "\nUtilisez l'export Word (.doc) ou PDF à la place.");
    }
  };
})();
