/* global React, Icon, Modal, NewSectorModal, sectorById, sectorLabel */
const { useState: useStateID, useEffect: useEffectID, useRef: useRefID } = React;

// ============================================================================
// INDICATOR DEFINITIONS — org-wide catalogue (admin only)
// ----------------------------------------------------------------------------
// Each row carries metadata for one indicator: code, bilingual name, origin
// institution (REFT, USAID, WHO…), level (output/outcome/impact/context) and
// an optional PIRS attachment (Performance Indicator Reference Sheet) — a
// Word / Excel / PDF file stored in the private indicator-pirs bucket.
//
// The Suivi des indicateurs module picks codes from this catalogue when
// creating a project-instance indicator.
// ============================================================================

const LEVELS = [
  { v: "output",  fr: "Output",                 en: "Output"                  },
  { v: "outcome", fr: "Outcome",                en: "Outcome"                 },
  // "Mixte" indicators span both Output and Outcome (common case in real
  // M&E frameworks). Logframe-aware views treat them as part of the core
  // logframe (output ∪ outcome ∪ impact ∪ mixed).
  { v: "mixed",   fr: "Mixte (Output/Outcome)", en: "Mixed (Output/Outcome)"  },
  { v: "impact",  fr: "Impact",                 en: "Impact"                  },
  { v: "context", fr: "Context",                en: "Context"                 },
];
// Friendly label for any level slug. Falls back to the raw slug if a new
// value has been added in DB but not yet listed in LEVELS.
function levelLabel(slug, lang) {
  const L = LEVELS.find((l) => l.v === slug);
  if (!L) return slug || "";
  return lang === "fr" ? L.fr : L.en;
}

