/* global React, Icon */
// ============================================================================
// L.1 · Intégration KoboToolbox / ODK Central — Foundation
// ----------------------------------------------------------------------------
// Liste, crée, modifie, teste les connexions vers des instances Kobo ou
// ODK Central. Les credentials sont stockés en BD (RLS admin-only) et
// toutes les requêtes API passent par l'edge function kobo-proxy.
//
// Phases à venir :
//   L.2 — Import des formulaires distants
//   L.3 — Mapping formulaires → indicateurs MELR
//   L.4 — Sync automatique des soumissions
// ============================================================================

const { useState: useStateKO, useEffect: useEffectKO } = React;

function KoboOdkScreen({ t, lang, isSuperAdmin, actingOrgId, activeOrgId, myOrgId }) {
  const { has, loading: permsLoading } = window.melr.useCurrentUserPermissions();
  const effOrg = (isSuperAdmin && actingOrgId) ? actingOrgId : (activeOrgId || myOrgId);
  const { data: conns, loading, refresh } = window.melr.useKoboConnections
    ? window.melr.useKoboConnections(effOrg)
    : { data: [], loading: false, refresh: () => {} };

  const [editor, setEditor] = useStateKO(null); // null | 'new' | row
  const [testing, setTesting] = useStateKO(null); // connection_id currently being tested
  const [busy, setBusy] = useStateKO(null);
  const [expanded, setExpanded] = useStateKO(null); // connection_id whose forms are visible
  const [importing, setImporting] = useStateKO(null); // connection_id being imported
  const [importResult, setImportResult] = useStateKO(null); // dernier résultat d'import

  if (permsLoading) {
    return <div className="page"><div style={{ padding: 28 }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div></div>;
  }
  const canEdit = isSuperAdmin || (has && (has("orgs.manage") || has("users.manage")));
  if (!canEdit) {
    return (
      <div className="page">
        <div className="page-header">
          <div className="page-eyebrow">{window.L("INTÉGRATIONS", "INTEGRATIONS", "INTEGRACIONES")}</div>
          <h1 className="page-title">{window.L("KoboToolbox / ODK Central", "KoboToolbox / ODK Central", "KoboToolbox / ODK Central")}</h1>
        </div>
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="text-faint" style={{ textAlign: "center" }}>
            {window.L("Accès réservé aux administrateurs d'organisation.", "Restricted to organization administrators.", "Acceso reservado a los administradores de la organización.")}
          </div>
        </div>
      </div>
    );
  }

  const T = (fr, en) => (lang === "fr" ? fr : en);

  const onTest = async (id) => {
    setTesting(id);
    try {
      const r = await window.melr.testKoboConnection(id);
      // Refresh pour récupérer last_test_*
      await refresh();
      window.alert(r.ok
        ? T("✓ Connexion OK", "✓ Connection OK") + " — " + r.message
        : T("✕ Échec du test", "✕ Test failed") + " — " + r.message);
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + (e.message || String(e)));
    } finally {
      setTesting(null);
    }
  };

  const onImportForms = async (id) => {
    setImporting(id); setImportResult(null);
    try {
      const r = await window.melr.importKoboForms(id);
      setImportResult({ connection_id: id, ...r });
      setExpanded(id);  // ouvre automatiquement la liste des formulaires
    } catch (e) {
      window.alert((window.L("Erreur d'import : ", "Import error: ", "Error de importación: ")) + (e.message || String(e)));
    } finally {
      setImporting(null);
    }
  };

  const onDelete = async (row) => {
    if (!window.confirm(T(
      `Supprimer la connexion « ${row.name} » ?`,
      `Delete connection "${row.name}"?`
    ))) return;
    setBusy(row.id);
    try {
      await window.melr.koboConnectionsCrud.remove(row.id);
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  const onSaved = () => {
    setEditor(null);
    refresh();
  };

  const fmtDate = (iso) => {
    if (!iso) return "—";
    try { return new Date(iso).toLocaleString(window.L("fr-FR", "en-GB", "es-ES")); }
    catch (_) { return iso; }
  };

  const kindLabel = (kind) =>
    kind === "kobo" ? "KoboToolbox" : kind === "odk_central" ? "ODK Central" : kind;

  return (
    <div className="page" data-screen-label="App / kobo_odk">
      <div className="page-header">
        <div className="page-eyebrow">{T("INTÉGRATIONS", "INTEGRATIONS")}</div>
        <div className="page-header-row" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
          <div>
            <h1 className="page-title">{T("KoboToolbox / ODK Central", "KoboToolbox / ODK Central")}</h1>
            <div className="page-sub" style={{ fontSize: 13, color: "var(--text-faint)", marginTop: 4 }}>
              {T(
                "Connectez vos instances Kobo ou ODK Central existantes pour importer vos soumissions en tant que valeurs d'indicateurs MELR.",
                "Connect your existing Kobo or ODK Central instances to import submissions as MELR indicator values."
              )}
            </div>
          </div>
          <button onClick={() => setEditor("new")}
            style={{
              padding: "8px 14px", borderRadius: 6, border: "1px solid #1d4ed8",
              background: "#2563eb", color: "white", cursor: "pointer",
              fontWeight: 600, fontSize: 13,
            }}>
            + {T("Nouvelle connexion", "New connection")}
          </button>
        </div>
      </div>

      <div className="card" style={{ marginTop: 16 }}>
        {loading ? (
          <div style={{ padding: 28, textAlign: "center", color: "var(--text-faint)" }}>
            {T("Chargement…", "Loading…")}
          </div>
        ) : (conns || []).length === 0 ? (
          <div style={{ padding: 36, textAlign: "center" }}>
            <div style={{ fontSize: 32, marginBottom: 12 }}>🔗</div>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>
              {T("Aucune connexion configurée", "No connection configured yet")}
            </div>
            <div className="text-faint" style={{ fontSize: 12.5, maxWidth: 480, margin: "0 auto" }}>
              {T(
                "Créez une connexion vers votre instance Kobo (token API) ou ODK Central (email+mot de passe) pour démarrer.",
                "Create a connection to your Kobo instance (API token) or ODK Central (email+password) to get started."
              )}
            </div>
          </div>
        ) : (
          <div style={{ overflowX: "auto" }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
              <thead>
                <tr style={{ background: "var(--bg-soft, #f9fafb)", textAlign: "left" }}>
                  <th style={th}>{T("Nom", "Name")}</th>
                  <th style={th}>{T("Type", "Type")}</th>
                  <th style={th}>URL</th>
                  <th style={th}>{T("Dernier test", "Last test")}</th>
                  <th style={th}>{T("Actions", "Actions")}</th>
                </tr>
              </thead>
              <tbody>
                {conns.map((c) => (
                  <React.Fragment key={c.id}>
                  <tr style={{ borderTop: "1px solid var(--border-soft, #f1f5f9)" }}>
                    <td style={td}>
                      <div style={{ fontWeight: 600 }}>{c.name}</div>
                      {!c.enabled && (
                        <span style={{ display: "inline-block", marginTop: 2, padding: "1px 6px", borderRadius: 8, background: "#fef3c7", color: "#92400e", fontSize: 10, fontWeight: 700 }}>
                          {T("DÉSACTIVÉE", "DISABLED")}
                        </span>
                      )}
                    </td>
                    <td style={td}>
                      <span style={{
                        display: "inline-block", padding: "2px 8px", borderRadius: 10,
                        background: c.kind === "kobo" ? "#dbeafe" : "#e0e7ff",
                        color:      c.kind === "kobo" ? "#1e40af" : "#3730a3",
                        fontSize: 11, fontWeight: 600,
                      }}>{kindLabel(c.kind)}</span>
                    </td>
                    <td style={td}>
                      <code style={{ fontSize: 11, color: "#475569" }}>{c.base_url}</code>
                    </td>
                    <td style={td}>
                      {c.last_test_at ? (
                        <div>
                          <div style={{
                            display: "inline-flex", alignItems: "center", gap: 4,
                            color: c.last_test_ok ? "#16a34a" : "#dc2626", fontWeight: 600,
                          }}>
                            {c.last_test_ok ? "✓" : "✕"} {fmtDate(c.last_test_at)}
                          </div>
                          {c.last_test_message && (
                            <div style={{ fontSize: 11, color: "var(--text-faint)", maxWidth: 240, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={c.last_test_message}>
                              {c.last_test_message}
                            </div>
                          )}
                        </div>
                      ) : <span className="text-faint">—</span>}
                    </td>
                    <td style={td}>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                        <button onClick={() => onTest(c.id)} disabled={testing === c.id || busy === c.id}
                          style={btn(testing === c.id ? "#cbd5e1" : "#22c55e", "white", "#16a34a")}>
                          {testing === c.id ? "…" : T("Tester", "Test")}
                        </button>
                        <button onClick={() => onImportForms(c.id)} disabled={importing === c.id || busy === c.id}
                          style={btn(importing === c.id ? "#cbd5e1" : "#3b82f6", "white", "#2563eb")}>
                          {importing === c.id ? T("Import…", "Import…") : T("Importer formulaires", "Import forms")}
                        </button>
                        <button onClick={() => setExpanded(expanded === c.id ? null : c.id)} disabled={busy === c.id}
                          style={btn("white", "#334155", "#cbd5e1")}>
                          {expanded === c.id ? T("Masquer", "Hide") : T("Formulaires", "Forms")}
                        </button>
                        <button onClick={() => setEditor(c)} disabled={busy === c.id}
                          style={btn("white", "#334155", "#cbd5e1")}>
                          {T("Modifier", "Edit")}
                        </button>
                        <button onClick={() => onDelete(c)} disabled={busy === c.id}
                          style={btn("white", "#dc2626", "#fca5a5")}>
                          {T("Supprimer", "Delete")}
                        </button>
                      </div>
                    </td>
                  </tr>
                  {expanded === c.id && (
                    <tr>
                      <td colSpan={5} style={{ padding: 0, background: "#f8fafc", borderTop: "1px solid #e2e8f0" }}>
                        <KoboFormsList connectionId={c.id} lang={lang}
                          lastImport={importResult && importResult.connection_id === c.id ? importResult : null} />
                      </td>
                    </tr>
                  )}
                  </React.Fragment>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>

      {editor && (
        <KoboConnectionEditor
          row={editor === "new" ? null : editor}
          lang={lang}
          orgId={effOrg}
          onClose={() => setEditor(null)}
          onSaved={onSaved} />
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Editor (create / update)
// ─────────────────────────────────────────────────────────────────────────────
function KoboConnectionEditor({ row, lang, orgId, onClose, onSaved }) {
  const T = (fr, en) => (lang === "fr" ? fr : en);
  const isNew = !row;

  const [name, setName]         = useStateKO(row ? row.name : "");
  const [kind, setKind]         = useStateKO(row ? row.kind : "kobo");
  const [baseUrl, setBaseUrl]   = useStateKO(row ? row.base_url : "https://kf.kobotoolbox.org");
  const [authType, setAuthType] = useStateKO(row ? row.auth_type : "token");
  const [apiToken, setApiToken] = useStateKO(row ? (row.api_token || "") : "");
  const [username, setUsername] = useStateKO(row ? (row.username || "") : "");
  const [password, setPassword] = useStateKO(row ? (row.password || "") : "");
  const [projectId, setProjectId] = useStateKO(row ? (row.default_project_id || "") : "");
  const [enabled, setEnabled]   = useStateKO(row ? !!row.enabled : true);

  const [busy, setBusy] = useStateKO(false);
  const [err, setErr]   = useStateKO(null);

  // Quand on change le kind, on aligne les valeurs par défaut sensées
  const onKindChange = (k) => {
    setKind(k);
    if (k === "kobo") {
      if (!baseUrl || baseUrl.includes("central")) setBaseUrl("https://kf.kobotoolbox.org");
      setAuthType("token");
    } else {
      if (!baseUrl || baseUrl.includes("kobotoolbox")) setBaseUrl("https://central.example.org");
      setAuthType("basic");
    }
  };

  const onSubmit = async (e) => {
    if (e && e.preventDefault) e.preventDefault();
    if (!name.trim()) { setErr(T("Nom requis.", "Name required.")); return; }
    if (!baseUrl.trim()) { setErr(T("URL requise.", "URL required.")); return; }
    // Validation des credentials selon le mode
    if (kind === "kobo" && !apiToken.trim()) {
      setErr(T("Token API Kobo requis.", "Kobo API token required.")); return;
    }
    if (kind === "odk_central" && authType === "basic" && (!username.trim() || !password)) {
      setErr(T("Email + mot de passe requis pour ODK Central.", "Email + password required for ODK Central.")); return;
    }

    setBusy(true); setErr(null);
    try {
      const payload = {
        organization_id: orgId,
        name: name.trim(),
        kind,
        base_url: baseUrl.trim().replace(/\/+$/, ""),
        auth_type: kind === "kobo" ? "token" : authType,
        api_token: (kind === "kobo" || authType === "token") ? apiToken.trim() : null,
        username:  (kind === "odk_central" && authType === "basic") ? username.trim() : null,
        password:  (kind === "odk_central" && authType === "basic") ? password : null,
        default_project_id: kind === "odk_central" ? (projectId.trim() || null) : null,
        enabled,
      };
      if (isNew) await window.melr.koboConnectionsCrud.create(payload);
      else       await window.melr.koboConnectionsCrud.update(row.id, payload);
      onSaved();
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={overlay} onClick={() => !busy && onClose()}>
      <div style={modal} onClick={(e) => e.stopPropagation()}>
        <h3 style={{ margin: 0, marginBottom: 14, fontSize: 17 }}>
          {isNew ? T("Nouvelle connexion", "New connection") : T("Modifier la connexion", "Edit connection")}
        </h3>

        <form onSubmit={onSubmit}>
          <div style={fieldRow}>
            <label style={lbl}>{T("Nom (libre)", "Name (free text)")} *</label>
            <input value={name} onChange={(e) => setName(e.target.value)} style={inp} required maxLength={120} />
          </div>

          <div style={fieldRow}>
            <label style={lbl}>{T("Plateforme", "Platform")} *</label>
            <select value={kind} onChange={(e) => onKindChange(e.target.value)} style={inp}>
              <option value="kobo">KoboToolbox</option>
              <option value="odk_central">ODK Central</option>
            </select>
          </div>

          <div style={fieldRow}>
            <label style={lbl}>{T("URL de l'instance", "Instance URL")} *</label>
            <input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} style={inp} required maxLength={300}
              placeholder={kind === "kobo" ? "https://kf.kobotoolbox.org" : "https://central.example.org"} />
            <div style={hint}>
              {kind === "kobo"
                ? T("Par défaut : https://kf.kobotoolbox.org (instance OCHA). Remplacez si vous avez une instance dédiée.",
                    "Default: https://kf.kobotoolbox.org (OCHA instance). Replace if you have a dedicated instance.")
                : T("URL racine de votre instance ODK Central (sans /v1).", "Root URL of your ODK Central instance (without /v1).")}
            </div>
          </div>

          {kind === "kobo" && (
            <div style={fieldRow}>
              <label style={lbl}>{T("Token API Kobo", "Kobo API token")} *</label>
              <input type="password" value={apiToken} onChange={(e) => setApiToken(e.target.value)} style={{ ...inp, fontFamily: "ui-monospace, monospace", fontSize: 12 }}
                required maxLength={200} autoComplete="off" />
              <div style={hint}>
                {T(
                  "Récupérable dans Kobo : avatar → Account Settings → API → API token.",
                  "From Kobo: avatar → Account Settings → API → API token."
                )}
              </div>
            </div>
          )}

          {kind === "odk_central" && (
            <>
              <div style={fieldRow}>
                <label style={lbl}>{T("Type d'authentification", "Authentication type")}</label>
                <select value={authType} onChange={(e) => setAuthType(e.target.value)} style={inp}>
                  <option value="basic">{T("Email + mot de passe (Basic)", "Email + password (Basic)")}</option>
                  <option value="token">{T("Token (avancé)", "Token (advanced)")}</option>
                </select>
              </div>
              {authType === "basic" ? (
                <>
                  <div style={fieldRow}>
                    <label style={lbl}>{T("Email du compte ODK", "ODK account email")} *</label>
                    <input type="email" value={username} onChange={(e) => setUsername(e.target.value)} style={inp} required maxLength={200} autoComplete="off" />
                  </div>
                  <div style={fieldRow}>
                    <label style={lbl}>{T("Mot de passe", "Password")} *</label>
                    <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} style={inp} required maxLength={200} autoComplete="off" />
                    <div style={hint}>
                      {T(
                        "Astuce : créez un compte service dédié à MELR pour ne pas exposer un compte humain.",
                        "Tip: create a dedicated service account for MELR instead of a personal one."
                      )}
                    </div>
                  </div>
                </>
              ) : (
                <div style={fieldRow}>
                  <label style={lbl}>{T("Token de session", "Session token")} *</label>
                  <input type="password" value={apiToken} onChange={(e) => setApiToken(e.target.value)} style={{ ...inp, fontFamily: "ui-monospace, monospace", fontSize: 12 }}
                    required maxLength={400} autoComplete="off" />
                </div>
              )}
              <div style={fieldRow}>
                <label style={lbl}>{T("ID projet par défaut (optionnel)", "Default project ID (optional)")}</label>
                <input value={projectId} onChange={(e) => setProjectId(e.target.value)} style={inp} maxLength={40} placeholder="42" />
                <div style={hint}>
                  {T("Numéro entier du projet dans ODK Central. Si vide, vous le choisirez plus tard pour chaque formulaire.",
                     "Integer project ID in ODK Central. If empty, choose it per form later.")}
                </div>
              </div>
            </>
          )}

          <div style={{ ...fieldRow, display: "flex", alignItems: "center", gap: 8 }}>
            <input type="checkbox" id="kobo-enabled" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} />
            <label htmlFor="kobo-enabled" style={{ fontSize: 13 }}>
              {T("Connexion activée", "Connection enabled")}
            </label>
          </div>

          {err && (
            <div style={{ marginTop: 10, padding: 8, background: "#fee2e2", border: "1px solid #fca5a5", borderRadius: 6, color: "#7f1d1d", fontSize: 12 }}>
              {err}
            </div>
          )}

          <div style={{ marginTop: 16, display: "flex", justifyContent: "flex-end", gap: 8 }}>
            <button type="button" onClick={onClose} disabled={busy}
              style={btnGhost}>
              {T("Annuler", "Cancel")}
            </button>
            <button type="submit" disabled={busy}
              style={btnPrimary(busy)}>
              {busy ? T("Enregistrement…", "Saving…") : (isNew ? T("Créer", "Create") : T("Enregistrer", "Save"))}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Styles
// ─────────────────────────────────────────────────────────────────────────────
const th = { padding: "8px 12px", fontSize: 11, fontWeight: 600, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: 0.4 };
const td = { padding: "10px 12px", fontSize: 13, verticalAlign: "top" };
const lbl = { display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4, color: "#334155" };
const inp = { width: "100%", boxSizing: "border-box", padding: 8, borderRadius: 6, border: "1px solid #cbd5e1", fontSize: 13, fontFamily: "inherit" };
const fieldRow = { marginBottom: 14 };
const hint = { fontSize: 11, color: "#94a3b8", marginTop: 3, lineHeight: 1.5 };
const overlay = { position: "fixed", inset: 0, background: "rgba(15,23,42,0.55)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 20 };
const modal = { background: "white", borderRadius: 10, maxWidth: 560, width: "100%", maxHeight: "90vh", overflow: "auto", padding: 24, boxShadow: "0 20px 50px rgba(0,0,0,0.25)" };

const btn = (bg, fg, br) => ({
  padding: "4px 10px", borderRadius: 6, border: `1px solid ${br}`,
  background: bg, color: fg, cursor: "pointer", fontSize: 11.5, fontWeight: 600,
});
const btnGhost = { padding: "8px 14px", borderRadius: 6, border: "1px solid #94a3b8", background: "white", color: "#334155", cursor: "pointer", fontSize: 13 };
const btnPrimary = (busy) => ({
  padding: "8px 14px", borderRadius: 6, border: "1px solid #1d4ed8",
  background: busy ? "#cbd5e1" : "#2563eb", color: "white",
  cursor: busy ? "default" : "pointer", fontWeight: 600, fontSize: 13,
});

// ─────────────────────────────────────────────────────────────────────────────
// L.2 · Liste des formulaires distants importés pour une connexion
// ─────────────────────────────────────────────────────────────────────────────
function KoboFormsList({ connectionId, lang, lastImport }) {
  const T = (fr, en) => (lang === "fr" ? fr : en);

  // CRITIQUE : guard AVANT tout hook. Si window.melr ou ses hooks ne sont
  // pas encore chargés (race au boot), on render une UI minimale SANS
  // appeler de hooks. Une fois melr-data.jsx parsé, ce composant sera
  // démonté et re-monté avec les hooks complets — pas de Rules-of-Hooks
  // violation.
  if (!window.melr || !window.melr.useKoboForms || !window.melr.useProjects) {
    return (
      <div style={{ padding: 16, textAlign: "center", color: "var(--text-faint)", fontSize: 13 }}>
        {window.L("Chargement…", "Loading…", "Cargando…")}
      </div>
    );
  }

  // À partir d'ici, tous les hooks sont TOUJOURS appelés dans le même ordre.
  const { data: forms, loading, refresh } = window.melr.useKoboForms(connectionId);
  const { projects } = window.melr.useProjects();
  const [busy, setBusy] = useStateKO(null);
  const [mappingFor, setMappingFor] = useStateKO(null);  // L.3b : form en cours d'édition de mapping
  const [syncingId, setSyncingId] = useStateKO(null);    // L.4d : form en cours de sync
  const [syncFlash, setSyncFlash] = useStateKO(null);    // L.4d : { formId, ok, msg, counts? } — bandeau résultat
  const [runsOpenFor, setRunsOpenFor] = useStateKO(null); // L.4d : form dont le panneau Runs est ouvert
  const lastSeen = lastImport ? lastImport.seenAt : null;

  // Re-fetch quand un import vient de se terminer.
  useEffectKO(() => {
    if (lastSeen) { refresh(); }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lastSeen]);

  const onLinkProject = async (formId, projectId) => {
    setBusy(formId);
    try {
      await window.melr.koboFormsCrud.update(formId, {
        linked_project_id: projectId || null,
      });
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  const onToggleEnabled = async (form) => {
    setBusy(form.id);
    try {
      await window.melr.koboFormsCrud.update(form.id, { enabled: !form.enabled });
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  const onRemove = async (form) => {
    if (!window.confirm(T(
      `Supprimer le formulaire « ${form.title} » localement ? (Il restera sur Kobo/ODK et pourra être ré-importé.)`,
      `Delete form "${form.title}" locally? (It will remain on Kobo/ODK and can be re-imported.)`
    ))) return;
    setBusy(form.id);
    try {
      await window.melr.koboFormsCrud.remove(form.id);
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  // L.4d · Sync manuelle d'un formulaire (pull Kobo + map → indicator_values).
  // Sécurise contre les fonctions absentes (race au boot melr-data.jsx).
  const onSync = async (form) => {
    if (!window.melr.syncKoboForm) {
      window.alert(T(
        "La fonction de synchronisation n'est pas encore chargée. Rechargez la page.",
        "Sync function not loaded yet. Please reload the page.",
      ));
      return;
    }
    setSyncingId(form.id);
    setSyncFlash(null);
    try {
      const r = await window.melr.syncKoboForm(form.id);
      // Succès : afficher les compteurs
      setSyncFlash({
        formId: form.id, ok: true,
        msg: T(
          `Sync ${r.status === "partial" ? "partielle" : "réussie"} · ${r.counts.fetched} soumission(s), ${r.counts.indicator_values} valeur(s) créée(s)${r.counts.errors > 0 ? `, ${r.counts.errors} erreur(s)` : ""}`,
          `Sync ${r.status === "partial" ? "partial" : "succeeded"} · ${r.counts.fetched} submission(s), ${r.counts.indicator_values} value(s) created${r.counts.errors > 0 ? `, ${r.counts.errors} error(s)` : ""}`,
        ),
        counts: r.counts,
      });
      await refresh();
      // Si le panneau runs est ouvert pour ce form → il se rafraîchira tout seul
      // via son propre hook useKoboSyncRuns (realtime n'est pas wired, mais
      // au prochain re-render il re-fetchera).
    } catch (e) {
      // Distinguer panne amont (retryable) vs vraie erreur
      setSyncFlash({
        formId: form.id, ok: false,
        msg: (e && e.message) || T("Erreur inconnue", "Unknown error"),
        retryable: !!(e && e.retryable),
      });
    } finally {
      setSyncingId(null);
    }
  };

  const fmtDate = (iso) => {
    if (!iso) return "—";
    try { return new Date(iso).toLocaleDateString(window.L("fr-FR", "en-GB", "es-ES")); }
    catch (_) { return iso; }
  };

  const statusChip = (status) => {
    const m = {
      deployed: { fr: "Déployé", en: "Deployed", es: "Desplegado", bg: "#dcfce7", color: "#166534" },
      draft:    { fr: "Brouillon", en: "Draft", es: "Borrador", bg: "#fef3c7", color: "#92400e" },
      archived: { fr: "Archivé", en: "Archived", es: "Archivado", bg: "#f1f5f9", color: "#475569" },
      open:     { fr: "Ouvert", en: "Open", es: "Abierto", bg: "#dcfce7", color: "#166534" },
      closing:  { fr: "En clôture", en: "Closing", es: "En cierre", bg: "#fed7aa", color: "#9a3412" },
      closed:   { fr: "Clos", en: "Closed", es: "Cerrado", bg: "#fee2e2", color: "#991b1b" },
    }[status] || (status ? { fr: status, en: status, bg: "#f1f5f9", color: "#475569" } : null);
    if (!m) return <span className="text-faint">—</span>;
    return (
      <span style={{
        display: "inline-block", padding: "1px 8px", borderRadius: 10,
        background: m.bg, color: m.color, fontSize: 10.5, fontWeight: 600,
      }}>{T(m.fr, m.en)}</span>
    );
  };

  return (
    <div style={{ padding: "14px 18px 18px 18px" }}>
      {/* Récap du dernier import */}
      {lastImport && (
        <div style={{
          marginBottom: 12, padding: 10, borderRadius: 6, fontSize: 12.5,
          background: (lastImport.errors && lastImport.errors.length > 0) ? "#fef3c7" : "#dcfce7",
          border: "1px solid " + ((lastImport.errors && lastImport.errors.length > 0) ? "#fde68a" : "#86efac"),
          color: (lastImport.errors && lastImport.errors.length > 0) ? "#92400e" : "#166534",
        }}>
          ✓ {T(
            `Import terminé : ${lastImport.total} formulaire(s) trouvé(s).`,
            `Import complete: ${lastImport.total} form(s) found.`
          )}
          {lastImport.errors && lastImport.errors.length > 0 && (
            <details style={{ marginTop: 4 }}>
              <summary style={{ cursor: "pointer", color: "#92400e" }}>
                {T(`${lastImport.errors.length} avertissement(s)`, `${lastImport.errors.length} warning(s)`)}
              </summary>
              <ul style={{ marginTop: 6, paddingLeft: 18, fontSize: 11.5 }}>
                {lastImport.errors.slice(0, 10).map((e, i) => <li key={i}>{e}</li>)}
              </ul>
            </details>
          )}
        </div>
      )}

      {/* Tableau formulaires */}
      {loading ? (
        <div style={{ padding: 16, textAlign: "center", color: "var(--text-faint)", fontSize: 13 }}>
          {T("Chargement…", "Loading…")}
        </div>
      ) : (forms || []).length === 0 ? (
        <div style={{ padding: 16, textAlign: "center", color: "var(--text-faint)", fontSize: 13 }}>
          {T(
            "Aucun formulaire importé. Cliquez sur « Importer formulaires » pour récupérer la liste depuis Kobo/ODK.",
            "No form imported yet. Click \"Import forms\" to fetch the list from Kobo/ODK."
          )}
        </div>
      ) : (
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12.5 }}>
          <thead>
            <tr style={{ textAlign: "left", color: "var(--text-faint)" }}>
              <th style={{ ...thF, paddingLeft: 0 }}>{T("Titre", "Title")}</th>
              <th style={thF}>{T("ID distant", "Remote ID")}</th>
              <th style={thF}>{T("Statut", "Status")}</th>
              <th style={thF}>{T("Soumissions", "Submissions")}</th>
              <th style={thF}>{T("Lié au projet MELR", "Linked MELR project")}</th>
              <th style={thF}>{T("Actions", "Actions")}</th>
            </tr>
          </thead>
          <tbody>
            {(forms || []).map((f) => (
              <React.Fragment key={f.id}>
              <tr style={{ borderTop: "1px solid #e2e8f0", opacity: f.enabled ? 1 : 0.55 }}>
                <td style={{ ...tdF, paddingLeft: 0 }}>
                  <div style={{ fontWeight: 600 }}>{f.title}</div>
                  {f.remote_project_id && (
                    <div style={{ fontSize: 10.5, color: "var(--text-faint)" }}>
                      {T("Projet ODK :", "ODK project:")} {f.remote_project_id}
                    </div>
                  )}
                </td>
                <td style={tdF}>
                  <code style={{ fontSize: 10.5, background: "white", padding: "1px 5px", borderRadius: 3, border: "1px solid #e2e8f0" }}>
                    {f.remote_id}
                  </code>
                </td>
                <td style={tdF}>{statusChip(f.status)}</td>
                <td style={tdF}>
                  <div style={{ fontWeight: 600 }}>{f.submission_count || 0}</div>
                  {f.last_submission_at && (
                    <div style={{ fontSize: 10.5, color: "var(--text-faint)" }}>
                      {T("Dernière :", "Last:")} {fmtDate(f.last_submission_at)}
                    </div>
                  )}
                </td>
                <td style={tdF}>
                  <select value={f.linked_project_id || ""} onChange={(e) => onLinkProject(f.id, e.target.value || null)}
                    disabled={busy === f.id}
                    style={{ padding: "3px 6px", borderRadius: 4, border: "1px solid #cbd5e1", fontSize: 11.5, width: "100%", maxWidth: 220 }}>
                    <option value="">{T("(non lié)", "(unlinked)")}</option>
                    {/* mapProjectRow remaps: p.uuid = DB UUID, p.id = legacy code,
                        p.nameFr/p.nameEn = localized names. linked_project_id stores the UUID. */}
                    {(projects || []).map((p) => (
                      <option key={p.uuid} value={p.uuid}>
                        {p.id ? `${p.id} · ` : ""}{(lang === "fr" ? (p.nameFr || p.nameEn) : (p.nameEn || p.nameFr)) || T("(sans nom)", "(no name)")}
                      </option>
                    ))}
                  </select>
                </td>
                <td style={tdF}>
                  <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                    {/* L.4d · Sync : exige status='deployed' chez Kobo (sinon /data/ renvoie 400 « not deployed »).
                        On désactive le bouton + tooltip explicatif pour épargner un aller-retour. */}
                    {(() => {
                      const notDeployed = f.status && f.status !== "deployed" && f.status !== "open";
                      const syncDisabled = syncingId === f.id || busy === f.id || !f.enabled || notDeployed;
                      const syncTitle = !f.enabled
                        ? T("Formulaire désactivé localement", "Form disabled locally")
                        : notDeployed
                          ? T(
                              `Impossible : ce formulaire est en statut « ${f.status} » sur Kobo/ODK. Déployez-le d'abord sur la plateforme distante, puis ré-importez pour rafraîchir le status.`,
                              `Cannot sync: this form is in status "${f.status}" on Kobo/ODK. Deploy it on the remote platform first, then re-import to refresh the status.`,
                            )
                          : T("Synchroniser les soumissions (pull Kobo + map vers indicateurs)", "Sync submissions (pull from Kobo + map to indicators)");
                      return (
                        <button onClick={() => onSync(f)} disabled={syncDisabled}
                          style={{
                            ...btnTiny(
                              syncingId === f.id ? "#cbd5e1" : (syncDisabled ? "#e2e8f0" : "#10b981"),
                              syncDisabled && syncingId !== f.id ? "#94a3b8" : "white",
                              syncDisabled && syncingId !== f.id ? "#cbd5e1" : "#059669",
                            ),
                            cursor: syncDisabled ? "not-allowed" : "pointer",
                          }}
                          title={syncTitle}>
                          {syncingId === f.id ? "⏳" : "🔄"} {T("Sync", "Sync")}
                        </button>
                      );
                    })()}
                    <button onClick={() => setRunsOpenFor(runsOpenFor === f.id ? null : f.id)} disabled={busy === f.id}
                      style={btnTiny(runsOpenFor === f.id ? "#dbeafe" : "white", "#475569", "#cbd5e1")}
                      title={T("Voir les derniers runs de sync", "Show last sync runs")}>
                      📊 {T("Runs", "Runs")}
                    </button>
                    <button onClick={() => setMappingFor(f)} disabled={busy === f.id}
                      style={btnTiny("#3b82f6", "white", "#2563eb")}
                      title={T("Définir le mapping vers les indicateurs MELR", "Define mapping to MELR indicators")}>
                      🔗 {T("Mapping", "Mapping")}
                    </button>
                    <button onClick={() => onToggleEnabled(f)} disabled={busy === f.id}
                      style={btnTiny(f.enabled ? "white" : "#dcfce7", f.enabled ? "#475569" : "#166534", "#cbd5e1")}
                      title={f.enabled ? T("Désactiver localement", "Disable locally") : T("Activer", "Enable")}>
                      {f.enabled ? T("Actif", "On") : T("Off", "Off")}
                    </button>
                    <button onClick={() => onRemove(f)} disabled={busy === f.id}
                      style={btnTiny("white", "#dc2626", "#fca5a5")}>
                      {T("Suppr.", "Del.")}
                    </button>
                  </div>
                </td>
              </tr>

              {/* L.4d · Bandeau de résultat de sync (succès / erreur) — affiché juste sous la ligne du form concerné */}
              {syncFlash && syncFlash.formId === f.id && (
                <tr>
                  <td colSpan={6} style={{ padding: "6px 10px 0 10px" }}>
                    <div style={{
                      padding: "8px 12px", borderRadius: 6, fontSize: 12,
                      background: syncFlash.ok ? "#dcfce7" : (syncFlash.retryable ? "#fef3c7" : "#fee2e2"),
                      border: "1px solid " + (syncFlash.ok ? "#86efac" : (syncFlash.retryable ? "#fde68a" : "#fca5a5")),
                      color:      (syncFlash.ok ? "#166534" : (syncFlash.retryable ? "#92400e" : "#991b1b")),
                      display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12,
                    }}>
                      <div style={{ flex: 1 }}>
                        {syncFlash.ok ? "✓ " : (syncFlash.retryable ? "⏳ " : "✗ ")}
                        {syncFlash.msg}
                      </div>
                      <button onClick={() => setSyncFlash(null)}
                        style={{ background: "transparent", border: "none", cursor: "pointer", color: "inherit", padding: 0, fontSize: 14 }}
                        title={T("Fermer", "Close")}>✕</button>
                    </div>
                  </td>
                </tr>
              )}

              {/* L.4d · Panneau dépliable des derniers runs de sync */}
              {runsOpenFor === f.id && (
                <tr>
                  <td colSpan={6} style={{ padding: "6px 10px 10px 10px", background: "#f8fafc" }}>
                    <KoboFormSyncRunsPanel formId={f.id} lang={lang} />
                  </td>
                </tr>
              )}
              </React.Fragment>
            ))}
          </tbody>
        </table>
      )}

      <div style={{ marginTop: 10, fontSize: 11.5, color: "#94a3b8", lineHeight: 1.5 }}>
        💡 {T(
          "Liez chaque formulaire à un projet MELR + cliquez sur « 🔗 Mapping » pour associer chaque champ XLSForm à un indicateur, puis « 🔄 Sync » pour importer les soumissions et créer les valeurs d'indicateur. « 📊 Runs » affiche l'historique des syncs.",
          "Link each form to a MELR project + click \"🔗 Mapping\" to associate each XLSForm field to an indicator, then \"🔄 Sync\" to import submissions and create indicator values. \"📊 Runs\" shows sync history."
        )}
      </div>

      {/* L.3b · Modal d'édition du mapping pour un formulaire */}
      {mappingFor && (
        <KoboFormMappingEditor form={mappingFor} lang={lang}
          onClose={() => setMappingFor(null)} />
      )}
    </div>
  );
}

const thF = { padding: "6px 10px", fontSize: 10.5, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.4 };
const tdF = { padding: "8px 10px", fontSize: 12.5, verticalAlign: "top" };
const btnTiny = (bg, fg, br) => ({
  padding: "2px 8px", borderRadius: 4, border: `1px solid ${br}`,
  background: bg, color: fg, cursor: "pointer", fontSize: 10.5, fontWeight: 600,
});

// ─────────────────────────────────────────────────────────────────────────────
// L.4d · Panneau des derniers runs de sync pour un formulaire
// ─────────────────────────────────────────────────────────────────────────────
// Affiché en ligne sous la ligne du form quand l'admin clique « 📊 Runs ».
// Pattern défensif : hook safe avec noop fallback (race au boot).
// ─────────────────────────────────────────────────────────────────────────────
const _noopRunsHook = function () {
  return { data: [], loading: false, refresh: function () {} };
};

function KoboFormSyncRunsPanel({ formId, lang }) {
  const T = (fr, en) => (lang === "fr" ? fr : en);
  const useRunsSafe = (window.melr && window.melr.useKoboSyncRuns) || _noopRunsHook;
  const { data: runs, loading, refresh } = useRunsSafe(formId);

  const fmt = (iso) => {
    if (!iso) return "—";
    try { return new Date(iso).toLocaleString(window.L("fr-FR", "en-GB", "es-ES")); }
    catch (_) { return iso; }
  };
  const duration = (start, end) => {
    if (!start || !end) return "—";
    const ms = new Date(end).getTime() - new Date(start).getTime();
    if (isNaN(ms) || ms < 0) return "—";
    if (ms < 1000) return `${ms}ms`;
    if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
    return `${(ms / 60_000).toFixed(1)}min`;
  };
  const statusBadge = (s) => {
    const m = {
      running: { fr: "En cours", en: "Running", es: "En curso",  bg: "#dbeafe", color: "#1e40af" },
      success: { fr: "Succès",   en: "Success", es: "Éxito",  bg: "#dcfce7", color: "#166534" },
      partial: { fr: "Partiel",  en: "Partial", es: "Parcial",  bg: "#fef3c7", color: "#92400e" },
      error:   { fr: "Échec",    en: "Failed", es: "Fracaso",   bg: "#fee2e2", color: "#991b1b" },
    }[s] || { fr: s, en: s, bg: "#f1f5f9", color: "#475569" };
    return (
      <span style={{
        display: "inline-block", padding: "1px 8px", borderRadius: 10,
        background: m.bg, color: m.color, fontSize: 10.5, fontWeight: 600,
      }}>{T(m.fr, m.en)}</span>
    );
  };

  return (
    <div style={{ padding: "8px 12px", border: "1px solid #e2e8f0", borderRadius: 6, background: "white" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
        <strong style={{ fontSize: 12.5 }}>📊 {T("Derniers runs de synchronisation", "Last sync runs")}</strong>
        <button onClick={refresh} disabled={loading}
          style={{ ...btnTiny("white", "#475569", "#cbd5e1"), opacity: loading ? 0.5 : 1 }}>
          {loading ? "⏳" : "↻"} {T("Rafraîchir", "Refresh")}
        </button>
      </div>
      {loading && (!runs || runs.length === 0) ? (
        <div style={{ padding: 8, textAlign: "center", color: "var(--text-faint)", fontSize: 12 }}>
          {T("Chargement…", "Loading…")}
        </div>
      ) : (!runs || runs.length === 0) ? (
        <div style={{ padding: 8, textAlign: "center", color: "var(--text-faint)", fontSize: 12 }}>
          {T("Aucun run de sync enregistré pour ce formulaire. Cliquez sur « 🔄 Sync » pour en lancer un.",
             "No sync run recorded for this form yet. Click \"🔄 Sync\" to launch one.")}
        </div>
      ) : (
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11.5 }}>
          <thead>
            <tr style={{ textAlign: "left", color: "var(--text-faint)" }}>
              <th style={thR}>{T("Démarré", "Started")}</th>
              <th style={thR}>{T("Durée", "Duration")}</th>
              <th style={thR}>{T("Statut", "Status")}</th>
              <th style={thR}>{T("Par", "By")}</th>
              <th style={thR}>{T("Soumissions", "Submissions")}</th>
              <th style={thR}>{T("Valeurs créées", "Values created")}</th>
              <th style={thR}>{T("Détails", "Details")}</th>
            </tr>
          </thead>
          <tbody>
            {(runs || []).map((r) => (
              <React.Fragment key={r.id}>
                <tr style={{ borderTop: "1px solid #f1f5f9" }}>
                  <td style={tdR}>{fmt(r.started_at)}</td>
                  <td style={tdR}>{duration(r.started_at, r.finished_at)}</td>
                  <td style={tdR}>{statusBadge(r.status)}</td>
                  <td style={tdR}>
                    {r.triggered_by === "cron" ? "⏱ " + T("auto", "auto")
                      : r.triggered_by === "api" ? "🔌 API"
                      : "👤 " + T("manuel", "manual")}
                  </td>
                  <td style={tdR}>
                    <span title={T("Récupérées / Nouvelles / Mises à jour / Ignorées",
                                   "Fetched / Inserted / Updated / Skipped")}>
                      {r.n_fetched} <span style={{ color: "#94a3b8" }}>
                        ({r.n_inserted}+{r.n_updated}{r.n_skipped > 0 ? `, ${r.n_skipped} skip` : ""})
                      </span>
                    </span>
                  </td>
                  <td style={tdR}>
                    <strong>{r.n_indicator_values}</strong>
                    {r.n_errors > 0 && (
                      <span style={{ color: "#dc2626", marginLeft: 4 }}>
                        · {r.n_errors} err
                      </span>
                    )}
                  </td>
                  <td style={tdR}>
                    {r.error_message ? (
                      <code style={{ color: "#dc2626", fontSize: 10.5 }}
                        title={r.error_message}>
                        {r.error_message.length > 60 ? r.error_message.slice(0, 60) + "…" : r.error_message}
                      </code>
                    ) : (r.details && Array.isArray(r.details) && r.details.length > 0) ? (
                      <span style={{ color: "#94a3b8" }}>
                        {r.details.length} {T("avertissement(s)", "warning(s)")}
                      </span>
                    ) : "—"}
                  </td>
                </tr>
                {r.details && Array.isArray(r.details) && r.details.length > 0 && (
                  <tr>
                    <td colSpan={7} style={{ ...tdR, paddingTop: 0 }}>
                      <details>
                        <summary style={{ cursor: "pointer", fontSize: 11, color: "#94a3b8" }}>
                          {T("Voir les avertissements", "Show warnings")}
                        </summary>
                        <ul style={{ margin: "4px 0 0 0", paddingLeft: 18, fontSize: 10.5, color: "#64748b" }}>
                          {r.details.slice(0, 30).map((d, i) => (
                            <li key={i}>
                              {d.remote_id ? <code style={{ fontSize: 10 }}>{d.remote_id.slice(0, 12)}…</code> : null}
                              {d.remote_id ? " · " : ""}{d.msg}
                            </li>
                          ))}
                          {r.details.length > 30 && (
                            <li style={{ fontStyle: "italic" }}>
                              {T(`… et ${r.details.length - 30} autres`,
                                 `… and ${r.details.length - 30} more`)}
                            </li>
                          )}
                        </ul>
                      </details>
                    </td>
                  </tr>
                )}
              </React.Fragment>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

const thR = { padding: "4px 8px", fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.4 };
const tdR = { padding: "5px 8px", fontSize: 11.5, verticalAlign: "top" };

// ─────────────────────────────────────────────────────────────────────────────
// L.3b · Éditeur de mapping XLSForm → indicateurs MELR
// ─────────────────────────────────────────────────────────────────────────────
// Pattern défensif strict : hooks toujours appelés inconditionnellement
// via des helpers noop fallback. Pas d'early return AVANT les hooks.
// ─────────────────────────────────────────────────────────────────────────────
const _noopMappingsHook = function () {
  return { data: [], loading: false, refresh: function () {} };
};
const _noopIndicatorsHook = function () {
  return { data: [], loading: false };
};

function KoboFormMappingEditor({ form, lang, onClose }) {
  const T = (fr, en) => (lang === "fr" ? fr : en);

  // Hooks safe : appelés toujours, dans le même ordre. Si window.melr n'est
  // pas prêt, on tombe sur les noop qui retournent un état vide.
  const useKoboFormMappingsSafe = (window.melr && window.melr.useKoboFormMappings)
    || _noopMappingsHook;
  const useIndicatorsSafe = (window.melr && window.melr.useIndicators)
    || _noopIndicatorsHook;

  const { data: mappings, refresh } = useKoboFormMappingsSafe(form.id);
  const indicatorsHook = useIndicatorsSafe(form.linked_project_id || null);
  const indicators = (indicatorsHook && indicatorsHook.data) || [];

  const [fields, setFields] = useStateKO([]);
  const [fieldsLoading, setFieldsLoading] = useStateKO(true);
  const [fieldsErr, setFieldsErr] = useStateKO(null);
  const [busy, setBusy] = useStateKO(null);

  const [draftIndicator, setDraftIndicator] = useStateKO("");
  const [draftField, setDraftField]         = useStateKO("");
  const [draftAgg, setDraftAgg]             = useStateKO("sum");
  const [draftPeriodField, setDraftPeriodField] = useStateKO("");
  const [draftPeriodGran, setDraftPeriodGran]   = useStateKO("month");
  const [draftSiteField, setDraftSiteField] = useStateKO("");
  const [draftFilter, setDraftFilter]       = useStateKO("");

  useEffectKO(function () {
    let cancelled = false;
    if (!window.melr || !window.melr.fetchKoboFormFields) {
      setFieldsLoading(false);
      return function () { cancelled = true; };
    }
    (async function () {
      setFieldsLoading(true); setFieldsErr(null);
      try {
        const r = await window.melr.fetchKoboFormFields(form.id);
        if (cancelled) return;
        const arr = r.fields || [];
        // Debug : expose pour inspection F12 + log clair
        if (window && window.console) {
          console.log("[MELR] Mapping · fetchKoboFormFields → " + arr.length + " field(s)");
          if (arr.length > 0) {
            console.log("[MELR] First field:", JSON.stringify(arr[0]));
          }
          window.__lastKoboFields = arr;
        }
        setFields(arr);
      } catch (e) {
        if (cancelled) return;
        if (window && window.console) console.error("[MELR] fetchKoboFormFields error:", e);
        setFieldsErr(e.message || String(e));
      } finally {
        if (!cancelled) setFieldsLoading(false);
      }
    })();
    return function () { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [form.id]);

  const resetDraft = function () {
    setDraftIndicator(""); setDraftField(""); setDraftAgg("sum");
    setDraftPeriodField(""); setDraftPeriodGran("month");
    setDraftSiteField(""); setDraftFilter("");
  };

  const onAdd = async function () {
    if (!draftIndicator || !draftField) {
      window.alert(T("Sélectionnez un indicateur ET un champ XLSForm.",
                     "Pick both an indicator AND an XLSForm field."));
      return;
    }
    if (!window.melr || !window.melr.koboFormMappingsCrud) {
      window.alert(T("Module non prêt, réessayez dans quelques secondes.",
                     "Module not ready yet, retry in a few seconds."));
      return;
    }
    setBusy("create");
    try {
      await window.melr.koboFormMappingsCrud.create({
        form_id: form.id,
        indicator_id: draftIndicator,
        field_path: draftField.trim(),
        aggregation: draftAgg,
        period_field: draftPeriodField.trim() || null,
        period_granularity: draftPeriodGran,
        site_field: draftSiteField.trim() || null,
        filter_expr: draftFilter.trim() || null,
        enabled: true,
      });
      resetDraft();
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + (e.message || String(e)));
    } finally { setBusy(null); }
  };

  const onToggleMapping = async function (m) {
    if (!window.melr || !window.melr.koboFormMappingsCrud) return;
    setBusy(m.id);
    try {
      await window.melr.koboFormMappingsCrud.update(m.id, { enabled: !m.enabled });
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  const onRemoveMapping = async function (m) {
    if (!window.confirm(T("Supprimer cette règle de mapping ?", "Delete this mapping rule?"))) return;
    if (!window.melr || !window.melr.koboFormMappingsCrud) return;
    setBusy(m.id);
    try {
      await window.melr.koboFormMappingsCrud.remove(m.id);
      await refresh();
    } catch (e) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message);
    } finally { setBusy(null); }
  };

  // L.3b polish · liste de champs prête à afficher dans les <select>.
  // Calculée HORS du JSX pour éviter les soucis du transformer Babel
  // in-browser avec les IIFE imbriquées (v104 avait cassé l'affichage).
  const cleanFields = [];
  const _seenPath = {};
  for (let i = 0; i < fields.length; i++) {
    const f = fields[i] || {};
    const p = (f.path || "").trim();
    if (!p) continue;
    if (_seenPath[p]) continue;
    _seenPath[p] = true;
    cleanFields.push({ path: p, label: f.label || "", type: f.type || "", name: f.name || "" });
  }
  const dateFields = cleanFields.filter(function (f) { return /date|time|datetime/i.test(f.type); });
  const nonDateFields = cleanFields.filter(function (f) { return !/date|time|datetime/i.test(f.type); });

  return (
    <div style={overlayMap} onClick={() => !busy && onClose()}>
      <div style={modalMap} onClick={(e) => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 6 }}>
          <div>
            <h3 style={{ margin: 0, fontSize: 17 }}>
              🔗 {T("Mapping vers les indicateurs MELR", "Mapping to MELR indicators")}
            </h3>
            <div style={{ fontSize: 12.5, color: "#475569", marginTop: 4 }}>
              {T("Formulaire :", "Form:")} <strong>{form.title}</strong>
              {!form.linked_project_id && (
                <span style={{ marginLeft: 8, padding: "1px 6px", background: "#fef3c7", color: "#92400e", borderRadius: 4, fontSize: 10.5, fontWeight: 700 }}>
                  ⚠ {T("Pas lié à un projet", "Not linked to a project")}
                </span>
              )}
            </div>
          </div>
          <button onClick={onClose} style={{ padding: "4px 10px", borderRadius: 6, border: "1px solid #94a3b8", background: "white", color: "#334155", cursor: "pointer", fontSize: 18, lineHeight: 1 }}>×</button>
        </div>

        {!form.linked_project_id && (
          <div style={{ padding: 10, marginBottom: 12, borderRadius: 6, background: "#fef3c7", border: "1px solid #fde68a", color: "#92400e", fontSize: 12 }}>
            {T(
              "Astuce : liez d'abord ce formulaire à un projet MELR (sélecteur dans la liste). Sinon le sélecteur d'indicateur ci-dessous montrera TOUS les indicateurs de l'organisation.",
              "Tip: link this form to a MELR project first (selector in the list). Otherwise the indicator picker below lists ALL indicators of the org."
            )}
          </div>
        )}

        {/* Section champs XLSForm */}
        <div style={{ marginBottom: 16, padding: 10, background: "#f8fafc", border: "1px solid #e2e8f0", borderRadius: 8 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 6 }}>
            {T("Champs du formulaire distant", "Remote form fields")}
          </div>
          {fieldsLoading ? (
            <div style={{ fontSize: 12.5, color: "var(--text-faint)" }}>
              {T("Chargement…", "Loading…")}
            </div>
          ) : fieldsErr ? (
            <div style={{ fontSize: 12.5, color: "#dc2626" }}>
              {T("Erreur :", "Error:")} {fieldsErr}
            </div>
          ) : fields.length === 0 ? (
            <div style={{ fontSize: 12.5, color: "var(--text-faint)" }}>
              {T("Aucun champ trouvé.", "No field found.")}
            </div>
          ) : (
            <div style={{ fontSize: 11.5, color: "#475569", maxHeight: 120, overflowY: "auto" }}>
              {fields.map(function (f) {
                return (
                  <div key={f.path} style={{ padding: "2px 0", borderTop: "1px solid #f1f5f9" }}>
                    <code style={{ background: "white", padding: "1px 5px", borderRadius: 3, border: "1px solid #e2e8f0", marginRight: 6 }}>{f.path}</code>
                    <span style={{ color: "#94a3b8" }}>{f.type}</span>
                    {f.label && <span style={{ marginLeft: 8 }}>— {f.label}</span>}
                  </div>
                );
              })}
            </div>
          )}
        </div>

        {/* Nouvelle règle */}
        <div style={{ marginBottom: 16, padding: 12, background: "#eff6ff", border: "1px solid #bfdbfe", borderRadius: 8 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "#1e40af", marginBottom: 8 }}>
            ➕ {T("Nouvelle règle de mapping", "New mapping rule")}
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 8 }}>
            <div>
              <label style={lblMap}>{T("Indicateur MELR cible", "Target MELR indicator")} *</label>
              <select value={draftIndicator} onChange={(e) => setDraftIndicator(e.target.value)} style={inpMap}>
                <option value="">{T("— Choisir —", "— Select —")}</option>
                {indicators.map(function (i) {
                  return (
                    <option key={i.id} value={i.id}>
                      {i.code ? (i.code + " · ") : ""}{i.name_fr || i.name_en || i.name}
                    </option>
                  );
                })}
              </select>
            </div>
            <div>
              <label style={lblMap}>{T("Champ XLSForm (chemin)", "XLSForm field (path)")} *</label>
              <select value={draftField}
                onChange={(e) => {
                  const v = e.target.value;
                  if (window && window.console) console.log("[MELR] Mapping XLSForm pick:", v);
                  setDraftField(v);
                }}
                style={inpMap}>
                <option value="">{T("— Choisir un champ —", "— Pick a field —")}</option>
                {cleanFields.map(function (f) {
                  return (
                    <option key={f.path} value={f.path}>
                      {f.path}{f.label ? " — " + f.label : ""} ({f.type})
                    </option>
                  );
                })}
              </select>
              {cleanFields.length === 0 && !fieldsLoading && (
                <div style={{ fontSize: 10.5, color: "#dc2626", marginTop: 4 }}>
                  {fields.length === 0
                    ? T(
                        "fetchKoboFormFields a retourné 0 champ — voir la console F12 (erreur Kobo ?).",
                        "fetchKoboFormFields returned 0 field — see F12 console (Kobo error?).",
                      )
                    : T(
                        `Kobo a retourné ${fields.length} champ(s) mais tous avec un path vide. Vérifiez que vos questions Kobo ont un nom (Question name) défini.`,
                        `Kobo returned ${fields.length} field(s) but all with empty path. Check that your Kobo questions have a Name set.`,
                      )}
                </div>
              )}
              {draftField && (
                <div style={{ fontSize: 10.5, color: "#475569", marginTop: 4 }}>
                  {T("Sélectionné :", "Selected:")} <code style={{ background: "#f1f5f9", padding: "1px 5px", borderRadius: 3 }}>{draftField}</code>
                </div>
              )}
            </div>
            <div>
              <label style={lblMap}>{T("Agrégation", "Aggregation")}</label>
              <select value={draftAgg} onChange={(e) => setDraftAgg(e.target.value)} style={inpMap}>
                <option value="sum">{T("Somme", "Sum")}</option>
                <option value="count">{T("Nombre", "Count")}</option>
                <option value="avg">{T("Moyenne", "Average")}</option>
                <option value="min">Min</option>
                <option value="max">Max</option>
                <option value="first">{T("Première", "First")}</option>
                <option value="last">{T("Dernière", "Last")}</option>
              </select>
            </div>
            <div>
              <label style={lblMap}>{T("Granularité période", "Period granularity")}</label>
              <select value={draftPeriodGran} onChange={(e) => setDraftPeriodGran(e.target.value)} style={inpMap}>
                <option value="day">{T("Jour", "Day")}</option>
                <option value="month">{T("Mois", "Month")}</option>
                <option value="quarter">{T("Trimestre", "Quarter")}</option>
                <option value="year">{T("Année", "Year")}</option>
              </select>
            </div>
            <div>
              <label style={lblMap}>{T("Champ date pour la période (optionnel)", "Date field for period (optional)")}</label>
              <select value={draftPeriodField} onChange={(e) => setDraftPeriodField(e.target.value)} style={inpMap}>
                <option value="">{T("— Par défaut : date de soumission —", "— Default: submitted_at —")}</option>
                {dateFields.map(function (f) {
                  return (
                    <option key={"d-" + f.path} value={f.path}>
                      {f.path}{f.label ? " — " + f.label : ""} ({f.type})
                    </option>
                  );
                })}
                {nonDateFields.length > 0 && (
                  <optgroup label={T("Autres champs", "Other fields")}>
                    {nonDateFields.map(function (f) {
                      return (
                        <option key={"o-" + f.path} value={f.path}>
                          {f.path}{f.label ? " — " + f.label : ""} ({f.type})
                        </option>
                      );
                    })}
                  </optgroup>
                )}
              </select>
            </div>
            <div>
              <label style={lblMap}>{T("Champ site (optionnel)", "Site field (optional)")}</label>
              <select value={draftSiteField} onChange={(e) => setDraftSiteField(e.target.value)} style={inpMap}>
                <option value="">{T("— Aucun (pas de site lié) —", "— None (no linked site) —")}</option>
                {cleanFields.map(function (f) {
                  return (
                    <option key={"s-" + f.path} value={f.path}>
                      {f.path}{f.label ? " — " + f.label : ""} ({f.type})
                    </option>
                  );
                })}
              </select>
            </div>
          </div>
          <div style={{ display: "flex", justifyContent: "flex-end" }}>
            <button onClick={onAdd} disabled={busy === "create"}
              style={{
                padding: "8px 14px", borderRadius: 6, border: "1px solid #1d4ed8",
                background: busy === "create" ? "#cbd5e1" : "#2563eb", color: "white",
                cursor: busy === "create" ? "default" : "pointer", fontWeight: 600, fontSize: 13,
              }}>
              {busy === "create" ? T("Ajout…", "Adding…") : T("Ajouter la règle", "Add rule")}
            </button>
          </div>
        </div>

        {/* Règles existantes */}
        <div style={{ marginBottom: 4 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 8 }}>
            {T("Règles existantes", "Existing rules")} ({(mappings || []).length})
          </div>
          {(mappings || []).length === 0 ? (
            <div style={{ padding: 12, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5, background: "#f8fafc", borderRadius: 6 }}>
              {T("Aucune règle pour l'instant.", "No rule yet.")}
            </div>
          ) : (
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12.5 }}>
              <thead>
                <tr style={{ background: "#f8fafc", textAlign: "left" }}>
                  <th style={Object.assign({}, thF, { paddingLeft: 8 })}>{T("Indicateur", "Indicator")}</th>
                  <th style={thF}>{T("Champ", "Field")}</th>
                  <th style={thF}>{T("Agg.", "Agg.")}</th>
                  <th style={thF}>{T("Période", "Period")}</th>
                  <th style={thF}>{T("Site", "Site")}</th>
                  <th style={thF}></th>
                </tr>
              </thead>
              <tbody>
                {mappings.map(function (m) {
                  // PostgREST peut renvoyer l'embed `indicators(...)` soit comme
                  // objet (relation many-to-one) soit comme tableau selon
                  // l'inférence de cardinalité. Fallback aussi sur le lookup
                  // par indicator_id dans la liste des indicateurs locale
                  // (au cas où l'embed serait vide à cause d'un timing RLS).
                  const indRaw = m.indicators;
                  let ind = Array.isArray(indRaw)
                    ? (indRaw[0] || null)
                    : (indRaw || null);
                  if (!ind && m.indicator_id) {
                    ind = (indicators || []).find(function (i) { return i.id === m.indicator_id; }) || null;
                  }
                  ind = ind || {};
                  return (
                    <tr key={m.id} style={{ borderTop: "1px solid #e2e8f0", opacity: m.enabled ? 1 : 0.55 }}>
                      <td style={Object.assign({}, tdF, { paddingLeft: 8 })}>
                        <div style={{ fontWeight: 600 }}>{ind.code || "—"}</div>
                        <div style={{ fontSize: 10.5, color: "var(--text-faint)" }}>{ind.name_fr || ind.name_en || ""}</div>
                      </td>
                      <td style={tdF}>
                        <code style={{ background: "#f1f5f9", padding: "1px 5px", borderRadius: 3, fontSize: 10.5 }}>
                          {m.field_path}
                        </code>
                      </td>
                      <td style={tdF}>{m.aggregation}</td>
                      <td style={tdF}>{m.period_granularity}{m.period_field ? (" (" + m.period_field + ")") : ""}</td>
                      <td style={tdF}>{m.site_field || "—"}</td>
                      <td style={tdF}>
                        <div style={{ display: "flex", gap: 4 }}>
                          <button onClick={() => onToggleMapping(m)} disabled={busy === m.id}
                            style={btnTiny(m.enabled ? "white" : "#dcfce7", m.enabled ? "#475569" : "#166534", "#cbd5e1")}>
                            {m.enabled ? T("Actif", "On") : "Off"}
                          </button>
                          <button onClick={() => onRemoveMapping(m)} disabled={busy === m.id}
                            style={btnTiny("white", "#dc2626", "#fca5a5")}>
                            {T("Suppr.", "Del.")}
                          </button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>

        <div style={{ marginTop: 14, display: "flex", justifyContent: "flex-end" }}>
          <button onClick={onClose}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid #94a3b8", background: "white", color: "#334155", cursor: "pointer", fontSize: 13 }}>
            {T("Fermer", "Close")}
          </button>
        </div>
      </div>
    </div>
  );
}

const overlayMap = { position: "fixed", inset: 0, background: "rgba(15,23,42,0.55)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 20 };
const modalMap = { background: "white", borderRadius: 10, maxWidth: 900, width: "100%", maxHeight: "90vh", overflow: "auto", padding: 24, boxShadow: "0 20px 50px rgba(0,0,0,0.25)" };
const lblMap = { display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4, color: "#334155" };
const inpMap = { width: "100%", boxSizing: "border-box", padding: 8, borderRadius: 6, border: "1px solid #cbd5e1", fontSize: 13, fontFamily: "inherit" };

window.KoboOdkScreen = KoboOdkScreen;
