/* global React, Icon, window */
// ============================================================================
// MELR · Portfolio dashboard (Tableau bailleur multi-projets)
// ----------------------------------------------------------------------------
// Aggregated view across multiple projects for donors and programme
// managers. Shows :
//   - 5 KPI cards (project count, total budget, indicators on-track,
//     indicators at-risk, weighted average achievement)
//   - Projects table with RAG status per project (sortable)
//   - Indicators roll-up by level (Impact / Outcome / Output) with
//     weighted achievement
//   - Alerts list (projects with no recent measurement, indicators
//     at-risk, etc.)
//   - Export buttons (Word, Excel) for the consolidated view
//
// Rendered inside the Reporting screen as a third tab ("Portefeuille")
// next to "Indicateurs" and "Activités".
//
// Entry point :
//   <PortfolioDashboard projects={...} indicators={...} programmes={...}
//                       lang={...} />
//
// Uses window.melr.* hooks already loaded by other screens.
// ============================================================================

(function () {
  const { useState, useMemo } = React;

  // ── Aggregation helpers ──────────────────────────────────────────────────
  // Reduce a project + its indicators into a single status object used by
  // the projects table and KPI cards. Pure function — no React state.
  function aggregateProject(project, indicators) {
    const projInds = indicators.filter((i) =>
      (i.project_uuid || i.projectId) === (project.uuid || project.id) ||
      i.project === (project.id || project.code));
    let sumPct = 0;
    let countPct = 0;
    let atRisk = 0;
    let onTrack = 0;
    projInds.forEach((i) => {
      const b = Number(i.baseline) || 0;
      const t = Number(i.target) || b;
      const a = Number(i.actual) || 0;
      if (t === b) return;
      let pct;
      if (i.invert) {
        // lower is better
        const num = b - a;
        const den = b - t;
        pct = den === 0 ? null : (num / den) * 100;
      } else {
        const num = a - b;
        const den = t - b;
        pct = den === 0 ? null : (num / den) * 100;
      }
      if (pct == null) return;
      pct = Math.max(0, Math.min(150, pct));  // clamp
      sumPct += pct;
      countPct++;
      if (pct >= 90) onTrack++;
      else if (pct < 70) atRisk++;
    });
    const avgPct = countPct > 0 ? sumPct / countPct : null;
    let status = "n/a";
    if (avgPct != null) {
      if (avgPct >= 90) status = "ok";
      else if (avgPct >= 70) status = "warn";
      else status = "risk";
    }
    return {
      project,
      indicatorCount: projInds.length,
      avgPct,
      onTrack,
      atRisk,
      status,
    };
  }

  // Roll-up indicators across all in-scope projects, grouped by level.
  function rollupByLevel(indicators) {
    const bag = { Impact: [], Outcome: [], Output: [] };
    indicators.forEach((i) => {
      const lvl = (i.level || "Output").trim();
      if (bag[lvl]) bag[lvl].push(i);
      else bag.Output.push(i);
    });
    return bag;
  }

  // Detect cross-project alerts: indicators at-risk, projects with no
  // recent measurement, etc.
  function detectAlerts(projects, indicators, lang) {
    const alerts = [];
    indicators.forEach((i) => {
      const b = Number(i.baseline) || 0;
      const t = Number(i.target) || b;
      const a = Number(i.actual) || 0;
      if (t === b) return;
      const pct = i.invert ? ((b - a) / (b - t)) * 100 : ((a - b) / (t - b)) * 100;
      if (pct != null && !isNaN(pct) && pct < 70 && pct > -100) {
        alerts.push({
          kind: "risk",
          message: (window.L("Indicateur à risque : ", "At-risk indicator: ", "Indicador en riesgo: ")) +
            (i.id || i.code) + " — " +
            (lang === "es" ? (i.name_es || i.name_en || i.name) : lang === "fr" ? (i.name_fr || i.name) : (i.name_en || i.name)) +
            " (" + Math.round(pct) + " %)",
        });
      }
    });

    // Projects without any indicator
    projects.forEach((p) => {
      const has = indicators.some((i) =>
        (i.project_uuid || i.projectId) === (p.uuid || p.id) ||
        i.project === (p.id || p.code));
      if (!has) {
        alerts.push({
          kind: "warn",
          message: (window.L("Projet sans indicateur : ", "Project with no indicators: ", "Proyecto sin indicadores: ")) +
            (p.id || p.code) + " — " + (p.nameFr || p.name_fr || p.name || ""),
        });
      }
    });

    return alerts.slice(0, 30);  // cap to keep UI readable
  }

  // ── Helper functions for formatting ────────────────────────────────────
  function fmtPct(n) {
    if (n == null || isNaN(n)) return "—";
    return Math.round(n) + " %";
  }
  function statusFill(s) {
    if (s === "ok")   return "#dcfce7";
    if (s === "warn") return "#fef3c7";
    if (s === "risk") return "#fee2e2";
    return "#f3f4f6";
  }
  function statusColor(s) {
    if (s === "ok")   return "#166534";
    if (s === "warn") return "#92400e";
    if (s === "risk") return "#991b1b";
    return "#6b7280";
  }
  function statusLabel(s, lang) {
    if (s === "ok")   return window.L("Sur la cible", "On track", "En objetivo");
    if (s === "warn") return window.L("À surveiller", "Watch", "Por vigilar");
    if (s === "risk") return window.L("À risque", "At risk", "En riesgo");
    return "—";
  }

  // ── UI sub-components ──────────────────────────────────────────────────
  function KpiCard({ label, value, hint, color }) {
    return (
      <div style={{
        background: "var(--bg-elev)",
        border: "1px solid var(--line)",
        borderRadius: 8,
        padding: "14px 18px",
        minWidth: 140,
        flex: 1,
        boxShadow: "0 1px 2px rgba(0,0,0,.04)",
      }}>
        <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: ".5px", color: "var(--muted)", fontWeight: 600 }}>{label}</div>
        <div style={{ fontSize: 28, fontWeight: 800, color: color || "var(--text)", marginTop: 4 }}>{value}</div>
        {hint && <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>{hint}</div>}
      </div>
    );
  }

  function ProjectsTable({ rows, lang, onSort, sortKey, sortDir }) {
    const headerStyle = { textAlign: "left", fontSize: 11, fontWeight: 600, color: "var(--muted)", padding: "8px 10px", borderBottom: "2px solid var(--line)", cursor: "pointer", userSelect: "none" };
    function sortArrow(k) {
      if (sortKey !== k) return "";
      return sortDir === "asc" ? " ↑" : " ↓";
    }
    return (
      <div style={{ overflowX: "auto", background: "var(--bg-elev)", border: "1px solid var(--line)", borderRadius: 8 }}>
        <table style={{ width: "100%", borderCollapse: "collapse" }}>
          <thead>
            <tr>
              <th style={headerStyle} onClick={() => onSort("code")}>{window.L("Code", "Code", "Código")}{sortArrow("code")}</th>
              <th style={headerStyle} onClick={() => onSort("name")}>{window.L("Projet", "Project", "Proyecto")}{sortArrow("name")}</th>
              <th style={headerStyle} onClick={() => onSort("country")}>{window.L("Pays", "Country", "País")}{sortArrow("country")}</th>
              <th style={headerStyle} onClick={() => onSort("phase")}>{window.L("Phase", "Phase", "Fase")}{sortArrow("phase")}</th>
              <th style={{ ...headerStyle, textAlign: "right" }} onClick={() => onSort("indicatorCount")}>{window.L("Indic.", "Ind.", "Ind.")}{sortArrow("indicatorCount")}</th>
              <th style={{ ...headerStyle, textAlign: "right" }} onClick={() => onSort("avgPct")}>{window.L("Moy. % atteinte", "Avg achievement", "% medio de logro")}{sortArrow("avgPct")}</th>
              <th style={{ ...headerStyle, textAlign: "center" }} onClick={() => onSort("status")}>{window.L("Statut", "Status", "Estado")}{sortArrow("status")}</th>
            </tr>
          </thead>
          <tbody>
            {rows.length === 0 && (
              <tr><td colSpan={7} style={{ textAlign: "center", padding: 30, color: "var(--muted)", fontStyle: "italic" }}>
                {window.L("Aucun projet dans le périmètre", "No project in scope", "Ningún proyecto en el perímetro")}
              </td></tr>
            )}
            {rows.map((r) => (
              <tr key={r.project.uuid || r.project.id}>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontWeight: 600, fontSize: 13 }}>{r.project.id || r.project.code}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontSize: 13 }}>{r.project.nameFr || r.project.name_fr || r.project.name || "—"}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontSize: 12, color: "var(--muted)" }}>{(r.project.countries || [r.project.country]).filter(Boolean).join(", ") || "—"}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontSize: 12 }}>{r.project.phase || r.project.status || "—"}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontSize: 13, textAlign: "right" }}>{r.indicatorCount}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", fontSize: 13, textAlign: "right", fontWeight: 600 }}>{fmtPct(r.avgPct)}</td>
                <td style={{ padding: "8px 10px", borderBottom: "1px solid var(--line)", textAlign: "center" }}>
                  <span style={{
                    display: "inline-block", padding: "3px 10px", borderRadius: 12,
                    background: statusFill(r.status), color: statusColor(r.status),
                    fontSize: 11, fontWeight: 600,
                  }}>{statusLabel(r.status, lang)}</span>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  function IndicatorsRollup({ bag, lang }) {
    const levels = [
      { key: "Impact",  fr: "Impact",                       en: "Impact", es: "Impacto" },
      { key: "Outcome", fr: "Résultats intermédiaires",     en: "Intermediate Results", es: "Resultados intermedios" },
      { key: "Output",  fr: "Produits",                     en: "Outputs", es: "Productos" },
    ];

    function indicatorRow(i) {
      const b = Number(i.baseline) || 0;
      const t = Number(i.target) || b;
      const a = Number(i.actual) || 0;
      let pct = null;
      if (t !== b) {
        pct = i.invert ? ((b - a) / (b - t)) * 100 : ((a - b) / (t - b)) * 100;
        pct = Math.max(0, Math.min(150, pct));
      }
      const s = pct == null ? "n/a" : (pct >= 90 ? "ok" : (pct >= 70 ? "warn" : "risk"));
      return (
        <tr key={i.id || i.code}>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", fontSize: 12, fontWeight: 600 }}>{i.id || i.code}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", fontSize: 12 }}>{lang === "es" ? (i.name_es || i.name_en || i.name) : lang === "fr" ? (i.name_fr || i.name) : (i.name_en || i.name)}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", fontSize: 11, color: "var(--muted)" }}>{i.unit || "—"}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", textAlign: "right", fontSize: 12 }}>{i.baseline ?? "—"}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", textAlign: "right", fontSize: 12 }}>{i.target ?? "—"}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", textAlign: "right", fontSize: 12 }}>{i.actual ?? "—"}</td>
          <td style={{ padding: "6px 10px", borderBottom: "1px solid var(--line)", textAlign: "center" }}>
            <span style={{
              display: "inline-block", padding: "2px 8px", borderRadius: 10,
              background: statusFill(s), color: statusColor(s),
              fontSize: 11, fontWeight: 600,
            }}>{fmtPct(pct)}</span>
          </td>
        </tr>
      );
    }

    return (
      <div>
        {levels.map((lvl) => {
          const inds = bag[lvl.key] || [];
          return (
            <div key={lvl.key} style={{ marginBottom: 18 }}>
              <h4 style={{ fontSize: 13, fontWeight: 700, margin: "0 0 8px", color: "var(--text)" }}>{(lang === "es" ? (lvl.es != null ? lvl.es : lvl.en) : lang === "fr" ? lvl.fr : lvl.en)} <span style={{ color: "var(--muted)", fontWeight: 500 }}>({inds.length})</span></h4>
              {inds.length === 0 ? (
                <div style={{ padding: 10, background: "var(--bg)", border: "1px dashed var(--line)", borderRadius: 6, color: "var(--muted)", fontSize: 12, fontStyle: "italic" }}>
                  {window.L("Aucun indicateur à ce niveau.", "No indicators at this level.", "Ningún indicador en este nivel.")}
                </div>
              ) : (
                <div style={{ background: "var(--bg-elev)", border: "1px solid var(--line)", borderRadius: 6, overflowX: "auto" }}>
                  <table style={{ width: "100%", borderCollapse: "collapse" }}>
                    <thead>
                      <tr style={{ background: "var(--bg)" }}>
                        <th style={{ padding: "6px 10px", textAlign: "left", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Code", "Code", "Código")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "left", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Indicateur", "Indicator", "Indicador")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "left", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Unité", "Unit", "Unidad")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "right", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Base", "Baseline", "Línea de base")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "right", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Cible", "Target", "Objetivo")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "right", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("Réel", "Actual", "Real")}</th>
                        <th style={{ padding: "6px 10px", textAlign: "center", fontSize: 11, color: "var(--muted)", fontWeight: 600, borderBottom: "1px solid var(--line)" }}>{window.L("% atteinte", "% achieved", "% de logro")}</th>
                      </tr>
                    </thead>
                    <tbody>{inds.map(indicatorRow)}</tbody>
                  </table>
                </div>
              )}
            </div>
          );
        })}
      </div>
    );
  }

  function AlertsList({ alerts, lang }) {
    if (alerts.length === 0) {
      return (
        <div style={{ padding: 18, background: "#dcfce7", borderRadius: 6, border: "1px solid #86efac", color: "#166534", fontSize: 13 }}>
          ✓ {window.L("Aucune alerte. Tous les indicateurs sont au-dessus de 70 % d'atteinte et tous les projets ont des indicateurs renseignés.", "No alerts. All indicators are above 70 % achievement and every project has indicators.", "Ninguna alerta. Todos los indicadores están por encima del 70 % de logro y todos los proyectos tienen indicadores cargados.")}
        </div>
      );
    }
    return (
      <div style={{ background: "var(--bg-elev)", border: "1px solid var(--line)", borderRadius: 6, maxHeight: 320, overflowY: "auto" }}>
        {alerts.map((a, i) => (
          <div key={i} style={{
            padding: "8px 12px",
            borderBottom: i === alerts.length - 1 ? "none" : "1px solid var(--line)",
            fontSize: 12.5,
            display: "flex",
            alignItems: "flex-start",
            gap: 8,
          }}>
            <span style={{ fontSize: 14 }}>{a.kind === "risk" ? "🔴" : "🟡"}</span>
            <span>{a.message}</span>
          </div>
        ))}
      </div>
    );
  }

  // ── Exports : CSV, Word (.docx), PDF (browser print → save as PDF) ────
  function exportPortfolioExcel(rows, indicators, alerts, lang, scopeLabel) {
    try {
      if (!window.melr || !window.melr.exportCSV) {
        alert(window.L("Export CSV indisponible.", "CSV export unavailable.", "Exportación CSV no disponible."));
        return;
      }
      const cols = [
        { key: "code",           label: window.L("Code projet", "Project code", "Código de proyecto"), value: (r) => r.project.id || r.project.code || "" },
        { key: "name",           label: window.L("Nom projet", "Project name", "Nombre del proyecto"),  value: (r) => r.project.nameFr || r.project.name_fr || r.project.name || "" },
        { key: "country",        label: window.L("Pays", "Country", "País"),             value: (r) => (r.project.countries || [r.project.country]).filter(Boolean).join(", ") },
        { key: "phase",          label: "Phase",                                         value: (r) => r.project.phase || r.project.status || "" },
        { key: "indicators",     label: window.L("Nb indicateurs", "# indicators", "N.º de indicadores"), value: (r) => r.indicatorCount },
        { key: "onTrack",        label: window.L("Sur la cible", "On track", "En objetivo"),     value: (r) => r.onTrack },
        { key: "atRisk",         label: window.L("À risque", "At risk", "En riesgo"),          value: (r) => r.atRisk },
        { key: "avgPct",         label: window.L("Moy. % atteinte", "Avg %", "% medio"),     value: (r) => r.avgPct == null ? "" : Math.round(r.avgPct) + "%" },
        { key: "status",         label: window.L("Statut", "Status", "Estado"),              value: (r) => statusLabel(r.status, lang) },
      ];
      const safeLabel = (scopeLabel || "portfolio").replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
      window.melr.exportCSV("portefeuille-" + safeLabel + ".csv", rows, cols);
    } catch (e) {
      console.error("[portfolio] export CSV:", e);
      alert((window.L("Erreur export : ", "Export error: ", "Error de exportación: ")) + e.message);
    }
  }

  // Word export — proper .docx via window.docx (loaded globally for the
  // rest of the app). Reuses melrDonor helpers if available, otherwise
  // falls back to inline docx-js calls.
  async function exportPortfolioWord(rows, indicators, alerts, kpis, lang, scopeLabel) {
    if (!window.docx || !window.docx.Document || !window.docx.Packer) {
      alert(window.L("Bibliothèque Word indisponible.", "Word library unavailable.", "Biblioteca Word no disponible."));
      return;
    }
    const D = window.docx;
    const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType,
            BorderStyle, ShadingType, AlignmentType, HeadingLevel, Footer, PageNumber } = D;

    const MUTED = "6B7280", LINE = "D1D5DB", PRIMARY = "0F766E", ACCENT = "002F6C";
    const GREEN = "15803D", AMBER = "B45309", RED = "B91C1C";

    function P(t, opts) {
      opts = opts || {};
      return new Paragraph({
        children: [new TextRun({ text: t == null ? "" : String(t), ...(opts.run || {}) })],
        spacing: { after: opts.after == null ? 80 : opts.after },
        alignment: opts.align,
      });
    }
    function H(level, t, opts) {
      opts = opts || {};
      const m = { 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2, 3: HeadingLevel.HEADING_3 };
      return new Paragraph({
        heading: m[level] || HeadingLevel.HEADING_1,
        children: [new TextRun({ text: t || "", color: ACCENT, bold: true, ...(opts.run || {}) })],
        spacing: { before: 280, after: 140 },
      });
    }
    function cellBorder() {
      return {
        top:    { style: BorderStyle.SINGLE, size: 4, color: LINE },
        bottom: { style: BorderStyle.SINGLE, size: 4, color: LINE },
        left:   { style: BorderStyle.SINGLE, size: 4, color: LINE },
        right:  { style: BorderStyle.SINGLE, size: 4, color: LINE },
      };
    }
    function Cell(text, opts) {
      opts = opts || {};
      return new TableCell({
        children: [new Paragraph({
          children: [new TextRun({
            text: text == null ? "" : String(text),
            bold: !!opts.bold, color: opts.color, size: opts.size || 20,
          })],
          alignment: opts.align,
        })],
        width: opts.width ? { size: opts.width, type: WidthType.DXA } : undefined,
        shading: opts.fill ? { type: ShadingType.CLEAR, color: "auto", fill: opts.fill } : undefined,
        margins: { top: 60, bottom: 60, left: 100, right: 100 },
        columnSpan: opts.colSpan,
        borders: cellBorder(),
      });
    }
    function Row(cells) { return new TableRow({ children: cells }); }
    function Tbl(rows, widths) {
      return new Table({
        width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA },
        columnWidths: widths,
        rows,
      });
    }

    function statusFill(s)  { return s === "ok" ? "DCFCE7" : s === "warn" ? "FEF3C7" : s === "risk" ? "FEE2E2" : undefined; }
    function statusColor(s) { return s === "ok" ? GREEN  : s === "warn" ? AMBER  : s === "risk" ? RED  : MUTED; }

    const children = [];

    // Cover
    children.push(new Paragraph({ spacing: { before: 1200 }, children: [] }));
    children.push(new Paragraph({
      alignment: AlignmentType.CENTER,
      children: [new TextRun({ text: window.L("Tableau bailleur", "Donor Portfolio Dashboard", "Cuadro de mando del donante"), bold: true, size: 40, color: PRIMARY })],
    }));
    children.push(new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { after: 600 },
      children: [new TextRun({ text: scopeLabel || (window.L("Portefeuille complet", "Full portfolio", "Cartera completa")), size: 28 })],
    }));
    children.push(new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { after: 1600 },
      children: [new TextRun({ text: (window.L("Document généré le ", "Generated on ", "Documento generado el ")) + new Date().toLocaleDateString(window.L("fr-FR", "en-US", "es-ES")), size: 20, color: MUTED, italics: true })],
    }));

    // KPIs
    children.push(new Paragraph({ children: [new D.PageBreak()] }));
    children.push(H(1, window.L("Indicateurs clés (KPIs)", "Key Performance Indicators (KPIs)", "Indicadores clave (KPIs)")));
    const kpiHdrFill = { fill: ACCENT, color: "FFFFFF", bold: true };
    const kpiRows = [
      Row([
        Cell(window.L("Indicateur", "KPI", "KPI"), { ...kpiHdrFill, width: 5000 }),
        Cell(window.L("Valeur", "Value", "Valor"),   { ...kpiHdrFill, width: 4360, align: AlignmentType.CENTER }),
      ]),
      Row([
        Cell(window.L("Nombre de projets", "Number of projects", "Número de proyectos"),   { width: 5000 }),
        Cell(String(kpis.projectCount), { width: 4360, bold: true, align: AlignmentType.CENTER }),
      ]),
      Row([
        Cell(window.L("Nombre d'indicateurs", "Number of indicators", "Número de indicadores"), { width: 5000 }),
        Cell(String(kpis.indicatorCount), { width: 4360, bold: true, align: AlignmentType.CENTER }),
      ]),
      Row([
        Cell(window.L("Indicateurs sur la cible (≥ 90 %)", "On-track indicators (≥ 90 %)", "Indicadores en objetivo (≥ 90 %)"), { width: 5000 }),
        Cell(String(kpis.onTrack), { width: 4360, bold: true, color: GREEN, align: AlignmentType.CENTER }),
      ]),
      Row([
        Cell(window.L("Indicateurs à risque (< 70 %)", "At-risk indicators (< 70 %)", "Indicadores en riesgo (< 70 %)"), { width: 5000 }),
        Cell(String(kpis.atRisk), { width: 4360, bold: true, color: RED, align: AlignmentType.CENTER }),
      ]),
      Row([
        Cell(window.L("Moyenne d'atteinte", "Average achievement", "Media de logro"), { width: 5000 }),
        Cell(kpis.avgAchievement == null ? "—" : Math.round(kpis.avgAchievement) + " %", {
          width: 4360, bold: true, align: AlignmentType.CENTER,
          color: kpis.avgAchievement == null ? MUTED : (kpis.avgAchievement >= 90 ? GREEN : kpis.avgAchievement >= 70 ? AMBER : RED),
        }),
      ]),
    ];
    children.push(Tbl(kpiRows, [5000, 4360]));

    // Projects table
    children.push(new Paragraph({ children: [new D.PageBreak()] }));
    children.push(H(1, window.L("Projets du portefeuille", "Portfolio projects", "Proyectos de la cartera")));
    const projHdr = { ...kpiHdrFill };
    const projHeaderRow = Row([
      Cell(window.L("Code", "Code", "Código"),                  { ...projHdr, width: 800 }),
      Cell(window.L("Projet", "Project", "Proyecto"),             { ...projHdr, width: 3200 }),
      Cell(window.L("Pays", "Country", "País"),               { ...projHdr, width: 1600 }),
      Cell(window.L("Phase", "Phase", "Fase"),                { ...projHdr, width: 1200 }),
      Cell(window.L("Ind.", "Ind.", "Ind."),                  { ...projHdr, width: 600, align: AlignmentType.CENTER }),
      Cell(window.L("Moy. %", "Avg %", "% medio"),               { ...projHdr, width: 800, align: AlignmentType.CENTER }),
      Cell(window.L("Statut", "Status", "Estado"),              { ...projHdr, width: 1160, align: AlignmentType.CENTER }),
    ]);
    const projRows = [projHeaderRow];
    rows.forEach((r) => {
      projRows.push(Row([
        Cell(r.project.id || r.project.code || "—",                                              { width: 800, bold: true, size: 18 }),
        Cell(r.project.nameFr || r.project.name_fr || r.project.name || "—",                    { width: 3200, size: 18 }),
        Cell((r.project.countries || [r.project.country]).filter(Boolean).join(", ") || "—",    { width: 1600, size: 18 }),
        Cell(r.project.phase || r.project.status || "—",                                         { width: 1200, size: 18 }),
        Cell(String(r.indicatorCount),                                                            { width: 600, size: 18, align: AlignmentType.CENTER }),
        Cell(r.avgPct == null ? "—" : Math.round(r.avgPct) + " %",                              { width: 800, size: 18, align: AlignmentType.CENTER, bold: true }),
        Cell(statusLabel(r.status, lang), {
          width: 1160, size: 18, align: AlignmentType.CENTER, bold: true,
          fill: statusFill(r.status), color: statusColor(r.status),
        }),
      ]));
    });
    if (rows.length === 0) {
      projRows.push(Row([Cell(window.L("Aucun projet dans le périmètre", "No project in scope", "Ningún proyecto en el perímetro"),
        { colSpan: 7, color: MUTED, align: AlignmentType.CENTER })]));
    }
    children.push(Tbl(projRows, [800, 3200, 1600, 1200, 600, 800, 1160]));

    // Indicators rollup
    children.push(new Paragraph({ children: [new D.PageBreak()] }));
    children.push(H(1, window.L("Indicateurs consolidés", "Consolidated indicators", "Indicadores consolidados")));
    const bag = rollupByLevel(indicators);
    const levels = [
      { key: "Impact",  fr: "Impact",                       en: "Impact", es: "Impacto" },
      { key: "Outcome", fr: "Résultats intermédiaires",     en: "Intermediate Results", es: "Resultados intermedios" },
      { key: "Output",  fr: "Produits",                     en: "Outputs", es: "Productos" },
    ];
    levels.forEach((lvl) => {
      children.push(H(2, (lang === "es" ? (lvl.es != null ? lvl.es : lvl.en) : lang === "fr" ? lvl.fr : lvl.en)));
      const arr = bag[lvl.key] || [];
      if (arr.length === 0) {
        children.push(P(window.L("Aucun indicateur à ce niveau.", "No indicators at this level.", "Ningún indicador en este nivel."),
          { run: { italics: true, color: MUTED } }));
        return;
      }
      const indHeader = Row([
        Cell(window.L("Code", "Code", "Código"),         { ...kpiHdrFill, width: 900 }),
        Cell(window.L("Indicateur", "Indicator", "Indicador"), { ...kpiHdrFill, width: 3800 }),
        Cell(window.L("Unité", "Unit", "Unidad"),        { ...kpiHdrFill, width: 700 }),
        Cell(window.L("Base", "Baseline", "Línea de base"),     { ...kpiHdrFill, width: 800, align: AlignmentType.CENTER }),
        Cell(window.L("Cible", "Target", "Objetivo"),      { ...kpiHdrFill, width: 800, align: AlignmentType.CENTER }),
        Cell(window.L("Réel", "Actual", "Real"),       { ...kpiHdrFill, width: 800, align: AlignmentType.CENTER }),
        Cell("%",                                       { ...kpiHdrFill, width: 1560, align: AlignmentType.CENTER }),
      ]);
      const indRows = [indHeader];
      arr.forEach((i) => {
        const b = Number(i.baseline) || 0;
        const t = Number(i.target) || b;
        const a = Number(i.actual) || 0;
        let pct = null;
        if (t !== b) {
          pct = i.invert ? ((b - a) / (b - t)) * 100 : ((a - b) / (t - b)) * 100;
          pct = Math.max(0, Math.min(150, pct));
        }
        const s = pct == null ? "n/a" : (pct >= 90 ? "ok" : (pct >= 70 ? "warn" : "risk"));
        indRows.push(Row([
          Cell(i.id || i.code || "—",                                                  { width: 900, size: 18, bold: true }),
          Cell(lang === "es" ? (i.name_es || i.name_en || i.name) : lang === "fr" ? (i.name_fr || i.name) : (i.name_en || i.name),         { width: 3800, size: 18 }),
          Cell(i.unit || "—",                                                          { width: 700, size: 18 }),
          Cell(i.baseline ?? "—",                                                       { width: 800, size: 18, align: AlignmentType.CENTER }),
          Cell(i.target ?? "—",                                                         { width: 800, size: 18, align: AlignmentType.CENTER }),
          Cell(i.actual ?? "—",                                                         { width: 800, size: 18, align: AlignmentType.CENTER }),
          Cell(pct == null ? "—" : Math.round(pct) + " %", {
            width: 1560, size: 18, align: AlignmentType.CENTER, bold: true,
            fill: statusFill(s), color: statusColor(s),
          }),
        ]));
      });
      children.push(Tbl(indRows, [900, 3800, 700, 800, 800, 800, 1560]));
    });

    // Alerts
    children.push(new Paragraph({ children: [new D.PageBreak()] }));
    children.push(H(1, window.L("Alertes", "Alerts", "Alertas")));
    if (alerts.length === 0) {
      children.push(P(window.L("Aucune alerte. Tous les indicateurs sont au-dessus de 70 % d'atteinte.", "No alerts. All indicators are above 70 % achievement.", "Ninguna alerta. Todos los indicadores están por encima del 70 % de logro."),
        { run: { color: GREEN, bold: true } }));
    } else {
      alerts.forEach((a) => {
        children.push(new Paragraph({
          children: [
            new TextRun({ text: (a.kind === "risk" ? "🔴 " : "🟡 "), size: 22 }),
            new TextRun({ text: a.message, size: 22 }),
          ],
          spacing: { after: 80 },
        }));
      });
    }

    const doc = new Document({
      creator: "MELR",
      title: "Portfolio Dashboard — " + (scopeLabel || "Portfolio"),
      description: "MELR portfolio dashboard auto-generated",
      styles: { default: { document: { run: { font: "Calibri", size: 22 } } } },
      sections: [{
        properties: {
          page: { margin: { top: 1200, right: 1200, bottom: 1200, left: 1200 }, size: { width: 11906, height: 16838 } },
        },
        footers: {
          default: new Footer({
            children: [new Paragraph({
              alignment: AlignmentType.CENTER,
              children: [
                new TextRun({ text: (window.L("Tableau bailleur · ", "Portfolio Dashboard · ", "Cuadro de mando del donante · ")) + (scopeLabel || "") + " · ", color: MUTED, size: 18 }),
                new TextRun({ children: [PageNumber.CURRENT], color: MUTED, size: 18 }),
                new TextRun({ text: " / ", color: MUTED, size: 18 }),
                new TextRun({ children: [PageNumber.TOTAL_PAGES], color: MUTED, size: 18 }),
              ],
            })],
          }),
        },
        children,
      }],
    });

    try {
      const blob = await Packer.toBlob(doc);
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      const safeLabel = (scopeLabel || "portfolio").replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
      a.href = url;
      a.download = "portefeuille-" + safeLabel + ".docx";
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
    } catch (e) {
      console.error("[portfolio] export Word:", e);
      alert((window.L("Erreur export Word : ", "Word export error: ", "Error de exportación a Word: ")) + e.message);
    }
  }

  // PDF export — uses window.print on a dedicated print stylesheet that
  // hides everything outside the portfolio container. The user picks
  // "Save as PDF" in the print dialog. Cross-browser, no extra deps.
  function exportPortfolioPdf(lang) {
    // Mark the body so the @media print rules in our inline <style> kick in
    document.documentElement.classList.add("melr-print-portfolio");
    setTimeout(() => {
      try { window.print(); } finally {
        // Restore after the print dialog closes (250ms is plenty since
        // print is synchronous in modern browsers)
        setTimeout(() => document.documentElement.classList.remove("melr-print-portfolio"), 500);
      }
    }, 50);
  }

  // ── Main Portfolio component ───────────────────────────────────────────
  function PortfolioDashboard({ projects, indicators, programmes, lang }) {
    const [scopeMode, setScopeMode] = useState("all");
    const [scopeId, setScopeId] = useState("");
    const [sortKey, setSortKey] = useState("avgPct");
    const [sortDir, setSortDir] = useState("desc");

    // Filter projects + indicators by chosen scope
    const { scopedProjects, scopedIndicators, scopeLabel } = useMemo(() => {
      let sp = projects;
      let si = indicators;
      let label = window.L("Portefeuille complet", "Full portfolio", "Cartera completa");
      if (scopeMode === "programme" && scopeId) {
        sp = projects.filter((p) => p.programme_id === scopeId);
        const projIds = new Set(sp.map((p) => p.uuid || p.id));
        si = indicators.filter((i) => projIds.has(i.project_uuid || i.projectId || i.project));
        const prog = programmes.find((p) => p.id === scopeId);
        label = prog ? ((prog.code || "") + " — " + (lang === "es" ? (prog.name_es || prog.name_en || prog.name_fr || "") : lang === "fr" ? (prog.name_fr || "") : (prog.name_en || prog.name_fr || ""))) : "Programme";
      }
      return { scopedProjects: sp, scopedIndicators: si, scopeLabel: label };
    }, [projects, indicators, programmes, scopeMode, scopeId, lang]);

    // Aggregate per project
    const projectStats = useMemo(() =>
      scopedProjects.map((p) => aggregateProject(p, scopedIndicators)),
      [scopedProjects, scopedIndicators]);

    // Sort
    const sortedRows = useMemo(() => {
      const arr = [...projectStats];
      arr.sort((a, b) => {
        let va, vb;
        switch (sortKey) {
          case "code":           va = a.project.id || a.project.code || ""; vb = b.project.id || b.project.code || ""; break;
          case "name":           va = a.project.nameFr || a.project.name_fr || a.project.name || ""; vb = b.project.nameFr || b.project.name_fr || b.project.name || ""; break;
          case "country":        va = ((a.project.countries || [a.project.country])[0] || ""); vb = ((b.project.countries || [b.project.country])[0] || ""); break;
          case "phase":          va = a.project.phase || a.project.status || ""; vb = b.project.phase || b.project.status || ""; break;
          case "indicatorCount": va = a.indicatorCount; vb = b.indicatorCount; break;
          case "avgPct":         va = a.avgPct == null ? -1 : a.avgPct; vb = b.avgPct == null ? -1 : b.avgPct; break;
          case "status": {
            const order = { ok: 3, warn: 2, risk: 1, "n/a": 0 };
            va = order[a.status] || 0; vb = order[b.status] || 0;
            break;
          }
          default: va = 0; vb = 0;
        }
        const cmp = va < vb ? -1 : va > vb ? 1 : 0;
        return sortDir === "asc" ? cmp : -cmp;
      });
      return arr;
    }, [projectStats, sortKey, sortDir]);

    // KPIs
    const kpis = useMemo(() => {
      const totalInd = scopedIndicators.length;
      let totalOnTrack = 0;
      let totalAtRisk = 0;
      let sumPct = 0; let countPct = 0;
      projectStats.forEach((p) => {
        totalOnTrack += p.onTrack;
        totalAtRisk  += p.atRisk;
        if (p.avgPct != null) { sumPct += p.avgPct; countPct++; }
      });
      const avgPct = countPct > 0 ? sumPct / countPct : null;
      return {
        projectCount: scopedProjects.length,
        indicatorCount: totalInd,
        onTrack: totalOnTrack,
        atRisk: totalAtRisk,
        avgAchievement: avgPct,
      };
    }, [scopedProjects, scopedIndicators, projectStats]);

    const bag = useMemo(() => rollupByLevel(scopedIndicators), [scopedIndicators]);
    const alerts = useMemo(() => detectAlerts(scopedProjects, scopedIndicators, lang),
      [scopedProjects, scopedIndicators, lang]);

    function onSort(key) {
      if (sortKey === key) setSortDir(sortDir === "asc" ? "desc" : "asc");
      else { setSortKey(key); setSortDir(key === "avgPct" || key === "indicatorCount" || key === "status" ? "desc" : "asc"); }
    }

    // Section wrapper helper — gives each block its own subtle bg + left
    // border color so blocks are visually distinct even at a glance.
    const sectionStyle = (tintHex, borderHex) => ({
      background: tintHex,
      borderLeft: "4px solid " + borderHex,
      padding: "14px 16px",
      borderRadius: 8,
      marginBottom: 16,
    });
    const sectionTitleStyle = (color) => ({
      fontSize: 14, fontWeight: 700, margin: "0 0 10px", color,
      display: "flex", alignItems: "center", gap: 8,
    });

    return (
      <div id="melr-portfolio-printable">
        {/* Inline print stylesheet : only this container is printed when
            html.melr-print-portfolio is set on <html>. Hides everything
            else and lets the browser dialog "Save as PDF" produce a clean
            single-document PDF. */}
        <style>{`
          @media print {
            html.melr-print-portfolio body * { visibility: hidden !important; }
            html.melr-print-portfolio #melr-portfolio-printable,
            html.melr-print-portfolio #melr-portfolio-printable * { visibility: visible !important; }
            html.melr-print-portfolio #melr-portfolio-printable {
              position: absolute !important; left: 0; top: 0; width: 100% !important;
              padding: 12px !important;
            }
            /* Hide the export toolbar buttons themselves */
            html.melr-print-portfolio .melr-portfolio-noprint { display: none !important; }
            /* Force wider table layout for print */
            html.melr-print-portfolio table { font-size: 10pt !important; }
          }
        `}</style>

        {/* ===== Scope + Export toolbar (orange/amber tint) ===== */}
        <div className="melr-portfolio-noprint" style={sectionStyle("rgba(245, 158, 11, 0.06)", "#f59e0b")}>
          <div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: "var(--text)" }}>
              {window.L("Périmètre :", "Scope:", "Perímetro:")}
            </div>
            <div className="seg" style={{ display: "inline-flex" }}>
              <button className={"seg-btn" + (scopeMode === "all" ? " active" : "")} onClick={() => { setScopeMode("all"); setScopeId(""); }}>
                {window.L("Tous projets", "All projects", "Todos los proyectos")}
              </button>
              <button className={"seg-btn" + (scopeMode === "programme" ? " active" : "")} onClick={() => setScopeMode("programme")}>
                {window.L("Par programme", "By programme", "Por programa")}
              </button>
            </div>
            {scopeMode === "programme" && (
              <select value={scopeId} onChange={(e) => setScopeId(e.target.value)} className="inp"
                style={{ padding: "6px 10px", fontSize: 13, minWidth: 220 }}>
                <option value="">{window.L("— Choisir un programme —", "— Select a programme —", "— Elegir un programa —")}</option>
                {(programmes || []).map((p) => (
                  <option key={p.id} value={p.id}>{p.code} — {(lang === "es" ? (p.name_es != null ? p.name_es : p.name_en || p.name_fr) : lang === "fr" ? p.name_fr : p.name_en || p.name_fr)}</option>
                ))}
              </select>
            )}
            <div style={{ flex: 1 }} />
            <span style={{ fontSize: 12, color: "var(--muted)", fontStyle: "italic" }}>{scopeLabel}</span>
            <button onClick={() => exportPortfolioExcel(sortedRows, scopedIndicators, alerts, lang, scopeLabel)}
              disabled={sortedRows.length === 0}
              title={window.L("Exporter la table des projets en CSV (ouvrable dans Excel)", "Export projects table as CSV (opens in Excel)", "Exportar la tabla de proyectos en CSV (se abre en Excel)")}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #16a34a", background: "#16a34a", color: "white", cursor: sortedRows.length === 0 ? "not-allowed" : "pointer", fontSize: 12.5, fontWeight: 600, opacity: sortedRows.length === 0 ? 0.5 : 1 }}>
              📊 CSV / Excel
            </button>
            <button onClick={() => exportPortfolioWord(sortedRows, scopedIndicators, alerts, kpis, lang, scopeLabel)}
              disabled={sortedRows.length === 0 || !window.docx}
              title={window.L("Exporter le tableau de bord complet en Word (.docx)", "Export the full dashboard as Word (.docx)", "Exportar el cuadro de mando completo en Word (.docx)")}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #2563eb", background: "#2563eb", color: "white", cursor: (sortedRows.length === 0 || !window.docx) ? "not-allowed" : "pointer", fontSize: 12.5, fontWeight: 600, opacity: (sortedRows.length === 0 || !window.docx) ? 0.5 : 1 }}>
              📄 Word
            </button>
            <button onClick={() => exportPortfolioPdf(lang)}
              disabled={sortedRows.length === 0}
              title={window.L("Ouvrir la boîte de dialogue d'impression — choisir 'Enregistrer en PDF'", "Open print dialog — choose 'Save as PDF'", "Abrir el cuadro de diálogo de impresión — elegir 'Guardar como PDF'")}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #dc2626", background: "#dc2626", color: "white", cursor: sortedRows.length === 0 ? "not-allowed" : "pointer", fontSize: 12.5, fontWeight: 600, opacity: sortedRows.length === 0 ? 0.5 : 1 }}>
              📕 PDF
            </button>
          </div>
        </div>

        {/* ===== KPI cards (blue tint) ===== */}
        <div style={sectionStyle("rgba(59, 130, 246, 0.06)", "#3b82f6")}>
          <div style={sectionTitleStyle("#1e40af")}>
            <span>📊</span>
            <span>{window.L("Indicateurs clés du portefeuille", "Portfolio Key Performance Indicators", "Indicadores clave de la cartera")}</span>
          </div>
          <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
            <KpiCard label={window.L("Projets", "Projects", "Proyectos")} value={kpis.projectCount} />
            <KpiCard label={window.L("Indicateurs", "Indicators", "Indicadores")} value={kpis.indicatorCount} />
            <KpiCard label={window.L("Sur la cible", "On track", "En objetivo")} value={kpis.onTrack} color="#15803d" hint={window.L("≥ 90 %", "≥ 90 %", "≥ 90 %")} />
            <KpiCard label={window.L("À risque", "At risk", "En riesgo")} value={kpis.atRisk} color="#b91c1c" hint={window.L("< 70 %", "< 70 %", "< 70 %")} />
            <KpiCard label={window.L("Moy. atteinte", "Avg achievement", "Media de logro")} value={fmtPct(kpis.avgAchievement)}
              color={kpis.avgAchievement == null ? "var(--text)" : (kpis.avgAchievement >= 90 ? "#15803d" : kpis.avgAchievement >= 70 ? "#b45309" : "#b91c1c")} />
          </div>
        </div>

        {/* ===== Projects table (green tint) ===== */}
        <div style={sectionStyle("rgba(16, 185, 129, 0.06)", "#10b981")}>
          <div style={sectionTitleStyle("#065f46")}>
            <span>🏗️</span>
            <span>{window.L("Projets du portefeuille", "Portfolio projects", "Proyectos de la cartera")}</span>
            <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--muted)", fontWeight: 500 }}>
              ({sortedRows.length} {window.L("projets", "projects", "proyectos")})
            </span>
          </div>
          <ProjectsTable rows={sortedRows} lang={lang} onSort={onSort} sortKey={sortKey} sortDir={sortDir} />
        </div>

        {/* ===== Indicators roll-up (purple tint) ===== */}
        <div style={sectionStyle("rgba(168, 85, 247, 0.06)", "#a855f7")}>
          <div style={sectionTitleStyle("#6b21a8")}>
            <span>📈</span>
            <span>{window.L("Indicateurs consolidés (par niveau)", "Consolidated indicators (by level)", "Indicadores consolidados (por nivel)")}</span>
            <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--muted)", fontWeight: 500 }}>
              ({scopedIndicators.length} {window.L("indicateurs", "indicators", "indicadores")})
            </span>
          </div>
          <IndicatorsRollup bag={bag} lang={lang} />
        </div>

        {/* ===== Alerts (red tint) ===== */}
        <div style={sectionStyle("rgba(239, 68, 68, 0.06)", "#ef4444")}>
          <div style={sectionTitleStyle("#991b1b")}>
            <span>🚨</span>
            <span>{window.L("Alertes", "Alerts", "Alertas")}</span>
            <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--muted)", fontWeight: 500 }}>
              ({alerts.length} {window.L("alerte(s)", "alert(s)", "alerta(s)")})
            </span>
          </div>
          <AlertsList alerts={alerts} lang={lang} />
        </div>
      </div>
    );
  }

  // Expose globally so screens-reporting.jsx can render it
  window.PortfolioDashboard = PortfolioDashboard;
})();