function IndicatorDefinitions({ t, lang, isSuperAdmin, actingOrgId, effectiveOrgId, myOrgId, isAdmin, hasPerm }) {
  // Phase 5 RLS (performance-rls-permissive-consolidation-phase5-indicators.sql)
  // makes indicator_definitions write-restricted to admin + super-admin. UI
  // mirrors that: hide the write buttons (Nouvelle définition + Importer)
  // from users without users.manage. canManage is true for super-admin,
  // is_admin_user (=users.manage), or anyone whose hasPerm says so.
  const canManage = !!isSuperAdmin || !!isAdmin
    || (hasPerm && hasPerm("users.manage"));
  const { has, loading: permsLoading } = window.melr.useCurrentUserPermissions();
  const { data: rawDefs, loading, realtime, refresh } = window.melr.useIndicatorDefinitions();
  // Live sectors for the bulk "Affecter à un secteur" picker.
  const { data: liveSectors }   = window.melr.useSectors();
  // Super-admin acting-as-org: scope the catalogue to the target org so the
  // page reflects what that org's admin would see — BUT keep shared (cross-
  // org) definitions visible because they're universal reference data that
  // every org sees by design. The CRUD modal (below) also propagates
  // effectiveOrgId on create so new private definitions land in the right
  // org; shared definitions are super-admin-only and bypass org scoping.
  const defs = (isSuperAdmin && actingOrgId && actingOrgId !== myOrgId)
    ? (rawDefs || []).filter((d) => d.is_shared || d.organization_id === actingOrgId)
    : (rawDefs || []);
  const LiveBadge = window.melr.LiveBadge;
  const [editing, setEditing]   = useStateID(null);   // "new" | row.id
  const [filter, setFilter]     = useStateID("all");  // level filter
  const [importOpen, setImportOpen] = useStateID(false);
  const [busy, setBusy]         = useStateID(null);
  // Bulk-selection state: a Set of row ids. The toolbar appears as soon as
  // it's non-empty. Selection is cleared after a successful bulk action.
  const [selectedIds, setSelectedIds] = useStateID(() => new Set());
  const [bulkBusy, setBulkBusy]       = useStateID(false);
  const [bulkSectorId, setBulkSectorId] = useStateID("");
  const [bulkToast, setBulkToast]     = useStateID(null);
  // Bulk-delete is a separate code-path from bulkUpdate because it's
  // destructive and needs an explicit confirmation. The Set captures
  // the ids to delete at the moment the user opens the modal — so even
  // if they keep clicking around, the count shown stays accurate.
  const [bulkDeleteConfirm, setBulkDeleteConfirm] = useStateID(null);
  // Column sort: { key: 'sector' | 'origin' | 'level', dir: 'asc' | 'desc' }.
  // null = no sort (preserve the catalogue's natural "by code" ordering).
  // Clicking the same column header cycles: none → asc → desc → none.
  const [sort, setSort] = useStateID(null);
  const cycleSort = (key) => {
    setSort((prev) => {
      if (!prev || prev.key !== key) return { key, dir: "asc" };
      if (prev.dir === "asc")        return { key, dir: "desc" };
      return null;
    });
  };
  const sortArrow = (key) => {
    if (!sort || sort.key !== key) return "↕";
    return sort.dir === "asc" ? "↑" : "↓";
  };

  // Bulk-selection helpers and auto-cleanup. When the level filter changes
  // or rows are deleted/moved out of scope, we prune the selection so the
  // toolbar count stays accurate.
  const toggleRow = (id) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };
  const clearSelection = () => setSelectedIds(new Set());
  const showBulkToast = (ok, msg, ttl) => {
    setBulkToast({ ok, msg });
    setTimeout(() => setBulkToast(null), ttl || 5000);
  };
  const runBulk = async (patch, label) => {
    const ids = Array.from(selectedIds);
    if (ids.length === 0) return;
    setBulkBusy(true);
    try {
      const res = await window.melr.indicatorDefinitionsCrud.bulkUpdate(ids, patch);
      const rejected = res.attempted - res.updated;
      const msg = lang === "fr"
        ? `✓ ${res.updated} indicateur${res.updated > 1 ? "s" : ""} ${label.fr}` +
          (rejected > 0
            ? ` · ${rejected} ${rejected > 1 ? "lignes rejetées" : "ligne rejetée"} (permission insuffisante)`
            : "")
        : `✓ ${res.updated} indicator${res.updated > 1 ? "s" : ""} ${label.en}` +
          (rejected > 0
            ? ` · ${rejected} ${rejected > 1 ? "rows rejected" : "row rejected"} (insufficient permission)`
            : "");
      showBulkToast(true, msg, rejected > 0 ? 7500 : 4500);
      clearSelection();
      await refresh();
    } catch (e) {
      showBulkToast(false, (lang === "fr" ? "Erreur : " : "Error: ") + e.message, 7500);
    } finally {
      setBulkBusy(false);
    }
  };

  // Bulk delete · super-admin only. Opens a confirmation modal first,
  // then on confirm calls bulkRemove and reports the outcome.
  const runBulkDelete = async () => {
    if (!bulkDeleteConfirm || bulkDeleteConfirm.ids.length === 0) return;
    const ids = bulkDeleteConfirm.ids;
    setBulkBusy(true);
    setBulkDeleteConfirm(null);
    try {
      const res = await window.melr.indicatorDefinitionsCrud.bulkRemove(ids);
      const rejected = res.attempted - res.deleted;
      const msg = lang === "fr"
        ? `✓ ${res.deleted} indicateur${res.deleted > 1 ? "s" : ""} supprimé${res.deleted > 1 ? "s" : ""}` +
          (rejected > 0 ? ` · ${rejected} ${rejected > 1 ? "lignes rejetées" : "ligne rejetée"} (permission insuffisante)` : "")
        : `✓ ${res.deleted} indicator${res.deleted > 1 ? "s" : ""} deleted` +
          (rejected > 0 ? ` · ${rejected} ${rejected > 1 ? "rows rejected" : "row rejected"} (insufficient permission)` : "");
      showBulkToast(true, msg, rejected > 0 ? 7500 : 4500);
      clearSelection();
      await refresh();
    } catch (e) {
      showBulkToast(false, (lang === "fr" ? "Erreur suppression : " : "Delete error: ") + e.message, 7500);
    } finally {
      setBulkBusy(false);
    }
  };

  // Attachment counts per definition — one cheap query when the list of
  // definitions changes so the table can show a "🗎 N" badge per row.
  const [attachmentCounts, setAttachmentCounts] = useStateID({});
  useEffectID(() => {
    let alive = true;
    (async () => {
      const sb = window.melr.supabase;
      if (!sb) return;
      const r = await sb.from("indicator_definition_attachments").select("definition_id");
      if (!alive || r.error) return;
      const counts = {};
      (r.data || []).forEach((row) => { counts[row.definition_id] = (counts[row.definition_id] || 0) + 1; });
      setAttachmentCounts(counts);
    })();
    return () => { alive = false; };
  }, [defs]);

  // Prune the selection when a row leaves the visible set (deleted, level
  // filter changed, acting-as-org switched, etc.) so the toolbar count
  // stays in sync with what the user actually sees.
  useEffectID(() => {
    if (selectedIds.size === 0) return;
    const visibleIds = new Set(defs.map((d) => d.id));
    let mutated = false;
    const next = new Set();
    selectedIds.forEach((id) => { if (visibleIds.has(id)) next.add(id); else mutated = true; });
    if (mutated) setSelectedIds(next);
  }, [defs]);

  if (permsLoading) return <div className="page"><div style={{ padding: 28 }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div></div>;
  if (!has("users.manage")) {
    return (
      <div className="page">
        <div className="page-header">
          <div className="page-eyebrow">{lang === "fr" ? "INDICATEURS" : "INDICATORS"}</div>
          <h1 className="page-title">{lang === "fr" ? "Définition des indicateurs" : "Indicator definitions"}</h1>
        </div>
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="text-faint" style={{ textAlign: "center" }}>
            {lang === "fr"
              ? "Accès réservé aux administrateurs (permission users.manage)."
              : "Admins only (users.manage permission required)."}
          </div>
        </div>
      </div>
    );
  }

  const onDelete = async (row) => {
    if (!window.confirm(lang === "fr"
      ? "Supprimer la définition « " + row.code + " » ? Les indicateurs projet qui s'y rattachent ne seront pas supprimés, juste détachés."
      : "Delete definition '" + row.code + "'? Project indicators linked to it stay; only the link is dropped.")) return;
    setBusy(row.id);
    try { await window.melr.indicatorDefinitionsCrud.remove(row.id); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusy(null); }
  };

  const filtered = filter === "all" ? defs : defs.filter((d) => d.level === filter);

  // Apply the active column sort, if any. We use locale-aware comparison so
  // accented characters (Éducation, Énergie, …) sort the way a French
  // speaker would expect. Empty values always fall to the BOTTOM regardless
  // of direction so the user can keep "missing data" rows out of the way.
  const sortKeyValue = (d, key) => {
    if (key === "sector") {
      if (!d.sector_id) return "";
      const s = (typeof sectorById === "function") ? sectorById(d.sector_id) : null;
      return (s && s.fr) ? s.fr : d.sector_id;
    }
    if (key === "origin") return d.origin_institution || "";
    if (key === "level")  return d.level || "";
    return "";
  };
  const visible = !sort ? filtered : filtered.slice().sort((a, b) => {
    const va = sortKeyValue(a, sort.key);
    const vb = sortKeyValue(b, sort.key);
    const aEmpty = !va, bEmpty = !vb;
    if (aEmpty && bEmpty) return 0;
    if (aEmpty) return 1;          // empties always last
    if (bEmpty) return -1;
    const cmp = va.localeCompare(vb, lang === "fr" ? "fr" : "en", { sensitivity: "base" });
    return sort.dir === "asc" ? cmp : -cmp;
  });

  // Select-all state for the header checkbox: "all" / "some" / "none".
  // We compare against the CURRENTLY VISIBLE rows so toggling the level
  // filter doesn't sweep up hidden rows accidentally.
  const visibleIds = visible.map((d) => d.id);
  const selectedCount = visibleIds.filter((id) => selectedIds.has(id)).length;
  const allVisibleSelected = visibleIds.length > 0 && selectedCount === visibleIds.length;
  const someVisibleSelected = selectedCount > 0 && !allVisibleSelected;
  const toggleAllVisible = () => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (allVisibleSelected) {
        // Deselect everything currently visible (keep selections from a
        // different filter, if any — unlikely but a defensive choice).
        visibleIds.forEach((id) => next.delete(id));
      } else {
        visibleIds.forEach((id) => next.add(id));
      }
      return next;
    });
  };

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "INDICATEURS · CATALOGUE" : "INDICATORS · CATALOGUE"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {lang === "fr" ? "Définition des indicateurs" : "Indicator definitions"}{" "}
              <LiveBadge on={realtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {lang === "fr"
                ? "Catalogue organisationnel des indicateurs (code, nom, origine, niveau, fiche PIRS). Les agents les sélectionnent depuis « Suivi des indicateurs »."
                : "Org-wide indicator catalogue (code, name, origin, level, PIRS sheet). Agents pick from these in 'Indicator tracking'."}
            </div>
          </div>
          <div className="page-header-actions">
            {canManage && (
              <button className="btn sm" onClick={() => setImportOpen(true)}>
                <Icon.upload /> {lang === "fr" ? "Importer Excel" : "Import Excel"}
              </button>
            )}
            <button className="btn sm" onClick={() => exportDefinitionsCsv(defs, lang)}>
              <Icon.download /> {lang === "fr" ? "Exporter CSV" : "Export CSV"}
            </button>
            {canManage && (
              <button className="btn sm primary" onClick={() => setEditing("new")}>
                <Icon.plus /> {lang === "fr" ? "Nouvelle définition" : "New definition"}
              </button>
            )}
          </div>
        </div>
        <div className="page-tabs">
          {[
            { k: "all",     l: lang === "fr" ? "Tous" : "All",  n: defs.length },
            { k: "output",  l: "Output",                                 n: defs.filter((d) => d.level === "output").length },
            { k: "outcome", l: "Outcome",                                n: defs.filter((d) => d.level === "outcome").length },
            { k: "mixed",   l: lang === "fr" ? "Mixte" : "Mixed",        n: defs.filter((d) => d.level === "mixed").length },
            { k: "impact",  l: "Impact",                                 n: defs.filter((d) => d.level === "impact").length },
            { k: "context", l: "Context",                                n: defs.filter((d) => d.level === "context").length },
          ].map((tab) => (
            <div key={tab.k} className={"tab" + (filter === tab.k ? " active" : "")} onClick={() => setFilter(tab.k)}
              style={{ cursor: "pointer" }}>
              {tab.l} <span className="tag-mono" style={{ marginLeft: 4 }}>{tab.n}</span>
            </div>
          ))}
        </div>
      </div>

      {/* Bulk-action toolbar — visible whenever ≥1 row is selected.
          Sticky to the top of the scrolling page container so the actions
          stay reachable while the user scrolls through the catalogue to
          tick more rows. z-index 20 sits above .page-header (z-index 4)
          so when both are stuck at top:0 the toolbar covers the header;
          well below modals (9999+). The translucent background + backdrop
          blur softens the transition so the toolbar feels like it's
          floating over the page-header rather than abruptly hiding it. */}
      {selectedIds.size > 0 && (
        <div style={{
          position: "sticky", top: 0, zIndex: 20,
          background: "rgba(238, 242, 255, 0.96)",
          border: "1px solid #c7d2fe", borderRadius: 6,
          padding: "10px 14px", marginTop: 12, marginBottom: 4,
          display: "flex", alignItems: "center", flexWrap: "wrap", gap: 10,
          boxShadow: "0 6px 16px rgba(49, 46, 129, 0.12), 0 2px 4px rgba(49, 46, 129, 0.08)",
          backdropFilter: "saturate(180%) blur(10px)",
          WebkitBackdropFilter: "saturate(180%) blur(10px)",
        }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "#3730a3" }}>
            {selectedIds.size}{" "}
            {lang === "fr"
              ? (selectedIds.size > 1 ? "indicateurs sélectionnés" : "indicateur sélectionné")
              : (selectedIds.size > 1 ? "indicators selected" : "indicator selected")}
          </div>
          <span style={{ color: "#6b7280", fontSize: 12 }}>·</span>
          {/* Sector bulk-apply */}
          <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
            <span style={{ fontSize: 11.5, color: "#374151" }}>
              {lang === "fr" ? "Affecter au secteur :" : "Assign sector:"}
            </span>
            <select value={bulkSectorId} onChange={(e) => setBulkSectorId(e.target.value)}
              disabled={bulkBusy}
              style={{ padding: "5px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "white", color: "var(--text)" }}>
              <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
              <option value="__CLEAR__">{lang === "fr" ? "(retirer le secteur)" : "(clear sector)"}</option>
              {(liveSectors || []).map((s) => (
                <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
              ))}
            </select>
            <button className="btn xs primary" disabled={bulkBusy || !bulkSectorId}
              onClick={() => runBulk(
                { sector_id: bulkSectorId === "__CLEAR__" ? null : bulkSectorId },
                {
                  fr: bulkSectorId === "__CLEAR__"
                    ? "détaché(s) du secteur"
                    : "affecté(s) au secteur",
                  en: bulkSectorId === "__CLEAR__"
                    ? "detached from sector"
                    : "assigned to sector",
                }
              )}>
              {bulkBusy ? "…" : (lang === "fr" ? "Appliquer" : "Apply")}
            </button>
          </div>
          {/* Share / unshare — super-admin only (RLS enforces it server-side too) */}
          {isSuperAdmin && (
            <>
              <span style={{ color: "#6b7280", fontSize: 12 }}>·</span>
              <button className="btn xs" disabled={bulkBusy}
                title={lang === "fr"
                  ? "Marquer les indicateurs sélectionnés comme partagés entre toutes les organisations"
                  : "Mark the selected indicators as shared across all organisations"}
                onClick={() => runBulk(
                  { is_shared: true },
                  { fr: "rendu(s) partagé(s)", en: "made shared" }
                )}>
                🌐 {lang === "fr" ? "Rendre partagés" : "Make shared"}
              </button>
              <button className="btn xs ghost" disabled={bulkBusy}
                title={lang === "fr"
                  ? "Retirer le partage : les indicateurs redeviendront privés à leur organisation"
                  : "Remove sharing: indicators become private to their organisation again"}
                onClick={() => runBulk(
                  { is_shared: false },
                  { fr: "rendu(s) privé(s)", en: "made private" }
                )}>
                🔒 {lang === "fr" ? "Rendre privés" : "Make private"}
              </button>
            </>
          )}
          <div style={{ flex: 1 }} />
          {/* Bulk delete · super-admin only. Always sits at the far right
              of the toolbar, just before "Désélectionner", styled red so
              it stands out among the safer bulk actions. RLS also
              enforces super-admin server-side as a defence in depth. */}
          {isSuperAdmin && (
            <button
              className="btn xs"
              disabled={bulkBusy}
              onClick={() => setBulkDeleteConfirm({ ids: Array.from(selectedIds) })}
              style={{
                background: "#fee2e2",
                color: "#991b1b",
                border: "1px solid #fca5a5",
                fontWeight: 600,
              }}
              title={lang === "fr"
                ? "Supprimer les définitions sélectionnées (action destructive)"
                : "Delete the selected definitions (destructive action)"}>
              🗑 {lang === "fr"
                ? `Supprimer ${selectedIds.size} indicateur${selectedIds.size > 1 ? "s" : ""}`
                : `Delete ${selectedIds.size} indicator${selectedIds.size > 1 ? "s" : ""}`}
            </button>
          )}
          <button className="btn xs ghost" onClick={clearSelection} disabled={bulkBusy}>
            {lang === "fr" ? "Désélectionner" : "Clear"}
          </button>
        </div>
      )}
      {bulkToast && (
        <div style={{
          marginTop: 8, padding: "8px 12px", borderRadius: 6, fontSize: 12.5,
          background: bulkToast.ok ? "#dcfce7" : "#fee2e2",
          color:      bulkToast.ok ? "#166534" : "#991b1b",
        }}>{bulkToast.msg}</div>
      )}

      <div className="card">
        {/* Make THIS card-body the scroll container (instead of letting the
            page scroll), so the sticky table headers actually stick : a
            sticky element latches onto its nearest scrollable ancestor.
            `.card { overflow: hidden }` would otherwise trap the sticky
            against an invisible non-scrolling parent. max-height keeps the
            list within the viewport while preserving the page-level layout
            (toolbar, filters, bulk bar stay where they are). */}
        <div className="card-body flush" style={{
          overflowX: "auto",
          overflowY: "auto",
          maxHeight: "calc(100vh - 240px)",
        }}>
          {loading ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>
          ) : visible.length === 0 ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>
              {lang === "fr"
                ? "Aucune définition pour le moment. Cliquez « Nouvelle définition » ou importez un Excel."
                : "No definition yet. Click 'New definition' or import an Excel file."}
            </div>
          ) : (
            <table className="tbl" style={{ minWidth: "100%" }}>
              <thead>
                <tr>
                  <th style={{ width: 32 }}>
                    <input type="checkbox"
                      checked={allVisibleSelected}
                      ref={(el) => { if (el) el.indeterminate = someVisibleSelected; }}
                      onChange={toggleAllVisible}
                      title={lang === "fr" ? "Tout sélectionner / désélectionner" : "Select / deselect all"}
                      style={{ cursor: "pointer" }} />
                  </th>
                  <th>{lang === "fr" ? "Code" : "Code"}</th>
                  <th>{lang === "fr" ? "Nom (FR)" : "Name (FR)"}</th>
                  <th>{lang === "fr" ? "Nom (EN)" : "Name (EN)"}</th>
                  <th onClick={() => cycleSort("sector")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={lang === "fr" ? "Trier par secteur (clic pour cycler)" : "Sort by sector (click to cycle)"}>
                    {lang === "fr" ? "Secteur" : "Sector"}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "sector" ? 1 : 0.35,
                      color: sort && sort.key === "sector" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("sector")}</span>
                  </th>
                  <th onClick={() => cycleSort("origin")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={lang === "fr" ? "Trier par origine (clic pour cycler)" : "Sort by origin (click to cycle)"}>
                    {lang === "fr" ? "Origine" : "Origin"}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "origin" ? 1 : 0.35,
                      color: sort && sort.key === "origin" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("origin")}</span>
                  </th>
                  <th onClick={() => cycleSort("level")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={lang === "fr" ? "Trier par niveau (clic pour cycler)" : "Sort by level (click to cycle)"}>
                    {lang === "fr" ? "Niveau" : "Level"}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "level" ? 1 : 0.35,
                      color: sort && sort.key === "level" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("level")}</span>
                  </th>
                  <th>{lang === "fr" ? "PIRS" : "PIRS"}</th>
                  {/* Actions column — sticky to the right edge so the
                      "Modifier" + "Supprimer" buttons stay visible even
                      if the table needs horizontal scrolling on narrow
                      screens. min-width prevents the column from being
                      squashed by long Name FR / Name EN columns. */}
                  <th style={{
                    position: "sticky", right: 0, zIndex: 2,
                    minWidth: 140,
                    background: "var(--bg-sunken)",
                    boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                  }}>{lang === "fr" ? "Actions" : "Actions"}</th>
                </tr>
              </thead>
              <tbody>
                {visible.map((d) => {
                  const attCount = attachmentCounts[d.id] || 0;
                  return (
                  <tr key={d.id} style={selectedIds.has(d.id) ? { background: "var(--bg-sunken)" } : null}>
                    <td>
                      <input type="checkbox"
                        checked={selectedIds.has(d.id)}
                        onChange={() => toggleRow(d.id)}
                        style={{ cursor: "pointer" }} />
                    </td>
                    <td className="mono">
                      <div className="row gap-xs" style={{ alignItems: "center" }}>
                        <span>{d.code}</span>
                        {d.is_shared && (
                          <span className="pill" style={{ fontSize: 9.5, background: "#dbeafe", color: "#1e3a8a", padding: "1px 6px" }}
                                title={lang === "fr"
                                  ? "Indicateur partagé · visible par toutes les organisations · modifiable uniquement par un super-administrateur"
                                  : "Shared indicator · visible to every organisation · editable by super-admins only"}>
                            🌐 {lang === "fr" ? "Partagé" : "Shared"}
                          </span>
                        )}
                      </div>
                    </td>
                    <td>{d.name_fr}</td>
                    <td className="text-faint">{d.name_en || "—"}</td>
                    <td>
                      {d.sector_id ? (() => {
                        const s = (typeof sectorById === "function") ? sectorById(d.sector_id) : null;
                        if (!s) return <span className="text-faint">{d.sector_id}</span>;
                        return (
                          <span className="sector-chip"
                            style={{ background: s.bg, color: s.color, borderColor: s.color, fontSize: 10.5 }}>
                            {lang === "en" ? s.en : s.fr}
                          </span>
                        );
                      })() : <span className="text-faint">—</span>}
                    </td>
                    <td className="text-faint">{d.origin_institution || "—"}</td>
                    <td>{d.level
                      ? <span className="pill" style={{ fontSize: 10.5 }}
                              title={d.level === "mixed" ? (lang === "fr" ? "Niveau couvrant Output et Outcome" : "Spans Output and Outcome") : undefined}>
                          {levelLabel(d.level, lang)}
                        </span>
                      : <span className="text-faint">—</span>}</td>
                    <td>
                      {attCount > 0 ? (
                        <button className="pill green dot" style={{ fontSize: 10.5, cursor: "pointer", border: 0 }}
                          onClick={() => setEditing(d.id)}
                          title={lang === "fr" ? "Voir les fichiers PIRS" : "View PIRS files"}>
                          🗎 {attCount}
                        </button>
                      ) : (
                        <span className="text-faint" style={{ fontSize: 11 }}>—</span>
                      )}
                    </td>
                    {/* Actions cell · sticky right (matches the th above).
                        White background so the buttons stay readable
                        when scrolling under the previous columns. */}
                    <td style={{
                      position: "sticky", right: 0,
                      background: selectedIds.has(d.id) ? "var(--bg-sunken)" : "var(--bg-elev, white)",
                      boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                      minWidth: 140,
                    }}>
                      <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                        <button className="btn sm primary" onClick={() => setEditing(d.id)}
                          style={{ fontSize: 11.5, padding: "3px 10px" }}>
                          <Icon.edit /> {lang === "fr" ? "Modifier" : "Edit"}
                        </button>
                        <button className="btn xs ghost" onClick={() => onDelete(d)} disabled={busy === d.id}
                          title={lang === "fr" ? "Supprimer" : "Delete"}>
                          <Icon.x />
                        </button>
                      </div>
                    </td>
                  </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {editing && (
        <DefinitionEditor
          lang={lang}
          existing={editing === "new" ? null : defs.find((d) => d.id === editing)}
          onClose={() => setEditing(null)}
          onSaved={refresh}
          targetOrgId={effectiveOrgId}
          isSuperAdmin={isSuperAdmin}
        />
      )}
      {importOpen && (
        <ImportDefinitionsModal lang={lang} onClose={() => { setImportOpen(false); refresh(); }} />
      )}

      {/* Bulk-delete confirmation modal · super-admin only. Captures the
          count at the moment the user clicked, so changing selection
          later doesn't shift the message. */}
      {bulkDeleteConfirm && (
        <BulkDeleteConfirmModal
          lang={lang}
          count={bulkDeleteConfirm.ids.length}
          onCancel={() => setBulkDeleteConfirm(null)}
          onConfirm={runBulkDelete}
        />
      )}
    </div>
  );
}

// ── Bulk-delete confirmation modal (super-admin only) ───────────────────────
// Reused from the existing Modal component when available, falls back to a
// minimal centred panel if Modal is not registered globally.
function BulkDeleteConfirmModal({ lang, count, onCancel, onConfirm }) {
  const Modal = window.melr && window.melr.Modal;
  const body = (
    <div style={{ padding: "16px 18px" }}>
      <div style={{ fontSize: 14, lineHeight: 1.55, color: "#334155", marginBottom: 14 }}>
        {lang === "fr" ? (
          <>
            Vous êtes sur le point de supprimer <strong style={{ color: "#991b1b" }}>{count} définition{count > 1 ? "s" : ""}</strong> d'indicateurs.
            <br /><br />
            Les indicateurs projet déjà rattachés à ces définitions <strong>resteront en place</strong> — seul le lien vers la définition sera retiré. Les fichiers PIRS associés seront aussi supprimés du stockage.
            <br /><br />
            Cette action est <strong>irréversible</strong>. Confirmer ?
          </>
        ) : (
          <>
            You are about to delete <strong style={{ color: "#991b1b" }}>{count} indicator definition{count > 1 ? "s" : ""}</strong>.
            <br /><br />
            Project indicators already linked to these definitions <strong>will stay</strong> — only the link to the definition is removed. PIRS files attached will also be removed from storage.
            <br /><br />
            This action is <strong>irreversible</strong>. Confirm?
          </>
        )}
      </div>
      <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
        <button className="btn ghost" onClick={onCancel}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button
          className="btn"
          onClick={onConfirm}
          style={{ background: "#dc2626", color: "white", border: "1px solid #b91c1c", fontWeight: 600 }}>
          🗑 {lang === "fr" ? `Supprimer ${count} indicateur${count > 1 ? "s" : ""}` : `Delete ${count} indicator${count > 1 ? "s" : ""}`}
        </button>
      </div>
    </div>
  );
  // Modal component path — same convention as the rest of the app.
  if (Modal) {
    return (
      <Modal
        open
        onClose={onCancel}
        title={lang === "fr" ? "Confirmer la suppression" : "Confirm deletion"}
        width={520}>
        {body}
      </Modal>
    );
  }
  // Fallback if Modal isn't loaded yet (shouldn't happen in practice).
  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div style={{ background: "white", borderRadius: 10, width: 520, maxWidth: "100%", boxShadow: "0 12px 32px rgba(0,0,0,.25)" }}>
        <div style={{ padding: "12px 18px", borderBottom: "1px solid #e5e7eb", fontWeight: 600 }}>
          {lang === "fr" ? "Confirmer la suppression" : "Confirm deletion"}
        </div>
        {body}
      </div>
    </div>
  );
}

// ── PIRS link: signs a fresh URL on click, opens in a new tab ───────────────
function PirsLink({ path, filename, lang }) {
  const [busy, setBusy] = useStateID(false);
  const onOpen = async (e) => {
    e.preventDefault();
    if (busy) return;
    setBusy(true);
    try {
      const url = await window.melr.getIndicatorPirsUrl(path, 3600);
      if (url) window.open(url, "_blank", "noopener,noreferrer");
    } catch (err) {
      window.alert((lang === "fr" ? "Erreur : " : "Error: ") + err.message);
    } finally { setBusy(false); }
  };
  return (
    <a href="#" onClick={onOpen} className="row" style={{ gap: 4, fontSize: 12, alignItems: "center", color: "var(--accent, #2563eb)" }}>
      <Icon.fileText className="sm" />
      <span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{filename}</span>
      {busy && <span className="text-faint">…</span>}
    </a>
  );
}

// ── Definition editor (create + edit) ───────────────────────────────────────
function DefinitionEditor({ lang, existing, onClose, onSaved, targetOrgId, isSuperAdmin }) {
  const isNew = !existing;
  // Once an existing-id is in hand (either we opened an existing one OR we
  // just created one), the attachments section becomes available. We need
  // a stateful currentId so the attachments stay visible after a fresh save.
  const [currentId, setCurrentId] = useStateID(existing ? existing.id : null);
  const [code, setCode]         = useStateID(existing ? existing.code : "");
  const [nameFr, setNameFr]     = useStateID(existing ? existing.name_fr : "");
  const [nameEn, setNameEn]     = useStateID(existing ? (existing.name_en || "") : "");
  const [origin, setOrigin]     = useStateID(existing ? (existing.origin_institution || "") : "");
  const [level, setLevel]       = useStateID(existing ? (existing.level || "") : "");
  const [valueKind, setValueKind] = useStateID(existing ? (existing.value_kind || "numeric") : "numeric");
  // PEFA components — array of {code, name_fr, name_en}. Only meaningful
  // when valueKind === 'score_pefa'. Stored in indicator_definitions.components
  // (jsonb). Auto-suggestion of codes (PI-X.1, PI-X.2…) based on the parent
  // code is computed at render time so renaming the code stays consistent.
  const [components, setComponents] = useStateID(() => {
    if (existing && Array.isArray(existing.components)) return existing.components;
    return [];
  });
  // Shared flag: only a super-admin can flip this (RLS enforces it server-
  // side too). Rendered as a checkbox visible to super-admins only.
  const [isShared, setIsShared] = useStateID(existing ? !!existing.is_shared : false);
  // Sector link — optional. Sourced from the DB-backed sectors catalogue
  // (useSectors / sectorsCrud). "+ Nouveau secteur" opens the inline modal.
  const [sectorId, setSectorId] = useStateID(existing ? (existing.sector_id || "") : "");
  const { data: liveSectors }   = window.melr.useSectors();
  const [newSectorOpen, setNewSectorOpen] = useStateID(false);
  const [busy, setBusy]         = useStateID(false);
  const [err, setErr]           = useStateID(null);

  const onSectorChange = (val) => {
    if (val === "__NEW__") setNewSectorOpen(true);
    else                   setSectorId(val);
  };

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!code.trim()) { setErr(lang === "fr" ? "Code requis." : "Code required."); return; }
    if (!nameFr.trim()) { setErr(lang === "fr" ? "Nom (FR) requis." : "Name (FR) required."); return; }
    setBusy(true); setErr(null);
    try {
      const payload = {
        code:               code.trim(),
        name_fr:            nameFr.trim(),
        name_en:            nameEn.trim() || null,
        origin_institution: origin.trim() || null,
        level:              level || null,
        value_kind:         valueKind || "numeric",
        sector_id:          sectorId || null,
        // Only persist components for PEFA indicators — clear the array if
        // the type was switched back to numeric so we don't leave orphan
        // sub-components hanging around in the DB.
        components:         valueKind === "score_pefa"
          ? components.filter((c) => c && c.code && c.code.trim())
          : [],
      };
      // Only forward is_shared when the caller is a super-admin. Server-
      // side RLS would reject the flag anyway, but masking it client-side
      // keeps the payload clean for normal admins.
      if (isSuperAdmin) payload.is_shared = !!isShared;
      let saved;
      if (!currentId) {
        // Super-admin acting-as-org: route new PRIVATE definitions to the
        // target org. SHARED definitions don't need a target org (visible
        // to all) but we still record the creator's org for attribution.
        if (targetOrgId) payload.organization_id = targetOrgId;
        saved = await window.melr.indicatorDefinitionsCrud.create(payload);
      } else            saved = await window.melr.indicatorDefinitionsCrud.update(currentId, payload);
      // Remember the id so the user can stay in this modal and add PIRS
      // attachments without closing and reopening.
      setCurrentId(saved.id);
      if (onSaved) await onSaved();
      // Don't close — let the user add PIRS attachments after the row exists.
      // The "Fermer" button (modal-foot) closes the modal explicitly.
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };

  const inp = { width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  const lbl = { display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" };

  return (
    <Modal
      size="lg"
      title={!currentId
        ? (lang === "fr" ? "Nouvelle définition" : "New definition")
        : (lang === "fr" ? "Modifier : " + code : "Edit: " + code)}
      onClose={onClose}
      onSubmit={onSubmit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Fermer" : "Close"}
        </button>
        <button type="submit" className="btn sm primary" disabled={busy}>
          {busy ? "…" : (!currentId ? (lang === "fr" ? "Créer" : "Create") : (lang === "fr" ? "Enregistrer" : "Save"))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>{lang === "fr" ? "Code *" : "Code *"}</label>
              <input style={inp} value={code} onChange={(e) => setCode(e.target.value)}
                placeholder="REFT-CPN-01" maxLength={64} required />
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Niveau" : "Level"}</label>
              <select style={inp} value={level} onChange={(e) => setLevel(e.target.value)}>
                <option value="">{lang === "fr" ? "— Non renseigné —" : "— Not set —"}</option>
                {LEVELS.map((l) => (
                  <option key={l.v} value={l.v}>{lang === "fr" ? l.fr : l.en}</option>
                ))}
              </select>
            </div>
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Nom (FR) *" : "Name (FR) *"}</label>
            <input style={inp} value={nameFr} onChange={(e) => setNameFr(e.target.value)}
              placeholder={lang === "fr" ? "Couverture vaccinale Penta-3" : "Penta-3 immunization coverage"} required />
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Nom (EN)" : "Name (EN)"}</label>
            <input style={inp} value={nameEn} onChange={(e) => setNameEn(e.target.value)}
              placeholder="Penta-3 immunization coverage" />
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Secteur" : "Sector"}</label>
            <select style={inp} value={sectorId} onChange={(e) => onSectorChange(e.target.value)}>
              <option value="">{lang === "fr" ? "— Aucun secteur —" : "— No sector —"}</option>
              {(liveSectors || []).map((s) => (
                <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
              ))}
              <option value="__NEW__">{lang === "fr" ? "+ Nouveau secteur…" : "+ New sector…"}</option>
            </select>
            <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
              {lang === "fr"
                ? "Associer l'indicateur à un secteur facilite le filtrage et le regroupement dans les tableaux de bord."
                : "Linking the indicator to a sector helps filtering and grouping in dashboards."}
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>{lang === "fr" ? "Origine (Institution)" : "Origin (Institution)"}</label>
              <input style={inp} value={origin} onChange={(e) => setOrigin(e.target.value)}
                placeholder={lang === "fr" ? "REFT Africa · OMS · USAID · …" : "REFT Africa · WHO · USAID · …"} />
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Type de mesure" : "Measurement type"}</label>
              <select style={inp} value={valueKind} onChange={(e) => setValueKind(e.target.value)}>
                <option value="numeric">{lang === "fr" ? "Numérique (chiffres bruts)" : "Numeric (raw figures)"}</option>
                <option value="score_pefa">{lang === "fr" ? "Score PEFA (A · B+ · B · C+ · C · D+ · D)" : "PEFA score (A · B+ · B · C+ · C · D+ · D)"}</option>
              </select>
              <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
                {valueKind === "score_pefa"
                  ? (lang === "fr"
                      ? "Conversion auto : A=4, B+=3.5, B=3, C+=2.5, C=2, D+=1.5, D=1, NR=non noté"
                      : "Auto-conversion: A=4, B+=3.5, B=3, C+=2.5, C=2, D+=1.5, D=1, NR=not rated")
                  : (lang === "fr"
                      ? "Saisie numérique standard (baseline, cible, valeurs trimestrielles…)"
                      : "Standard numeric capture (baseline, target, periodic values…)")}
              </div>
            </div>
          </div>

          {/* PEFA components — visible only for score_pefa indicators.
              An indicator like PI-27 may have 1 to 4 sub-components (PI-27.1
              … PI-27.4), each scored separately A/B+/B/…/D/NR. The final
              indicator score stays manually entered (NO auto-averaging —
              PEFA framework is qualitative). */}
          {valueKind === "score_pefa" && (
            <PefaComponentsSection
              lang={lang}
              parentCode={code}
              components={components}
              setComponents={setComponents} />
          )}

          {/* Shared-catalogue flag — super-admin only */}
          {isSuperAdmin ? (
            <div style={{ padding: "10px 12px", background: "var(--bg-sunken)", borderRadius: 6, border: "1px solid var(--line)" }}>
              <label style={{ display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer" }}>
                <input type="checkbox" checked={isShared} onChange={(e) => setIsShared(e.target.checked)}
                  style={{ marginTop: 3 }} />
                <div>
                  <div style={{ fontSize: 13, fontWeight: 600 }}>
                    🌐 {lang === "fr"
                      ? "Indicateur partagé entre toutes les organisations"
                      : "Shared across all organisations"}
                  </div>
                  <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>
                    {lang === "fr"
                      ? "Cocher pour les indicateurs standards (CNUCED, ODD, PEFA, REFT…) qui doivent apparaître dans le catalogue de toutes les organisations. Seul un super-administrateur peut créer ou modifier ces indicateurs."
                      : "Tick for standard indicators (UNCTAD, SDG, PEFA, REFT…) that should appear in every organisation's catalogue. Only super-administrators can create or edit them."}
                  </div>
                </div>
              </label>
            </div>
          ) : existing && existing.is_shared && (
            <div style={{ padding: "8px 10px", background: "#dbeafe", color: "#1e3a8a", borderRadius: 6, fontSize: 12 }}>
              🌐 {lang === "fr"
                ? "Cet indicateur est partagé entre toutes les organisations. Seul un super-administrateur peut le modifier."
                : "This indicator is shared across all organisations. Only a super-administrator can edit it."}
            </div>
          )}

          {err && (
            <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>
              {err}
            </div>
          )}

          {/* PIRS attachments — visible once the definition exists */}
          <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
            <div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>
                {lang === "fr" ? "Fiches PIRS (multilingues)" : "PIRS sheets (multilingual)"}
              </div>
              <div style={{ flex: 1 }} />
              {!currentId && (
                <span className="text-faint" style={{ fontSize: 11 }}>
                  {lang === "fr" ? "Enregistrez d'abord la définition pour pouvoir attacher des fichiers." : "Save the definition first to attach files."}
                </span>
              )}
            </div>
            {currentId ? (
              <PirsAttachmentsSection definitionId={currentId} lang={lang} />
            ) : (
              <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
                {lang === "fr"
                  ? "Pas encore de fichiers — cliquer « Créer » d'abord, puis attacher les fiches PIRS dans la langue de votre choix."
                  : "No files yet — click 'Create' first, then attach PIRS sheets in any language."}
              </div>
            )}
          </div>

          {/* Désagrégation requise — Phase C-1
              Le super-admin et l'admin org cochent les axes selon lesquels
              cet indicateur devra être désagrégé à la saisie. La saisie de
              la grille (Phase D) lit cette config pour générer toutes les
              combinaisons de cellules à remplir. */}
          <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
            <div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>
                {lang === "fr" ? "Désagrégation requise" : "Required disaggregation"}
              </div>
              <div style={{ flex: 1 }} />
              {!currentId && (
                <span className="text-faint" style={{ fontSize: 11 }}>
                  {lang === "fr" ? "Enregistrez d'abord la définition pour configurer les axes." : "Save the definition first to configure axes."}
                </span>
              )}
            </div>
            {currentId ? (
              <DisaggregationAxesSection definitionId={currentId} lang={lang} />
            ) : (
              <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
                {lang === "fr"
                  ? "Pas encore d'axes — cliquer « Créer » d'abord, puis cocher les axes selon lesquels cet indicateur sera désagrégé."
                  : "No axes yet — click 'Create' first, then tick the axes this indicator should be disaggregated along."}
              </div>
            )}
          </div>
        </div>
      {newSectorOpen && (
        <NewSectorModal lang={lang}
          onClose={() => setNewSectorOpen(false)}
          onCreated={(sec) => { setSectorId(sec.id); setNewSectorOpen(false); }} />
      )}
    </Modal>
  );
}

// ── Multi-PIRS attachments section ──────────────────────────────────────────
// Lists current attachments and offers an inline "+ Ajouter PIRS" form with
// language selector, optional label, and a file picker. Each row can be
// opened (signed URL) or removed.
function PirsAttachmentsSection({ definitionId, lang }) {
  const { data: attachments, loading, refresh } = window.melr.useDefinitionAttachments(definitionId);
  const [busy, setBusy]   = useStateID(null);
  const [err, setErr]     = useStateID(null);

  // Inline-add form state
  const [addLang, setAddLang]   = useStateID("");      // "" = neutral
  const [addLabel, setAddLabel] = useStateID("");
  const addFileRef = useRefID(null);
  const [uploadBusy, setUploadBusy] = useStateID(false);

  const LANGS = [
    { v: "",   l: lang === "fr" ? "— Langue neutre —" : "— Language-neutral —" },
    { v: "fr", l: "Français (FR)" },
    { v: "en", l: "English (EN)" },
    { v: "es", l: "Español (ES)" },
    { v: "pt", l: "Português (PT)" },
    { v: "ar", l: "العربية (AR)" },
  ];

  const onAdd = async (e) => {
    e.preventDefault();
    const f = addFileRef.current && addFileRef.current.files && addFileRef.current.files[0];
    if (!f) { setErr(lang === "fr" ? "Choisir un fichier." : "Pick a file."); return; }
    if (!/\.(pdf|docx?|xlsx?|ods|odt)$/i.test(f.name)) {
      setErr(lang === "fr"
        ? "Formats acceptés : PDF, Word (.doc, .docx), Excel (.xls, .xlsx)."
        : "Accepted formats: PDF, Word (.doc, .docx), Excel (.xls, .xlsx).");
      return;
    }
    if (f.size > 10 * 1024 * 1024) {
      setErr(lang === "fr" ? "Taille max 10 Mo." : "Max size 10 MB.");
      return;
    }
    setErr(null); setUploadBusy(true);
    try {
      await window.melr.addDefinitionAttachment(definitionId, f, {
        language: addLang || null,
        label:    addLabel.trim() || null,
      });
      setAddLabel("");
      setAddLang("");
      if (addFileRef.current) addFileRef.current.value = "";
      await refresh();
    } catch (e2) { setErr(e2.message); }
    finally { setUploadBusy(false); }
  };

  const onRemove = async (a) => {
    if (!window.confirm(lang === "fr"
      ? "Retirer le fichier « " + (a.filename || a.path) + " » ?"
      : "Remove file '" + (a.filename || a.path) + "'?")) return;
    setBusy(a.id);
    try { await window.melr.removeDefinitionAttachment(a.id); await refresh(); }
    catch (e2) { setErr(e2.message); }
    finally { setBusy(null); }
  };

  const inp = { padding: "6px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  const langLabel = (code) => {
    const m = LANGS.find((l) => l.v === code);
    return m ? m.l : (code || "—");
  };

  return (
    <div>
      {loading ? (
        <div className="text-faint" style={{ padding: 8, fontSize: 12 }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>
      ) : attachments.length === 0 ? (
        <div className="text-faint" style={{ padding: 8, fontSize: 12, fontStyle: "italic" }}>
          {lang === "fr" ? "Aucun fichier pour le moment." : "No file yet."}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 4, marginBottom: 10 }}>
          {attachments.map((a) => (
            <div key={a.id} style={{
              display: "grid", gridTemplateColumns: "90px 1fr auto auto", gap: 8, alignItems: "center",
              padding: "6px 10px", border: "1px solid var(--line)", borderRadius: 5, background: "var(--bg-elev, white)",
            }}>
              <span className="pill" style={{ fontSize: 10, padding: "2px 6px" }}>
                {a.language ? a.language.toUpperCase() : (lang === "fr" ? "neutre" : "neutral")}
              </span>
              <div style={{ minWidth: 0 }}>
                <PirsLink path={a.path} filename={a.filename || "PIRS"} lang={lang} />
                {a.label && (
                  <div className="text-faint" style={{ fontSize: 10.5, marginTop: 2 }}>{a.label}</div>
                )}
              </div>
              <span className="text-faint mono" style={{ fontSize: 10.5 }}>
                {a.size_bytes ? Math.max(1, Math.round(a.size_bytes / 1024)) + " KB" : ""}
              </span>
              <button type="button" className="btn xs ghost" onClick={() => onRemove(a)} disabled={busy === a.id}
                title={lang === "fr" ? "Retirer" : "Remove"}>
                <Icon.x />
              </button>
            </div>
          ))}
        </div>
      )}

      {/* Inline "+ Add" subform */}
      <div style={{ padding: 10, background: "var(--bg-sunken, #f9fafb)", borderRadius: 6 }}>
        <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: 6 }}>
          {lang === "fr" ? "Ajouter une fiche PIRS" : "Add a PIRS file"}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "140px 1fr auto", gap: 8, alignItems: "center" }}>
          <select value={addLang} onChange={(e) => setAddLang(e.target.value)} style={inp}>
            {LANGS.map((l) => <option key={l.v || "neutral"} value={l.v}>{l.l}</option>)}
          </select>
          <input value={addLabel} onChange={(e) => setAddLabel(e.target.value)}
            placeholder={lang === "fr" ? "Libellé (optionnel) — ex: PIRS v2" : "Label (optional) — e.g. PIRS v2"}
            style={inp} maxLength={120} />
          <span></span>
          <span style={{ gridColumn: "1 / -1" }}>
            <input ref={addFileRef} type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ods,.odt"
              style={{ padding: 4, fontSize: 12, width: "100%" }} />
          </span>
        </div>
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 6 }}>
          {lang === "fr" ? "PDF, Word ou Excel · 10 Mo max." : "PDF, Word or Excel · 10 MB max."}
        </div>
        {err && (
          <div style={{ padding: "6px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 5, fontSize: 12, marginTop: 8 }}>
            {err}
          </div>
        )}
        <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 8 }}>
          <button type="button" className="btn sm" onClick={onAdd} disabled={uploadBusy}>
            <Icon.upload /> {uploadBusy ? "…" : (lang === "fr" ? "Ajouter" : "Add")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── PEFA components section (E1) ──────────────────────────────────────────
// Only renders when value_kind = 'score_pefa'. A PEFA indicator like PI-27
// has 1 to 4 sub-components (PI-27.1 … PI-27.4), each scored separately
// A/B+/B/C+/C/D+/D/NR. This section lets the user declare those components
// with auto-suggested codes (based on parent code) and bilingual names.
//
// The final indicator score (the parent's overall rating) stays MANUALLY
// entered — there is no auto-averaging because the PEFA framework is
// qualitative and component-to-parent rules are codified in the PEFA
// handbook, not a simple arithmetic mean.
function PefaComponentsSection({ lang, parentCode, components, setComponents }) {
  // Cap at 4 — PEFA framework rarely has more. Adjust if real-world data
  // needs change.
  const MAX_COMPONENTS = 4;
  // Sanitise parentCode for the auto-suggested child codes. Strip any
  // trailing ".N" so PI-27.1 doesn't become PI-27.1.1 when the user edits.
  const baseCode = (parentCode || "").replace(/\.[0-9]+$/, "").trim();

  const addComponent = () => {
    if (components.length >= MAX_COMPONENTS) return;
    const nextIdx = components.length + 1;
    const suggested = baseCode ? `${baseCode}.${nextIdx}` : "";
    setComponents([...components, { code: suggested, name_fr: "", name_en: "" }]);
  };
  const removeComponent = (i) => {
    setComponents(components.filter((_, idx) => idx !== i));
  };
  const updateComponent = (i, patch) => {
    setComponents(components.map((c, idx) => idx === i ? { ...c, ...patch } : c));
  };
  // Move a component up/down to reorder (useful when the original PEFA
  // handbook order matters but the user added them out of sequence).
  const moveComponent = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= components.length) return;
    const next = [...components];
    [next[i], next[j]] = [next[j], next[i]];
    setComponents(next);
  };

  const inp = { width: "100%", padding: "6px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12.5, background: "var(--input-bg, var(--bg, white))", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };

  return (
    <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
        <div style={{ fontSize: 13, fontWeight: 600 }}>
          {lang === "fr" ? "Composantes PEFA" : "PEFA components"}
        </div>
        <span className="pill" style={{ fontSize: 10, padding: "1px 7px" }}>
          {components.length} / {MAX_COMPONENTS}
        </span>
        <div style={{ flex: 1 }} />
        <button type="button" className="btn xs"
          onClick={addComponent}
          disabled={components.length >= MAX_COMPONENTS}>
          + {lang === "fr" ? "Ajouter" : "Add"}
        </button>
      </div>
      <div className="text-faint" style={{ fontSize: 11, marginBottom: 8 }}>
        {lang === "fr"
          ? "Un indicateur PEFA comporte 1 à 4 composantes notées séparément. Codification suggérée : "
          : "A PEFA indicator has 1 to 4 components scored separately. Suggested code: "}
        <code style={{ fontSize: 10.5 }}>
          {baseCode ? `${baseCode}.1, ${baseCode}.2…` : (lang === "fr" ? "(saisir d'abord le code parent)" : "(enter parent code first)")}
        </code>
        . {lang === "fr"
          ? "La note finale de l'indicateur reste saisie manuellement."
          : "The final indicator grade stays manually entered."}
      </div>

      {components.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
          {lang === "fr"
            ? "Aucune composante. Cliquer « + Ajouter » pour en créer une."
            : "No components. Click '+ Add' to create one."}
        </div>
      ) : (
        <div style={{ display: "grid", gap: 6 }}>
          {components.map((c, i) => (
            <div key={i} style={{
              display: "grid",
              gridTemplateColumns: "auto 110px 1fr 1fr auto",
              gap: 6, alignItems: "center",
              padding: "6px 8px",
              background: "var(--bg-sunken)",
              border: "1px solid var(--line)",
              borderRadius: 6,
            }}>
              <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
                <button type="button" className="btn xs ghost"
                  onClick={() => moveComponent(i, -1)} disabled={i === 0}
                  title={lang === "fr" ? "Monter" : "Move up"}
                  style={{ padding: "1px 5px", fontSize: 10, lineHeight: 1 }}>↑</button>
                <button type="button" className="btn xs ghost"
                  onClick={() => moveComponent(i, +1)} disabled={i === components.length - 1}
                  title={lang === "fr" ? "Descendre" : "Move down"}
                  style={{ padding: "1px 5px", fontSize: 10, lineHeight: 1 }}>↓</button>
              </div>
              <input
                style={{ ...inp, fontFamily: "var(--font-mono)", fontSize: 11.5 }}
                value={c.code || ""}
                onChange={(e) => updateComponent(i, { code: e.target.value })}
                placeholder={baseCode ? `${baseCode}.${i + 1}` : "PI-XX.N"} />
              <input
                style={inp}
                value={c.name_fr || ""}
                onChange={(e) => updateComponent(i, { name_fr: e.target.value })}
                placeholder={lang === "fr" ? "Nom de la composante (FR)" : "Component name (FR)"} />
              <input
                style={inp}
                value={c.name_en || ""}
                onChange={(e) => updateComponent(i, { name_en: e.target.value })}
                placeholder="Component name (EN)" />
              <button type="button" className="btn xs ghost danger"
                onClick={() => removeComponent(i)}
                title={lang === "fr" ? "Retirer" : "Remove"}
                style={{ padding: "2px 7px" }}>
                ✕
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Disaggregation axes section (Phase C-1) ───────────────────────────────
// Lists all axes visible to the user (shared globals + this org's local axes)
// and lets the admin tick a checkbox per axis. Checked = this indicator will
// be disaggregated along that axis. The saisie grid (Phase D) reads this
// config to generate the cells to fill.
//
// For each checked axis we display a preview of its values (chips with the
// short label, full description on hover) so the admin can verify they
// match the indicator's reporting requirements before committing.
//
// Save is autosaved per-toggle via definitionDisaggregationsCrud (no
// separate Save button needed — feels lighter and matches the PirsAttachments
// section pattern that also saves inline).
function DisaggregationAxesSection({ definitionId, lang }) {
  const { data: axes,    loading: axesLoading }    = window.melr.useDisaggregationAxes();
  const { data: assigned, loading: assignedLoading, refresh } = window.melr.useDefinitionDisaggregations(definitionId);
  const [busy, setBusy] = useStateID(null);       // axis_id being toggled
  const [err, setErr]   = useStateID(null);

  // Build a Set of axis_id currently assigned to this definition for O(1) lookup
  const assignedIds = React.useMemo(() => {
    const s = new Set();
    (assigned || []).forEach((row) => s.add(row.axis_id));
    return s;
  }, [assigned]);

  const toggleAxis = async (axisId, currentlyOn) => {
    setBusy(axisId);
    setErr(null);
    try {
      if (currentlyOn) {
        await window.melr.definitionDisaggregationsCrud.removeAxis(definitionId, axisId);
      } else {
        // Append at the end of the display order
        const nextOrder = (assigned || []).reduce((m, r) => Math.max(m, r.display_order || 0), -1) + 1;
        await window.melr.definitionDisaggregationsCrud.addAxis(definitionId, axisId, { is_required: true, display_order: nextOrder });
      }
      await refresh();
    } catch (e) { setErr(e.message); }
    finally { setBusy(null); }
  };

  if (axesLoading || assignedLoading) {
    return <div className="text-faint" style={{ fontSize: 11.5, padding: 8 }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>;
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      <div style={{ fontSize: 11, color: "var(--text-muted)" }}>
        {lang === "fr"
          ? "Cocher les axes selon lesquels cet indicateur sera désagrégé à la saisie. Chaque combinaison génère une cellule N/D à remplir."
          : "Tick the axes this indicator will be disaggregated along at data entry. Each combination generates one N/D cell to fill."}
      </div>

      {axes.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
          {lang === "fr"
            ? "Aucun axe disponible. Les axes globaux (sex, age, population, entité) seront seedés par la migration."
            : "No axis available. The global axes (sex, age, population, entity) are seeded by the migration."}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {axes.map((a) => {
            const on = assignedIds.has(a.id);
            const isBusy = busy === a.id;
            return (
              <DisaggregationAxisRow
                key={a.id}
                axis={a}
                on={on}
                busy={isBusy}
                lang={lang}
                onToggle={() => toggleAxis(a.id, on)}
              />
            );
          })}
        </div>
      )}

      {err && (
        <div style={{ padding: "6px 10px", borderRadius: 5, fontSize: 11.5, background: "#fee2e2", color: "#991b1b" }}>
          {err}
        </div>
      )}
    </div>
  );
}

// One row in the axes panel = one axis with checkbox + label + values preview
function DisaggregationAxisRow({ axis, on, busy, lang, onToggle }) {
  // Lazy-load values when the axis is expanded (saves DB queries on first paint).
  // ⚠️ React Rules of Hooks: useAxisValues MUST be called unconditionally on
  // every render. We toggle its argument to null when not expanded — the hook
  // returns an empty list in that case (it null-checks at the top).
  const [expanded, setExpanded] = useStateID(on);   // start expanded if on
  const { data: values, loading } = window.melr.useAxisValues(expanded ? axis.id : null);

  // Whenever the toggle flips ON, auto-expand to show the values
  React.useEffect(() => { if (on) setExpanded(true); }, [on]);

  const label = lang === "fr" ? axis.name_fr : (axis.name_en || axis.name_fr);
  const desc  = axis.description;
  const isShared = !!axis.is_shared;

  return (
    <div style={{
      border: "1px solid " + (on ? "var(--accent-line, #c7d2fe)" : "var(--line)"),
      borderRadius: 6,
      padding: "8px 12px",
      background: on ? "var(--accent-soft, #eef2ff)" : "var(--bg, white)",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <input
          type="checkbox"
          checked={on}
          disabled={busy}
          onChange={onToggle}
          style={{ width: 16, height: 16, cursor: busy ? "wait" : "pointer", accentColor: "var(--accent, #6366f1)" }}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
            <span style={{ fontSize: 12.5, fontWeight: 500 }}>{label}</span>
            <span style={{ fontFamily: "monospace", fontSize: 10, color: "var(--text-faint)" }}>· {axis.code}</span>
            {isShared && (
              <span style={{ fontSize: 9.5, padding: "1px 6px", borderRadius: 3, background: "oklch(0.95 0.04 195)", color: "oklch(0.40 0.10 195)", fontWeight: 600 }}>
                {lang === "fr" ? "PARTAGÉ" : "SHARED"}
              </span>
            )}
            <span style={{ fontSize: 10, color: "var(--text-faint)" }}>· {axis.kind === "value-list" ? (lang === "fr" ? "liste de valeurs" : "value list") : (lang === "fr" ? "référence entité" : "entity reference")}</span>
          </div>
          {desc && <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 2 }}>{desc}</div>}
        </div>
        <button
          type="button"
          className="btn xs ghost"
          onClick={() => setExpanded((e) => !e)}
          disabled={busy}>
          {expanded ? (lang === "fr" ? "Masquer" : "Hide") : (lang === "fr" ? "Aperçu" : "Preview")}
        </button>
      </div>

      {expanded && (
        <div style={{ marginTop: 8, paddingTop: 8, borderTop: "1px dashed var(--line)" }}>
          {axis.kind === "entity-ref" ? (
            <div style={{ fontSize: 11, color: "var(--text-muted)", fontStyle: "italic" }}>
              {lang === "fr"
                ? "Cet axe pointe vers la liste « Entités » (ONG partenaires, communautés, districts) — à gérer dans le drawer dédié."
                : "This axis points to the 'Entities' list (partner NGOs, communities, districts) — managed in the dedicated drawer."}
            </div>
          ) : loading ? (
            <div className="text-faint" style={{ fontSize: 11 }}>{lang === "fr" ? "Chargement des valeurs…" : "Loading values…"}</div>
          ) : (values && values.length > 0) ? (
            <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
              {values.map((v) => {
                const lbl = lang === "fr" ? v.label_fr : (v.label_en || v.label_fr);
                const ttl = lang === "fr" ? (v.description_fr || v.label_fr) : (v.description_en || v.label_en || v.label_fr);
                const isOrgLocal = !!v.organization_id;
                return (
                  <span key={v.id}
                    title={ttl + (isOrgLocal ? (lang === "fr" ? " (ajouté par votre org)" : " (added by your org)") : "")}
                    style={{
                      display: "inline-flex", alignItems: "center", gap: 4,
                      fontSize: 11, fontWeight: 500,
                      padding: "2px 8px", borderRadius: 999,
                      background: isOrgLocal ? "oklch(0.96 0.04 75)" : "var(--bg-sunken, #f3f4f6)",
                      color:      isOrgLocal ? "oklch(0.42 0.10 75)" : "var(--text)",
                      border: "1px solid " + (isOrgLocal ? "oklch(0.82 0.10 75)" : "var(--line)"),
                    }}>
                    {lbl}
                    {isOrgLocal && <span style={{ fontSize: 9 }}>★</span>}
                  </span>
                );
              })}
            </div>
          ) : (
            <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic" }}>
              {lang === "fr" ? "Aucune valeur définie pour cet axe." : "No values defined for this axis."}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Excel import modal ──────────────────────────────────────────────────────
function ImportDefinitionsModal({ lang, onClose }) {
  const [file, setFile]   = useStateID(null);
  const [busy, setBusy]   = useStateID(false);
  const [result, setResult] = useStateID(null);
  const [err, setErr]     = useStateID(null);

  const onRun = async () => {
    if (!file) { setErr(lang === "fr" ? "Choisir un fichier Excel." : "Pick an Excel file."); return; }
    setBusy(true); setErr(null); setResult(null);
    try {
      const r = await window.melr.importIndicatorDefinitionsFromExcel(file);
      setResult(r);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  return (
    <Modal
      title={lang === "fr" ? "Importer un Excel d'indicateurs" : "Import indicators from Excel"}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {result ? (lang === "fr" ? "Fermer" : "Close") : (lang === "fr" ? "Annuler" : "Cancel")}
        </button>
        <button className="btn sm primary" onClick={onRun} disabled={busy || !file}>
          {busy ? "…" : (lang === "fr" ? "Importer" : "Import")}
        </button>
      </>}>
      <div>
        <div className="text-faint" style={{ fontSize: 12, marginBottom: 12 }}>
            {lang === "fr"
              ? "Colonnes attendues (insensibles à la casse, séparateurs souples) :"
              : "Expected columns (case-insensitive, flexible separators):"}
          </div>
          <pre style={{ background: "var(--bg-sunken, #f3f4f6)", padding: 10, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5 }}>
{`Code | Nom (FR) | Nom (EN) | Secteur | Origine | Niveau
REFT-001 | Couverture vaccinale | Vaccination coverage | Santé | REFT Africa | outcome
USAID-EG.3.1-1 | Foyers raccordés | Connected households | Énergie | USAID | output
PEFA-PI-1 | Dépenses réelles | Actual expenditure | Finances publiques | PEFA | outcome
…`}
          </pre>
          <div className="text-faint" style={{ fontSize: 11.5, marginBottom: 12 }}>
            {lang === "fr"
              ? "• Les codes en doublon sont mis à jour (upsert). Les lignes sans Code ou sans Nom (FR) sont ignorées."
              : "• Duplicate codes are upserted. Rows without Code or Name (FR) are skipped."}
          </div>
          <div className="text-faint" style={{ fontSize: 11.5, marginBottom: 12 }}>
            {lang === "fr"
              ? "• Colonne Secteur (optionnelle) : nom FR (« Santé »), nom EN (« Health »), ou identifiant (« sante »). Un secteur inconnu est signalé en erreur et la ligne est importée sans secteur — créez-le d'abord depuis « Nouveau projet »."
              : "• Sector column (optional): FR name ('Health'), EN name, or slug ('sante'). Unknown sectors are flagged and the row is imported without a sector — create the sector first from 'New project'."}
          </div>
          <input type="file" accept=".xlsx,.xls" onChange={(e) => setFile(e.target.files && e.target.files[0])}
            style={{ padding: 6, fontSize: 13, width: "100%" }} />

          {err && <div style={{ marginTop: 12, padding: "8px 12px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>}
          {result && (
            <div style={{ marginTop: 12, padding: "10px 12px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>
              {lang === "fr"
                ? "✓ " + result.inserted + " ajouté(s) · " + result.updated + " mis à jour · " + result.skipped + " ignoré(s)"
                : "✓ " + result.inserted + " added · " + result.updated + " updated · " + result.skipped + " skipped"}
              {result.errors && result.errors.length > 0 && (
                <details style={{ marginTop: 6, fontSize: 11 }}>
                  <summary>{result.errors.length} {lang === "fr" ? "erreur(s)" : "error(s)"}</summary>
                  <ul style={{ margin: 0, paddingLeft: 18 }}>
                    {result.errors.slice(0, 10).map((er, i) => (
                      <li key={i}>{lang === "fr" ? "Ligne " : "Row "}{er.row}{er.code ? " (" + er.code + ")" : ""}: {er.message}</li>
                    ))}
                  </ul>
                </details>
              )}
            </div>
          )}
      </div>
    </Modal>
  );
}

// CSV export of the catalogue (for round-tripping with Excel imports).
// The "Secteur" column is exported by FR NAME (not the slug) so the file
// stays human-friendly. The importer accepts slug / FR / EN equivalently,
// so a round-trip works either way.
function exportDefinitionsCsv(rows, lang) {
  const header = ["Code", "Nom (FR)", "Nom (EN)", "Secteur", "Origine", "Niveau", "PIRS"];
  const lines = [header.join(",")];
  rows.forEach((r) => {
    const escape = (s) => {
      const v = s == null ? "" : String(s);
      return /[",\n]/.test(v) ? "\"" + v.replace(/"/g, "\"\"") + "\"" : v;
    };
    // Resolve the sector slug to a friendly FR name via the global lookup.
    // Falls back to the slug itself if for some reason the sector is gone.
    let sectorName = "";
    if (r.sector_id) {
      const s = (typeof sectorById === "function") ? sectorById(r.sector_id) : null;
      sectorName = (s && s.fr) ? s.fr : r.sector_id;
    }
    lines.push([
      escape(r.code),
      escape(r.name_fr),
      escape(r.name_en || ""),
      escape(sectorName),
      escape(r.origin_institution || ""),
      escape(r.level || ""),
      escape(r.pirs_filename || ""),
    ].join(","));
  });
  const blob = new Blob(["﻿" + lines.join("\n")], { type: "text/csv;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "indicator-definitions-" + new Date().toISOString().slice(0, 10) + ".csv";
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

window.IndicatorDefinitions = IndicatorDefinitions;
