/* global React, Icon, Modal */
const { useState: useStateFB, useEffect: useEffectFB, useMemo: useMemoFB } = React;

// ============================================================================
// FORM BUILDER — admin tool to create / edit forms in the database without SQL
// ----------------------------------------------------------------------------
// Two surfaces:
//   • List view (default)   — every form in the org with code / project /
//                             version / active toggle / restrictions count.
//   • Editor (slide-over)   — title metadata + drag-orderable schema fields
//                             + optional restricted-agent multi-select.
//
// Admin-only — the page is hidden in the sidebar nav unless the user has
// the users.manage permission. The underlying RLS in form-agents.sql
// rejects writes from non-admins anyway.
// ============================================================================

// Each entry can carry a short help line shown under the row to document what
// the field becomes at saisie time. Helps the form designer pick the right type.
const FIELD_TYPES = [
  { v: "text",      fr: "Texte libre",                 en: "Free text",
    helpFr: "Champ texte libre (single line).", helpEn: "Free single-line text input." },
  { v: "number",    fr: "Nombre",                      en: "Number",
    helpFr: "Saisie numérique (entier ou décimal).", helpEn: "Numeric input." },
  { v: "date",      fr: "Date",                        en: "Date",
    helpFr: "Sélecteur de date.", helpEn: "Date picker." },
  { v: "bool",      fr: "Oui / Non",                   en: "Yes / No",
    helpFr: "Case à cocher.", helpEn: "Checkbox." },
  { v: "select",    fr: "Liste déroulante",            en: "Dropdown",
    helpFr: "Dropdown avec options personnalisées.", helpEn: "Dropdown with custom options." },
  { v: "matrix",    fr: "Matrice (texte)",             en: "Matrix (text)",
    helpFr: "Tableau de saisie texte (lignes × colonnes).", helpEn: "Text-input grid (rows × columns)." },
  { v: "photo",     fr: "Photo (appareil)",            en: "Photo (camera)",
    helpFr: "Capture photo depuis l'appareil terrain.", helpEn: "Take a photo on device." },
  { v: "signature", fr: "Signature manuscrite",        en: "Hand signature",
    helpFr: "Zone de dessin pour signature.", helpEn: "Hand-drawn signature pad." },
  // ── Indicator picker (single) — picks ONE indicator and expands it
  //    into a disaggregation grid at data-entry time (Pass 2 - 2026-05).
  { v: "indicator", fr: "Indicateur (1 avec désagrégation)", en: "Indicator (1 with disaggregation)",
    helpFr: "Un seul indicateur. À la saisie, expand en grille N/D selon les axes de désagrégation.",
    helpEn: "Single indicator. At entry time, expands to N/D grid per axis." },
  // ── E2 (2026-05) · standard activity-form building blocks ─────────────
  { v: "project",         fr: "Projet (sélecteur)",      en: "Project (picker)",
    helpFr: "Dropdown des projets de l'organisation. À placer en première ligne.",
    helpEn: "Dropdown of org projects. Should be the first field." },
  { v: "activity",        fr: "Activité (sélecteur)",    en: "Activity (picker)",
    helpFr: "Dropdown des activités du projet sélectionné dans le formulaire.",
    helpEn: "Dropdown of activities for the project picked in the form." },
  { v: "gps",             fr: "Coordonnées GPS",         en: "GPS coordinates",
    helpFr: "Latitude + longitude + bouton de géolocalisation appareil.",
    helpEn: "Latitude + longitude + device-GPS button." },
  { v: "partners_multi",  fr: "Partenaires (multi-sélect)", en: "Partners (multi-select)",
    helpFr: "Liste de partenaires de mise en œuvre, sélection multiple.",
    helpEn: "Implementation partners, multi-select." },
  { v: "indicators_multi",fr: "Indicateurs (multi-sélect)", en: "Indicators (multi-select)",
    helpFr: "Plusieurs indicateurs du projet — cases à cocher avec recherche.",
    helpEn: "Multiple indicators of the project — searchable checklist." },
  { v: "attachments",     fr: "Pièces jointes (multiple)", en: "Attachments (multiple)",
    helpFr: "Téléversement de plusieurs fichiers (TDRs, présences, photos, rapports…).",
    helpEn: "Upload multiple files (TDRs, attendance, photos, reports…)." },
  { v: "activity_status", fr: "Statut activité",         en: "Activity status",
    helpFr: "Dropdown standardisé : Non démarrée · En cours · Terminée · Reportée · Annulée.",
    helpEn: "Standard dropdown: Not started · Ongoing · Completed · Postponed · Cancelled." },
];

// Suggested "Activity header" template — when the user clicks "Modèle activité",
// these 5 fields are inserted (or merged into) the schema in this order.
// PROJECT must always come first per the E2 spec.
const ACTIVITY_HEADER_TEMPLATE = [
  { k: "project",       l: "Projet",            type: "project"  },
  { k: "activity",      l: "Activité",          type: "activity" },
  { k: "date",          l: "Date",              type: "date"     },
  { k: "gps_coords",    l: "Coordonnées GPS",   type: "gps"      },
  { k: "description",   l: "Description",       type: "text"     },
];

function FormBuilder({ t, lang }) {
  const { has, loading: permsLoading } = window.melr.useCurrentUserPermissions();
  const { data: forms, loading: formsLoading, realtime } = window.melr.useForms();
  const { projects, loading: projectsLoading } = window.melr.useProjects();
  const LiveBadge = window.melr.LiveBadge;

  // Restriction counts per form_id — one query for the org's form_agents
  // (RLS limits admins to their org via form_org()). Polled when forms
  // change so the badge stays fresh after the editor closes.
  const [restrictionsByForm, setRestrictionsByForm] = useStateFB({});
  useEffectFB(() => {
    let alive = true;
    (async () => {
      const sb = window.melr.supabase;
      if (!sb) return;
      const r = await sb.from("form_agents").select("form_id");
      if (!alive || r.error) return;
      const counts = {};
      (r.data || []).forEach((row) => { counts[row.form_id] = (counts[row.form_id] || 0) + 1; });
      setRestrictionsByForm(counts);
    })();
    return () => { alive = false; };
  }, [forms]);
  const [editingId, setEditingId] = useStateFB(null);     // form UUID or "new"
  const [filter, setFilter]       = useStateFB("");        // project code or ""
  const [busyId, setBusyId]       = useStateFB(null);
  const [exportingForm, setExportingForm] = useStateFB(null); // form row being exported

  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" ? "ADMINISTRATION" : "ADMINISTRATION"}</div>
          <h1 className="page-title">{lang === "fr" ? "Constructeur de formulaires" : "Form builder"}</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 onToggleActive = async (form) => {
    setBusyId(form.id);
    try { await window.melr.formsCrud.setActive(form.id, !form.active); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusyId(null); }
  };
  const onDelete = async (form) => {
    if (!window.confirm(lang === "fr"
      ? "Supprimer le formulaire « " + (form.name_fr || form.code) + " » ? Les soumissions existantes seront conservées mais orphelines."
      : "Delete form '" + (form.name_en || form.name_fr || form.code) + "'? Existing submissions are kept but orphaned.")) return;
    setBusyId(form.id);
    try { await window.melr.formsCrud.remove(form.id); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusyId(null); }
  };

  const visible = (forms || []).filter((f) => !filter || (f.projects && f.projects.code === filter));
  const projectsFromForms = [...new Set((forms || []).map((f) => f.projects && f.projects.code).filter(Boolean))].sort();

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "ADMINISTRATION" : "ADMINISTRATION"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {lang === "fr" ? "Constructeur de formulaires" : "Form builder"}{" "}
              <LiveBadge on={realtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {lang === "fr"
                ? "Créer, modifier, désactiver les formulaires utilisés par la collecte de données mobile."
                : "Create, edit, disable the forms used by mobile data collection."}
            </div>
          </div>
          <div className="page-header-actions">
            <button className="btn sm primary" onClick={() => setEditingId("new")}>
              <Icon.plus /> {lang === "fr" ? "Nouveau formulaire" : "New form"}
            </button>
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">
            {lang === "fr" ? "Formulaires" : "Forms"}{" "}
            <span className="tag-mono" style={{ marginLeft: 6 }}>{visible.length} / {(forms || []).length}</span>
          </div>
          <div style={{ flex: 1 }} />
          {projectsFromForms.length > 1 && (
            <select value={filter} onChange={(e) => setFilter(e.target.value)}
              style={{ padding: "4px 8px", fontSize: 12, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg, white)", color: "var(--text)" }}>
              <option value="">{lang === "fr" ? "Tous les projets" : "All projects"}</option>
              {projectsFromForms.map((c) => <option key={c} value={c}>{c}</option>)}
            </select>
          )}
        </div>
        <div className="card-body flush">
          {formsLoading ? (
            <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"
                ? "Aucun formulaire pour le moment. Cliquez « Nouveau formulaire » pour démarrer."
                : "No form yet. Click 'New form' to start."}
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>{lang === "fr" ? "Code" : "Code"}</th>
                  <th>{lang === "fr" ? "Nom" : "Name"}</th>
                  <th>{lang === "fr" ? "Projet" : "Project"}</th>
                  <th>{lang === "fr" ? "Version" : "Version"}</th>
                  <th>{lang === "fr" ? "Champs" : "Fields"}</th>
                  <th>{lang === "fr" ? "Restriction" : "Restriction"}</th>
                  <th>{lang === "fr" ? "Statut" : "State"}</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {visible.map((f) => {
                  const fieldCount = (f.schema && Array.isArray(f.schema.fields)) ? f.schema.fields.length : 0;
                  const restrictionCount = restrictionsByForm[f.id] || 0;
                  return (
                    <tr key={f.id}>
                      <td className="mono">{f.code}</td>
                      <td>{lang === "en" ? (f.name_en || f.name_fr) : f.name_fr}</td>
                      <td className="mono text-faint">{f.projects ? f.projects.code : "—"}</td>
                      <td className="mono text-faint">{f.version || "—"}</td>
                      <td className="num mono">{fieldCount}</td>
                      <td>
                        {restrictionCount > 0 ? (
                          <span className="pill green dot" style={{ fontSize: 10 }}
                            title={lang === "fr" ? (restrictionCount + " agent(s) autorisé(s)") : (restrictionCount + " allowed agent(s)")}>
                            🔒 {restrictionCount}
                          </span>
                        ) : (
                          <span className="text-faint" style={{ fontSize: 10.5 }}
                            title={lang === "fr" ? "Aucune restriction — tous les agents du projet" : "No restriction — all project agents"}>
                            {lang === "fr" ? "Tous" : "All"}
                          </span>
                        )}
                      </td>
                      <td>
                        <button className={"pill " + (f.active ? "green dot" : "")}
                          style={{ cursor: "pointer", border: 0 }}
                          disabled={busyId === f.id}
                          onClick={() => onToggleActive(f)}
                          title={lang === "fr" ? "Basculer actif/inactif" : "Toggle active/inactive"}>
                          {f.active ? (lang === "fr" ? "Actif" : "Active") : (lang === "fr" ? "Inactif" : "Inactive")}
                        </button>
                      </td>
                      <td>
                        <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                          <button className="btn xs" onClick={() => setExportingForm(f)}
                            title={lang === "fr" ? "Exporter les saisies en Excel" : "Export submissions to Excel"}>
                            <Icon.download /> {lang === "fr" ? "Exporter" : "Export"}
                          </button>
                          <button className="btn xs" onClick={() => setEditingId(f.id)}>
                            <Icon.edit /> {lang === "fr" ? "Modifier" : "Edit"}
                          </button>
                          <button className="btn xs ghost" onClick={() => onDelete(f)} disabled={busyId === f.id}
                            title={lang === "fr" ? "Supprimer" : "Delete"}>
                            <Icon.x />
                          </button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {editingId && (
        <FormEditor
          lang={lang}
          formId={editingId === "new" ? null : editingId}
          existing={editingId === "new" ? null : (forms || []).find((f) => f.id === editingId)}
          projects={projects || []}
          projectsLoading={projectsLoading}
          onClose={() => setEditingId(null)}
        />
      )}

      {exportingForm && (
        <ExportModal
          lang={lang}
          form={exportingForm}
          onClose={() => setExportingForm(null)}
        />
      )}
    </div>
  );
}

// ── Export modal — date range + attachment URLs toggle, then writes .xlsx ──
function ExportModal({ lang, form, onClose }) {
  const [startDate, setStartDate] = useStateFB("");
  const [endDate, setEndDate]     = useStateFB("");
  const [includeAttachments, setIncludeAttachments] = useStateFB(true);
  const [busy, setBusy]           = useStateFB(false);
  const [done, setDone]           = useStateFB(null);
  const [err, setErr]             = useStateFB(null);

  const onExport = async () => {
    setBusy(true); setErr(null); setDone(null);
    try {
      const r = await window.melr.exportFormSubmissions(form.id, {
        startDate: startDate || null,
        endDate:   endDate || null,
        includeAttachments,
      });
      setDone(r);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

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

  return (
    <Modal
      size="sm"
      title={<>{lang === "fr" ? "Exporter les saisies" : "Export submissions"}
        <span className="text-faint mono" style={{ marginLeft: 6, fontSize: 12 }}>{form.code}</span>
      </>}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {done ? (lang === "fr" ? "Fermer" : "Close") : (lang === "fr" ? "Annuler" : "Cancel")}
        </button>
        <button className="btn sm primary" onClick={onExport} disabled={busy}>
          {busy ? "…" : (lang === "fr" ? "Exporter" : "Export")}
        </button>
      </>}>
      <div>
        <div className="text-faint" style={{ fontSize: 12, marginBottom: 14 }}>
          {lang === "fr"
            ? "Génère un fichier Excel avec une colonne par champ du schéma. Une feuille « Pièces jointes » contient les URLs signées (1h) pour les photos et signatures."
            : "Produces an Excel file with one column per schema field. The 'Attachments' sheet has signed URLs (1h) for photos and signatures."}
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <div>
            <label style={lbl}>{lang === "fr" ? "Du" : "From"}</label>
            <input type="date" style={inp} value={startDate} onChange={(e) => setStartDate(e.target.value)} />
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Au" : "To"}</label>
            <input type="date" style={inp} value={endDate} onChange={(e) => setEndDate(e.target.value)} />
          </div>
        </div>
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
          {lang === "fr"
            ? "Laisser vide pour exporter toutes les saisies."
            : "Leave blank to export all submissions."}
        </div>

        <label style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" }}>
          <input type="checkbox" checked={includeAttachments} onChange={(e) => setIncludeAttachments(e.target.checked)} />
          <span>
            {lang === "fr" ? "Inclure les URLs des pièces jointes (1h)" : "Include attachment URLs (1h)"}
          </span>
        </label>

        {done && (
          <div style={{ marginTop: 14, padding: "10px 12px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>
            {lang === "fr"
              ? "✓ Fichier généré : " + done.filename + " · " + done.count + " saisie(s)" + (done.attachments ? ", " + done.attachments + " pièce(s) jointe(s)" : "")
              : "✓ File generated: " + done.filename + " · " + done.count + " submission(s)" + (done.attachments ? ", " + done.attachments + " attachment(s)" : "")}
          </div>
        )}
        {err && (
          <div style={{ marginTop: 14, padding: "10px 12px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>
            {err}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ── Editor ───────────────────────────────────────────────────────────────────

function FormEditor({ lang, formId, existing, projects, projectsLoading, onClose }) {
  const isNew = !formId;
  const [code, setCode]       = useStateFB(existing ? existing.code     : "");
  const [nameFr, setNameFr]   = useStateFB(existing ? existing.name_fr  : "");
  const [nameEn, setNameEn]   = useStateFB(existing ? (existing.name_en || "") : "");
  const [version, setVersion] = useStateFB(existing ? (existing.version || "v1") : "v1");
  const [active, setActive]   = useStateFB(existing ? existing.active   : true);
  const [projectId, setProjectId] = useStateFB(existing ? (existing.project_id || "") : "");
  const [fields, setFields]   = useStateFB(() => {
    if (existing && existing.schema && Array.isArray(existing.schema.fields)) return existing.schema.fields.map((f) => ({ ...f }));
    return [];
  });
  const [busy, setBusy] = useStateFB(false);
  const [err, setErr]   = useStateFB(null);

  // Per-form agent restriction (form_agents). Empty list → no restriction
  // (all project agents). Non-empty → only these agents can use the form.
  const { members } = window.melr.useOrgMembers();
  const { data: formAgents, refresh: refreshFormAgents } = window.melr.useFormAgents(formId);
  const [restrictedAgentIds, setRestrictedAgentIds] = useStateFB([]);
  useEffectFB(() => {
    setRestrictedAgentIds((formAgents || []).map((fa) => fa.agent_id));
  }, [formAgents]);

  const onAddField = () => {
    setFields((arr) => [
      ...arr,
      { k: "field_" + (arr.length + 1), l: "", type: "text" },
    ]);
  };
  const onRemoveField = (idx) => setFields((arr) => arr.filter((_, i) => i !== idx));
  const onMoveField   = (idx, delta) => {
    setFields((arr) => {
      const next = arr.slice();
      const newIdx = idx + delta;
      if (newIdx < 0 || newIdx >= next.length) return next;
      const [item] = next.splice(idx, 1);
      next.splice(newIdx, 0, item);
      return next;
    });
  };
  const onPatchField = (idx, patch) => {
    setFields((arr) => arr.map((f, i) => i === idx ? { ...f, ...patch } : f));
  };

  const validate = () => {
    if (!code.trim())   return lang === "fr" ? "Le code est requis." : "Code is required.";
    if (!nameFr.trim()) return lang === "fr" ? "Le nom (français) est requis." : "Name (French) is required.";
    // Field keys must be unique and snake_case-friendly
    const keys = new Set();
    for (const f of fields) {
      if (!f.k || !f.k.trim()) return lang === "fr" ? "Chaque champ doit avoir une clé." : "Every field needs a key.";
      if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(f.k)) {
        return (lang === "fr" ? "Clé invalide : « " : "Invalid key: '") + f.k + (lang === "fr" ? " » (lettres, chiffres, underscore)" : "' (letters, digits, underscore)");
      }
      if (keys.has(f.k)) return (lang === "fr" ? "Clé en double : " : "Duplicate key: ") + f.k;
      keys.add(f.k);
      if (f.type === "select") {
        if (!Array.isArray(f.options) || f.options.length === 0) {
          return (lang === "fr" ? "Le champ « " : "Field '") + f.k + (lang === "fr" ? " » (select) doit avoir au moins une option." : "' (select) needs at least one option.");
        }
      }
    }
    return null;
  };

  const onSave = async () => {
    const v = validate();
    if (v) { setErr(v); return; }
    setBusy(true); setErr(null);
    try {
      const payload = {
        project_id: projectId || null,
        code: code.trim(),
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
        version: version.trim() || "v1",
        schema: { fields },
        active: !!active,
      };
      let saved;
      if (isNew) {
        saved = await window.melr.formsCrud.create(payload);
      } else {
        saved = await window.melr.formsCrud.update(formId, payload);
      }
      // Save the agent restriction list against the (now persisted) form id
      await window.melr.formAgentsCrud.setAll(saved.id, restrictedAgentIds);
      await refreshFormAgents();
      onClose();
    } catch (e) { setErr(e.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={isNew
        ? (lang === "fr" ? "Nouveau formulaire" : "New form")
        : ((lang === "en" ? (existing && existing.name_en) || (existing && existing.name_fr) : (existing && existing.name_fr)) || (lang === "fr" ? "Modifier" : "Edit"))}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button className="btn sm primary" onClick={onSave} disabled={busy}>
          {busy ? "…" : (isNew ? (lang === "fr" ? "Créer" : "Create") : (lang === "fr" ? "Enregistrer" : "Save"))}
        </button>
      </>}>
      <div>
          {/* Metadata */}
          <div style={{ display: "grid", gridTemplateColumns: "120px 1fr 1fr 80px", gap: 10, alignItems: "end" }}>
            <div>
              <label style={lbl}>{lang === "fr" ? "Code *" : "Code *"}</label>
              <input style={inp} value={code} onChange={(e) => setCode(e.target.value)} placeholder="VISITE-01" maxLength={32} />
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Nom français *" : "French name *"}</label>
              <input style={inp} value={nameFr} onChange={(e) => setNameFr(e.target.value)} placeholder={lang === "fr" ? "Visite mensuelle" : "Monthly visit"} />
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Nom anglais" : "English name"}</label>
              <input style={inp} value={nameEn} onChange={(e) => setNameEn(e.target.value)} placeholder="Monthly visit" />
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Version" : "Version"}</label>
              <input style={inp} value={version} onChange={(e) => setVersion(e.target.value)} placeholder="v1" />
            </div>
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 10, alignItems: "end", marginTop: 10 }}>
            <div>
              <label style={lbl}>{lang === "fr" ? "Projet" : "Project"}</label>
              <select style={inp} value={projectId} onChange={(e) => setProjectId(e.target.value)} disabled={projectsLoading}>
                <option value="">{lang === "fr" ? "— Aucun projet (formulaire org-wide) —" : "— No project (org-wide form) —"}</option>
                {(projects || []).map((p) => (
                  <option key={p.uuid} value={p.uuid}>{p.id} · {lang === "en" ? (p.nameEn || p.nameFr) : p.nameFr}</option>
                ))}
              </select>
            </div>
            <div>
              <label style={lbl}>{lang === "fr" ? "Actif" : "Active"}</label>
              <label style={{ display: "flex", alignItems: "center", gap: 6, padding: "8px 0" }}>
                <input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
                <span style={{ fontSize: 12 }}>{active ? (lang === "fr" ? "Visible sur mobile" : "Visible on mobile") : (lang === "fr" ? "Masqué" : "Hidden")}</span>
              </label>
            </div>
          </div>

          {/* Schema fields */}
          <div className="card-head" style={{ marginTop: 18, marginBottom: 8, padding: 0, borderBottom: "1px solid var(--line)", paddingBottom: 6 }}>
            <div className="card-title" style={{ fontSize: 13 }}>{lang === "fr" ? "Champs du formulaire" : "Form fields"}</div>
            <span className="tag-mono" style={{ marginLeft: 6 }}>{fields.length}</span>
            <div style={{ flex: 1 }} />
            {/* "Modèle activité" shortcut — adds the standard 5-field activity
                header (Projet + Activité + Date + GPS + Description) if none
                of those keys are present yet. Idempotent: existing keys are
                kept, missing ones are appended in the canonical order. */}
            <button className="btn sm ghost"
              onClick={() => {
                const existingKeys = new Set(fields.map((f) => f.k));
                const toAdd = ACTIVITY_HEADER_TEMPLATE.filter((t) => !existingKeys.has(t.k));
                if (toAdd.length === 0) return;
                // Prepend the template fields so PROJECT lands at position 0
                // (per E2 spec "le champ PROJET en première ligne toujours"),
                // unless the user already has them in some other order.
                setFields((arr) => [...toAdd, ...arr]);
              }}
              title={lang === "fr"
                ? "Insère l'en-tête standard : Projet + Activité + Date + GPS + Description"
                : "Insert the standard header: Project + Activity + Date + GPS + Description"}>
              🚀 {lang === "fr" ? "Modèle activité" : "Activity template"}
            </button>
            <button className="btn sm" onClick={onAddField}><Icon.plus /> {lang === "fr" ? "Ajouter un champ" : "Add field"}</button>
          </div>

          {fields.length === 0 && (
            <div className="text-faint" style={{ padding: 14, textAlign: "center", fontSize: 12.5, border: "1px dashed var(--line)", borderRadius: 6 }}>
              {lang === "fr"
                ? "Aucun champ. Cliquer « 🚀 Modèle activité » pour un en-tête standard, ou « Ajouter un champ » pour partir de zéro."
                : "No field yet. Click '🚀 Activity template' for a standard header, or 'Add field' to start from scratch."}
            </div>
          )}

          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {fields.map((f, idx) => <FieldRow key={idx} idx={idx} field={f} lang={lang}
              onPatch={(p) => onPatchField(idx, p)} onRemove={() => onRemoveField(idx)}
              onUp={() => onMoveField(idx, -1)} onDown={() => onMoveField(idx, +1)}
              first={idx === 0} last={idx === fields.length - 1}
            />)}
          </div>

          {/* Agent restriction */}
          {!isNew && (
            <>
              <div className="card-head" style={{ marginTop: 18, marginBottom: 8, padding: 0, borderBottom: "1px solid var(--line)", paddingBottom: 6 }}>
                <div className="card-title" style={{ fontSize: 13 }}>
                  {lang === "fr" ? "Restriction par agent (optionnel)" : "Restrict to specific agents (optional)"}
                </div>
                <span className="tag-mono" style={{ marginLeft: 6 }}>{restrictedAgentIds.length}</span>
              </div>
              <div style={{
                padding: "8px 10px", borderRadius: 6, fontSize: 11.5, marginBottom: 8,
                background: restrictedAgentIds.length === 0 ? "#fef3c7" : "#dcfce7",
                color:      restrictedAgentIds.length === 0 ? "#92400e" : "#166534",
                border: "1px solid " + (restrictedAgentIds.length === 0 ? "#fcd34d" : "#86efac"),
              }}>
                {restrictedAgentIds.length === 0 ? (
                  lang === "fr"
                    ? "⚠ Aucune restriction active. Tous les agents affectés au projet de ce formulaire pourront le voir et le remplir sur mobile."
                    : "⚠ No restriction active. Every agent assigned to this form's project will see and submit it on mobile."
                ) : (
                  lang === "fr"
                    ? "🔒 Restriction active : seuls les " + restrictedAgentIds.length + " agent(s) sélectionné(s) ci-dessous (et les admins) verront ce formulaire sur mobile."
                    : "🔒 Restriction active: only the " + restrictedAgentIds.length + " selected agent(s) below (and admins) will see this form on mobile."
                )}
              </div>
              <div className="text-faint" style={{ fontSize: 11, marginBottom: 8 }}>
                {lang === "fr"
                  ? "Cliquez sur un agent pour l'ajouter / le retirer de la liste, puis « Enregistrer »."
                  : "Click an agent to toggle them in / out of the list, then 'Save'."}
              </div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                {(members || []).map((m) => {
                  const on = restrictedAgentIds.includes(m.id);
                  return (
                    <button key={m.id}
                      onClick={() => {
                        setRestrictedAgentIds((arr) => on ? arr.filter((x) => x !== m.id) : [...arr, m.id]);
                      }}
                      className={"pill " + (on ? "green dot" : "")}
                      style={{ cursor: "pointer", border: on ? 0 : "1px solid var(--line)", background: on ? "" : "var(--bg, white)" }}>
                      {m.full_name || m.email}
                    </button>
                  );
                })}
                {(members || []).length === 0 && (
                  <div className="text-faint" style={{ fontSize: 12 }}>
                    {lang === "fr" ? "Aucun membre dans l'org." : "No member in the org."}
                  </div>
                )}
              </div>
            </>
          )}

          {isNew && (
            <div className="text-faint" style={{ fontSize: 11.5, marginTop: 14 }}>
              {lang === "fr"
                ? "💡 La restriction par agent sera disponible après la création du formulaire."
                : "💡 Per-agent restriction becomes available after the form is created."}
            </div>
          )}

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

// One row in the field-editor list
function FieldRow({ idx, field, lang, onPatch, onRemove, onUp, onDown, first, last }) {
  const inp = { width: "100%", padding: "5px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  // Type metadata for the help line (and any future type-specific hints)
  const typeMeta = FIELD_TYPES.find((tt) => tt.v === (field.type || "text"));
  const help = typeMeta && (lang === "fr" ? typeMeta.helpFr : typeMeta.helpEn);
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: 6, padding: 10, background: "var(--bg-elev, white)" }}>
      <div style={{ display: "grid", gridTemplateColumns: "30px 1fr 1fr 1fr auto", gap: 8, alignItems: "center" }}>
        <div className="mono text-faint" style={{ fontSize: 11, textAlign: "center" }}>#{idx + 1}</div>
        <div>
          <input style={inp} value={field.k || ""} onChange={(e) => onPatch({ k: e.target.value })}
            placeholder={lang === "fr" ? "clé (ex: date_visit)" : "key (e.g. date_visit)"} />
        </div>
        <div>
          <input style={inp} value={field.l || ""} onChange={(e) => onPatch({ l: e.target.value })}
            placeholder={lang === "fr" ? "Libellé affiché" : "Display label"} />
        </div>
        <div>
          <select style={inp} value={field.type || "text"} onChange={(e) => onPatch({ type: e.target.value })}>
            {FIELD_TYPES.map((t) => <option key={t.v} value={t.v}>{lang === "fr" ? t.fr : t.en}</option>)}
          </select>
        </div>
        <div className="row gap-xs">
          <button className="btn xs ghost" onClick={onUp} disabled={first} title={lang === "fr" ? "Monter" : "Move up"}>↑</button>
          <button className="btn xs ghost" onClick={onDown} disabled={last} title={lang === "fr" ? "Descendre" : "Move down"}>↓</button>
          <button className="btn xs ghost" onClick={onRemove} title={lang === "fr" ? "Supprimer" : "Remove"}><Icon.x /></button>
        </div>
      </div>
      {/* One-liner describing what this field type becomes at saisie time —
          documentation for the form designer, not the agent. */}
      {help && (
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4, paddingLeft: 38 }}>
          {help}
        </div>
      )}
      {field.type === "select" && (
        <OptionsEditor
          lang={lang}
          options={Array.isArray(field.options) ? field.options : []}
          onChange={(opts) => onPatch({ options: opts })}
        />
      )}
      {field.type === "indicator" && (
        <IndicatorFieldEditor
          lang={lang}
          indicatorId={field.indicator_id || ""}
          onChange={(id) => onPatch({ indicator_id: id })}
        />
      )}
      {/* The new E2 field types (project, activity, gps, partners_multi,
          indicators_multi, attachments, activity_status) don't need any
          extra per-field config — they're rendered with sensible defaults
          at saisie time. Just the help line above is enough. */}
    </div>
  );
}

// ── Indicator picker for the 'indicator' field type ────────────────────────
// Lets the form designer choose ONE indicator from the catalogue.  Below the
// picker we preview the disaggregation axes that will apply at saisie time
// so the designer can confirm the form will generate the right grid.
function IndicatorFieldEditor({ lang, indicatorId, onChange }) {
  // Live indicators across all projects the caller can see.
  const { data: indicators } = window.melr.useIndicators ? window.melr.useIndicators() : { data: [] };
  // Once an indicator is picked, fetch its definition + disaggregation
  // assignments so we can show the preview chips.
  const picked = (indicators || []).find((i) => i.id === indicatorId);
  const defCode = picked && (picked.definition_code || picked.defCode);
  const { data: definition } = window.melr.useDefinitionByCode(defCode);
  const { data: assigned, loading: axesLoading } = window.melr.useDefinitionDisaggregations(definition && definition.id);

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

  return (
    <div style={{ marginTop: 8, padding: 10, borderRadius: 6, background: "var(--bg-sunken, #f8fafc)", border: "1px dashed var(--line)" }}>
      <div style={{ fontSize: 10.5, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600, marginBottom: 5 }}>
        {lang === "fr" ? "Indicateur lie a ce champ" : "Indicator linked to this field"}
      </div>
      <select value={indicatorId} onChange={(e) => onChange(e.target.value)} style={sel}>
        <option value="">{lang === "fr" ? "-- Choisir un indicateur --" : "-- Pick an indicator --"}</option>
        {(indicators || []).map((ind) => (
          <option key={ind.id} value={ind.id}>
            {ind.code} - {lang === "en" ? (ind.name_en || ind.name_fr) : ind.name_fr}
          </option>
        ))}
      </select>

      {/* Disaggregation preview */}
      {indicatorId && (
        <div style={{ marginTop: 8 }}>
          {axesLoading ? (
            <div className="text-faint" style={{ fontSize: 11 }}>
              {lang === "fr" ? "Chargement de la desagregation..." : "Loading disaggregation..."}
            </div>
          ) : !assigned || assigned.length === 0 ? (
            <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic" }}>
              {lang === "fr"
                ? "Aucune desagregation configuree pour cet indicateur. Le champ collectera une seule valeur."
                : "No disaggregation configured for this indicator. The field will collect a single value."}
            </div>
          ) : (
            <div>
              <div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 4 }}>
                {lang === "fr" ? "Grille de saisie generee :" : "Generated entry grid:"}
              </div>
              <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
                {assigned.map((row, i) => {
                  const lbl = lang === "fr"
                    ? (row.axis && (row.axis.name_fr || row.axis.code))
                    : (row.axis && (row.axis.name_en || row.axis.name_fr || row.axis.code));
                  return (
                    <span key={i} style={{
                      fontSize: 11, fontWeight: 500,
                      padding: "2px 8px", borderRadius: 999,
                      background: "white", color: "oklch(0.36 0.10 195)",
                      border: "1px solid oklch(0.75 0.10 195)",
                    }}>{lbl}</span>
                  );
                })}
              </div>
              <div style={{ fontSize: 10.5, color: "var(--text-faint)", marginTop: 4, fontStyle: "italic" }}>
                {lang === "fr"
                  ? "Lors de la saisie du formulaire, l'agent verra une grille N/D croisant ces axes."
                  : "When the form is filled, the agent will see an N/D grid crossing these axes."}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ─── Visual options editor for select fields ────────────────────────────
// Row-based: each option has its own value + label inputs, reorder arrows
// and delete. "Auto" mode: if the user only fills the label, the value is
// auto-slugified from it so the saved JSON is clean.
function OptionsEditor({ lang, options, onChange }) {
  const slug = (s) => (s || "").toString().toLowerCase()
    .normalize("NFD").replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9_]+/g, "_")
    .replace(/^_+|_+$/g, "").slice(0, 32);

  const setAt = (idx, patch) => {
    const next = options.map((o, i) => i === idx ? { ...o, ...patch } : o);
    onChange(next);
  };
  const onLabelChange = (idx, label) => {
    const cur = options[idx] || {};
    // If the value was empty OR was the auto-slug of the previous label,
    // keep it in sync with the new label. Otherwise leave the value alone.
    const prevAuto = slug(cur.label);
    const valueIsAuto = !cur.value || cur.value === prevAuto;
    setAt(idx, { label, value: valueIsAuto ? slug(label) : cur.value });
  };
  const onValueChange = (idx, value) => setAt(idx, { value });
  const onAdd = () => onChange([...options, { value: "", label: "" }]);
  const onRemove = (idx) => onChange(options.filter((_, i) => i !== idx));
  const onMove = (idx, delta) => {
    const next = options.slice();
    const newIdx = idx + delta;
    if (newIdx < 0 || newIdx >= next.length) return;
    const [item] = next.splice(idx, 1);
    next.splice(newIdx, 0, item);
    onChange(next);
  };

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

  return (
    <div style={{ marginTop: 8, padding: 8, background: "var(--bg-sunken, #f9fafb)", borderRadius: 5 }}>
      <div className="row" style={{ marginBottom: 6, alignItems: "center" }}>
        <span className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.04em" }}>
          {lang === "fr" ? "Options du menu déroulant" : "Dropdown options"}
        </span>
        <span className="tag-mono" style={{ marginLeft: 6, fontSize: 10 }}>{options.length}</span>
        <div style={{ flex: 1 }} />
        <button className="btn xs" onClick={onAdd}><Icon.plus /> {lang === "fr" ? "Option" : "Option"}</button>
      </div>
      {options.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic", padding: "6px 4px" }}>
          {lang === "fr"
            ? "Aucune option. Cliquez « + Option » pour en ajouter une."
            : "No option yet. Click '+ Option' to add one."}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
          {/* Column headers */}
          <div className="text-faint" style={{ display: "grid", gridTemplateColumns: "24px 1fr 1fr auto", gap: 6, fontSize: 9.5, textTransform: "uppercase", letterSpacing: "0.04em", padding: "0 2px" }}>
            <div></div>
            <div>{lang === "fr" ? "Libellé affiché" : "Display label"}</div>
            <div>{lang === "fr" ? "Valeur stockée" : "Stored value"}</div>
            <div></div>
          </div>
          {options.map((o, idx) => (
            <div key={idx} style={{ display: "grid", gridTemplateColumns: "24px 1fr 1fr auto", gap: 6, alignItems: "center" }}>
              <div className="mono text-faint" style={{ fontSize: 10, textAlign: "center" }}>{idx + 1}</div>
              <input style={inp} value={o.label || ""} onChange={(e) => onLabelChange(idx, e.target.value)}
                placeholder={lang === "fr" ? "Oui, Non, Partiellement…" : "Yes, No, Partial…"} />
              <input style={{ ...inp, fontFamily: "ui-monospace, monospace", color: "var(--text-faint)" }}
                value={o.value || ""} onChange={(e) => onValueChange(idx, e.target.value)}
                placeholder="auto" />
              <div className="row gap-xs">
                <button className="btn xs ghost" onClick={() => onMove(idx, -1)} disabled={idx === 0}
                  title={lang === "fr" ? "Monter" : "Move up"} style={{ padding: "2px 4px" }}>↑</button>
                <button className="btn xs ghost" onClick={() => onMove(idx, +1)} disabled={idx === options.length - 1}
                  title={lang === "fr" ? "Descendre" : "Move down"} style={{ padding: "2px 4px" }}>↓</button>
                <button className="btn xs ghost" onClick={() => onRemove(idx)}
                  title={lang === "fr" ? "Supprimer cette option" : "Delete option"} style={{ padding: "2px 4px" }}>
                  <Icon.x />
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
      <div className="text-faint" style={{ fontSize: 10.5, marginTop: 6, lineHeight: 1.4 }}>
        {lang === "fr"
          ? "💡 La valeur stockée est générée automatiquement depuis le libellé. Modifiez-la manuellement uniquement si vous avez besoin d'un code précis (ex. clé d'export Excel)."
          : "💡 The stored value is auto-derived from the label. Override it only when you need a specific code (e.g. Excel export key)."}
      </div>
    </div>
  );
}

window.FormBuilder = FormBuilder;
