// ============================================================
// customizing-sub-4.jsx — FIX P2 (sessione 34)
//
// 4 NewXxxModal che fanno realmente POST verso i corrispondenti
// /api/config/* (templates, clauses, approval-workflows, approval-matrix).
//
// Le sezioni Cust* esistenti (in customizing-sub-1.jsx e -sub-2.jsx)
// hanno già il bottone "+ Nuovo X" e i corrispondenti detail modal
// con PATCH funzionante. Mancavano solo i modali di creazione.
// ============================================================

function _actorHeaders(userId) {
  return { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': userId || '' };
}

// ============================================================
// NewTemplateModalLive
// ============================================================
function NewTemplateModalLive({ open, onClose, clauses, onCreated }) {
  const { user, pushToast } = useStore();
  const [form, setForm] = React.useState({
    code: '', name: '', type: 'po', variables: '', locales: 'it-IT', clauseIds: [], aiAssist: false, active: true,
  });
  const [saving, setSaving] = React.useState(false);

  React.useEffect(() => {
    if (open) setForm({ code: '', name: '', type: 'po', variables: '', locales: 'it-IT', clauseIds: [], aiAssist: false, active: true });
  }, [open]);

  if (!open) return null;
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
  const valid = /^[A-Z0-9_]{2,40}$/.test(form.code) && form.name.trim();

  function toggleClause(id) {
    if (form.clauseIds.includes(id)) set('clauseIds', form.clauseIds.filter((x) => x !== id));
    else set('clauseIds', [...form.clauseIds, id]);
  }

  async function handleCreate() {
    if (!valid || saving) return;
    setSaving(true);
    try {
      const body = {
        code: form.code.trim(),
        name: form.name.trim(),
        type: form.type,
        variables: form.variables.split(',').map((s) => s.trim()).filter(Boolean),
        locales: form.locales.split(',').map((s) => s.trim()).filter(Boolean),
        clauseIds: form.clauseIds,
        aiAssist: form.aiAssist,
        active: form.active,
      };
      const r = await fetch('/api/config/templates', {
        method: 'POST',
        headers: _actorHeaders(user?.id),
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      onCreated && onCreated(j.data);
      pushToast({ title: 'Template creato', tone: 'ok' });
      onClose();
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e?.message || e), tone: 'err' });
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open={open}
      onClose={onClose}
      title="Nuovo template"
      size="lg"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleCreate}>
            {saving ? 'Creazione…' : 'Crea template'}
          </Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div className="grid grid-3">
          <div className="field"><label>Code <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.code} onChange={(e) => set('code', e.target.value.toUpperCase())} placeholder="es. PO_STD"/>
            {form.code && !/^[A-Z0-9_]{2,40}$/.test(form.code) && <div style={{ fontSize: 10, color: 'var(--err)' }}>Solo A-Z 0-9 _ (2-40 char)</div>}
          </div>
          <div className="field"><label>Nome <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="es. Ordine acquisto standard"/>
          </div>
          <div className="field"><label>Tipo</label>
            <select value={form.type} onChange={(e) => set('type', e.target.value)}>
              <option value="po">PO (ordine)</option>
              <option value="contract">Contratto</option>
              <option value="rfq">RFQ</option>
              <option value="email">Email</option>
              <option value="report">Report</option>
              <option value="memo">Memo</option>
            </select>
          </div>
        </div>
        <div className="grid grid-2">
          <div className="field"><label>Variables (CSV)</label>
            <input value={form.variables} onChange={(e) => set('variables', e.target.value)} placeholder="vendor.name, project.code, total"/>
          </div>
          <div className="field"><label>Locales (CSV)</label>
            <input value={form.locales} onChange={(e) => set('locales', e.target.value)} placeholder="it-IT, en-GB"/>
          </div>
        </div>
        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Clausole incluse ({form.clauseIds.length})</div>
          <div style={{ padding: 8, border: '1px solid var(--line)', borderRadius: 4, maxHeight: 150, overflowY: 'auto' }}>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
              {(clauses || []).map((c) => {
                const sel = form.clauseIds.includes(c.id);
                return (
                  <button key={c.id} className={`btn sm ${sel ? 'primary' : 'ghost'}`} style={{ fontSize: 10, padding: '2px 6px' }} onClick={() => toggleClause(c.id)} title={c.title}>
                    {c.code}
                  </button>
                );
              })}
              {(!clauses || clauses.length === 0) && <span style={{ fontSize: 11, color: 'var(--text-3)' }}>Nessuna clausola disponibile.</span>}
            </div>
          </div>
        </div>
        <div className="row" style={{ gap: 14 }}>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.aiAssist} onChange={(e) => set('aiAssist', e.target.checked)}/>
            AI assist (template AI fill compatibile)
          </label>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.active} onChange={(e) => set('active', e.target.checked)}/>
            Attivo subito
          </label>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// NewClauseModalLive
// ============================================================
function NewClauseModalLive({ open, onClose, onCreated }) {
  const { user, pushToast } = useStore();
  const [form, setForm] = React.useState({
    code: '', title: '', type: 'commercial', appliesTo: '', required: false, negotiable: true, language: 'it', tags: '', active: true,
  });
  const [saving, setSaving] = React.useState(false);

  React.useEffect(() => {
    if (open) setForm({ code: '', title: '', type: 'commercial', appliesTo: '', required: false, negotiable: true, language: 'it', tags: '', active: true });
  }, [open]);

  if (!open) return null;
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
  const valid = /^[A-Z0-9_]{2,40}$/.test(form.code) && form.title.trim();

  async function handleCreate() {
    if (!valid || saving) return;
    setSaving(true);
    try {
      const body = {
        code: form.code.trim(),
        title: form.title.trim(),
        type: form.type,
        appliesTo: form.appliesTo.split(',').map((s) => s.trim()).filter(Boolean),
        required: form.required,
        negotiable: form.negotiable,
        language: form.language,
        tags: form.tags.split(',').map((s) => s.trim()).filter(Boolean),
        active: form.active,
      };
      const r = await fetch('/api/config/clauses', {
        method: 'POST',
        headers: _actorHeaders(user?.id),
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      onCreated && onCreated(j.data);
      pushToast({ title: 'Clausola creata', tone: 'ok' });
      onClose();
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e?.message || e), tone: 'err' });
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open={open}
      onClose={onClose}
      title="Nuova clausola"
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleCreate}>{saving ? 'Creazione…' : 'Crea clausola'}</Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div className="grid grid-2">
          <div className="field"><label>Code <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.code} onChange={(e) => set('code', e.target.value.toUpperCase())} placeholder="es. PAY_30"/>
          </div>
          <div className="field"><label>Titolo <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.title} onChange={(e) => set('title', e.target.value)} placeholder="es. Pagamento 30gg DF"/>
          </div>
        </div>
        <div className="grid grid-3">
          <div className="field"><label>Tipo</label>
            <select value={form.type} onChange={(e) => set('type', e.target.value)}>
              <option value="commercial">commercial</option>
              <option value="legal">legal</option>
              <option value="hse">hse</option>
              <option value="quality">quality</option>
              <option value="warranty">warranty</option>
              <option value="other">other</option>
            </select>
          </div>
          <div className="field"><label>Applies to (CSV)</label>
            <input value={form.appliesTo} onChange={(e) => set('appliesTo', e.target.value)} placeholder="po, contract"/>
          </div>
          <div className="field"><label>Lingua</label>
            <select value={form.language} onChange={(e) => set('language', e.target.value)}>
              <option value="it">it</option>
              <option value="en">en</option>
              <option value="de">de</option>
              <option value="fr">fr</option>
            </select>
          </div>
        </div>
        <div className="field"><label>Tag (CSV)</label>
          <input value={form.tags} onChange={(e) => set('tags', e.target.value)} placeholder="standard, fastpay"/>
        </div>
        <div className="row" style={{ gap: 14 }}>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.required} onChange={(e) => set('required', e.target.checked)}/>
            Obbligatoria
          </label>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.negotiable} onChange={(e) => set('negotiable', e.target.checked)}/>
            Negoziabile
          </label>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.active} onChange={(e) => set('active', e.target.checked)}/>
            Attiva subito
          </label>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// NewWorkflowModalLive — sessione 97 Fase C: editor strutturato (no JSON)
// ============================================================
function NewWorkflowModalLive({ open, onClose, onCreated }) {
  const { user, pushToast, seedCustom } = useStore();
  const emptyForm = () => ({
    code: '', name: '', entityType: 'rda', status: 'draft',
    startConditions: { all: [] }, steps: [], active: true,
  });
  const [form, setForm] = React.useState(emptyForm);
  const [saving, setSaving] = React.useState(false);

  React.useEffect(() => {
    if (open) setForm(emptyForm());
  }, [open]);

  if (!open) return null;
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));

  const codeValid = /^[A-Z][A-Z0-9_-]{1,63}$/.test(form.code);
  const valid = codeValid && form.name.trim() && wfStepsValid(form.steps) && wfStartCondValid(form.startConditions);

  async function handleCreate() {
    if (!valid || saving) return;
    setSaving(true);
    try {
      const body = {
        code: form.code.trim(),
        name: form.name.trim(),
        entityType: form.entityType,
        status: form.status,
        startConditions: form.startConditions,
        steps: form.steps,
        active: form.active,
      };
      const r = await fetch('/api/config/approval-workflows', {
        method: 'POST',
        headers: _actorHeaders(user?.id),
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      onCreated && onCreated(j.data);
      pushToast({ title: 'Workflow creato', tone: 'ok' });
      onClose();
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e?.message || e), tone: 'err' });
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open={open}
      onClose={onClose}
      title="Nuovo approval workflow"
      size="xl"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleCreate}>{saving ? 'Creazione…' : 'Crea workflow'}</Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div className="grid grid-3">
          <div className="field"><label>Code <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.code} onChange={(e) => set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g, ''))} placeholder="es. RDA_STD_2026" style={{ fontFamily: 'var(--font-mono)' }}/>
          </div>
          <div className="field"><label>Nome <span style={{ color: 'var(--err)' }}>*</span></label>
            <input value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="es. Approvazione RdA standard"/>
          </div>
          <div className="field"><label>Entity type</label>
            <select value={form.entityType} onChange={(e) => set('entityType', e.target.value)}>
              <option value="rda">rda</option>
              <option value="vendor">vendor</option>
              <option value="project">project</option>
            </select>
          </div>
          <div className="field"><label>Stato</label>
            <select value={form.status} onChange={(e) => set('status', e.target.value)}>
              <option value="draft">draft</option>
              <option value="active">active</option>
              <option value="deprecated">deprecated</option>
            </select>
          </div>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer', alignSelf: 'end', paddingBottom: 8 }}>
            <input type="checkbox" checked={form.active} onChange={(e) => set('active', e.target.checked)}/>
            Attivo
          </label>
        </div>

        <WorkflowStartConditionsEditor value={form.startConditions} seedCustom={seedCustom}
          onChange={(v) => set('startConditions', v)}/>

        <WorkflowStepsEditor steps={form.steps} seedCustom={seedCustom}
          onChange={(v) => set('steps', v)}/>
        {!wfStepsValid(form.steps) && (
          <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
            Aggiungi almeno uno step con nome e approvatore per poter creare il workflow.
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// NewMatrixRuleModalLive
// ============================================================
function NewMatrixRuleModalLive({ open, onClose, roles, onCreated }) {
  const { user, pushToast } = useStore();
  const [form, setForm] = React.useState({
    roleCode: '', sites: '', bu: '', category: '', capexClass: '',
    amountMin: 0, amountMax: '', coApproval: false, coApprovers: '', notes: '', active: true,
  });
  const [saving, setSaving] = React.useState(false);

  React.useEffect(() => {
    if (open) setForm({
      roleCode: roles?.[0]?.code || 'BUYER',
      sites: '', bu: '', category: '', capexClass: '',
      amountMin: 0, amountMax: '', coApproval: false, coApprovers: '', notes: '', active: true,
    });
  }, [open, roles]);

  if (!open) return null;
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
  const aMin = Number(form.amountMin) || 0;
  const aMax = form.amountMax === '' ? null : Number(form.amountMax);
  const amountOk = aMax == null || aMax >= aMin;
  const valid = form.roleCode.trim() && amountOk;

  async function handleCreate() {
    if (!valid || saving) return;
    setSaving(true);
    try {
      const scope = {};
      if (form.sites.trim()) scope.sites = form.sites.split(',').map((s) => s.trim()).filter(Boolean);
      if (form.bu.trim()) scope.bu = form.bu.split(',').map((s) => s.trim()).filter(Boolean);
      const body = {
        roleCode: form.roleCode.trim(),
        scope: Object.keys(scope).length ? scope : null,
        category: form.category.trim() || null,
        capexClass: form.capexClass.trim() || null,
        amountMin: aMin,
        amountMax: aMax,
        coApproval: form.coApproval,
        coApprovers: form.coApprovers.split(',').map((s) => s.trim()).filter(Boolean),
        notes: form.notes.trim() || null,
        active: form.active,
      };
      const r = await fetch('/api/config/approval-matrix', {
        method: 'POST',
        headers: _actorHeaders(user?.id),
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      onCreated && onCreated(j.data);
      pushToast({ title: 'Regola autorizzativa creata', tone: 'ok' });
      onClose();
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e?.message || e), tone: 'err' });
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open={open}
      onClose={onClose}
      title="Nuova regola autorizzativa"
      size="lg"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleCreate}>{saving ? 'Creazione…' : 'Crea regola'}</Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div style={{ fontSize: 11.5, color: 'var(--text-2)' }}>
          Definisce chi può approvare quando il workflow raggiunge uno step di tipo <Chip kind="ai">matrix</Chip>. Campi vuoti = wildcard <code>*</code>.
        </div>
        <div className="grid grid-3">
          <div className="field"><label>Ruolo <span style={{ color: 'var(--err)' }}>*</span></label>
            <window.Autocomplete value={form.roleCode} onChange={v => set('roleCode', v)} allowClear={false}
              options={(roles || []).map((r) => ({ value: r.code, label: r.code, sublabel: r.name }))}
              placeholder="Cerca ruolo… (spazio per lista)" testId="cust-matrix4-role-ac" />
          </div>
          <div className="field"><label>Sites (CSV o vuoto = *)</label>
            <input value={form.sites} onChange={(e) => set('sites', e.target.value)} placeholder="cameri, nola"/>
          </div>
          <div className="field"><label>BU (CSV o vuoto = *)</label>
            <input value={form.bu} onChange={(e) => set('bu', e.target.value)} placeholder="aerospace"/>
          </div>
        </div>
        <div className="grid grid-2">
          <div className="field"><label>Categoria</label>
            <input value={form.category} onChange={(e) => set('category', e.target.value)} placeholder="opzionale"/>
          </div>
          <div className="field"><label>Classe CAPEX</label>
            <input value={form.capexClass} onChange={(e) => set('capexClass', e.target.value)} placeholder="opzionale"/>
          </div>
        </div>
        <div className="grid grid-2">
          <div className="field"><label>Soglia min (€)</label>
            <input type="number" min={0} value={form.amountMin} onChange={(e) => set('amountMin', Number(e.target.value))}/>
          </div>
          <div className="field"><label>Soglia max (€, vuoto = ∞)</label>
            <input type="number" min={0} value={form.amountMax} onChange={(e) => set('amountMax', e.target.value)}/>
            {!amountOk && <div style={{ fontSize: 10, color: 'var(--err)' }}>max deve essere ≥ min</div>}
          </div>
        </div>
        <div className="row" style={{ gap: 14, alignItems: 'flex-start' }}>
          <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.coApproval} onChange={(e) => set('coApproval', e.target.checked)}/>
            Co-firma richiesta
          </label>
          <div className="field" style={{ flex: 1 }}><label>Co-approvers (CSV ruolo o id)</label>
            <input value={form.coApprovers} onChange={(e) => set('coApprovers', e.target.value)} disabled={!form.coApproval} placeholder="CFO, COO"/>
          </div>
        </div>
        <div className="field"><label>Note</label>
          <input value={form.notes} onChange={(e) => set('notes', e.target.value)}/>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// CustAnomalies — FASE 11.I: lista anomaly_flag con filtri severity/state/entity
// + modal resolve/dismiss + KPI strip. Read da /api/anomaly-flags, mutation
// PATCH /api/anomaly-flags/[id] con state machine (open→resolved/dismissed).
// ============================================================
const ANOM_SEV_TONES = { low: 'info', medium: 'warn', high: 'err', critical: 'err' };
const ANOM_STATE_TONES = { open: 'warn', resolved: 'ok', dismissed: '' };
const ANOM_STATE_LABELS = { open: 'aperta', resolved: 'risolta', dismissed: 'archiviata' };

function CustAnomalies() {
  const { user, pushToast } = useStore();
  const [data, setData] = React.useState(null);
  const [filters, setFilters] = React.useState({ state: '', severity: '', entityType: '' });
  const [editing, setEditing] = React.useState(null);

  const reload = React.useCallback(async () => {
    setData(null);
    try {
      const params = new URLSearchParams();
      if (filters.state) params.set('state', filters.state);
      if (filters.severity) params.set('severity', filters.severity);
      if (filters.entityType) params.set('entityType', filters.entityType);
      params.set('limit', '100');
      const r = await fetch('/api/anomaly-flags?' + params.toString(), {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      setData(j);
    } catch (e) {
      setData({ data: [], total: 0, kpi: null, meta: null, error: String(e?.message || e) });
    }
  }, [filters, user?.id]);

  React.useEffect(() => { reload(); }, [reload]);

  const meta = data?.meta;
  const kpi = data?.kpi;
  const rows = data?.data || [];
  const setF = (k, v) => setFilters((f) => ({ ...f, [k]: v }));

  return (
    <div className="col" style={{ gap: 14 }}>
      {/* KPI strip */}
      {kpi && (
        <div className="grid grid-4" style={{ gap: 10 }}>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Totale anomalie</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4 }}>{kpi.total}</div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Aperte</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: kpi.byState.open > 0 ? 'var(--warn)' : 'var(--text-1)' }}>{kpi.byState.open}</div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Critical / High</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: (kpi.bySeverity.critical + kpi.bySeverity.high) > 0 ? 'var(--err)' : 'var(--text-1)' }}>
              {kpi.bySeverity.critical} / {kpi.bySeverity.high}
            </div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Risolte / Archiviate</div>
            <div style={{ fontSize: 14, marginTop: 4 }}>{kpi.byState.resolved} <span style={{color:'var(--text-3)'}}>· {kpi.byState.dismissed}</span></div>
          </div>
        </div>
      )}

      {/* Filtri */}
      <div className="row" style={{ gap: 10, alignItems: 'flex-end' }}>
        <div className="field" style={{ minWidth: 160 }}>
          <label style={{ fontSize: 10 }}>Stato</label>
          <select value={filters.state} onChange={(e) => setF('state', e.target.value)}>
            <option value="">— tutti —</option>
            {(meta?.states || []).map((s) => (
              <option key={s} value={s}>{ANOM_STATE_LABELS[s] || s}</option>
            ))}
          </select>
        </div>
        <div className="field" style={{ minWidth: 160 }}>
          <label style={{ fontSize: 10 }}>Severity</label>
          <select value={filters.severity} onChange={(e) => setF('severity', e.target.value)}>
            <option value="">— tutte —</option>
            {(meta?.severities || []).map((s) => (
              <option key={s} value={s}>{s}</option>
            ))}
          </select>
        </div>
        <div className="field" style={{ minWidth: 160 }}>
          <label style={{ fontSize: 10 }}>Entity type</label>
          <select value={filters.entityType} onChange={(e) => setF('entityType', e.target.value)}>
            <option value="">— tutti —</option>
            {(meta?.entityTypes || []).map((s) => (
              <option key={s} value={s}>{s}</option>
            ))}
          </select>
        </div>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm" onClick={reload}>
          <Icon name="refresh" size={11}/> Reload
        </Btn>
      </div>

      {/* Tabella */}
      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        {data === null ? (
          <div style={{ padding: 16, textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>Caricamento…</div>
        ) : data.error ? (
          <div style={{ padding: 16, color: 'var(--err)', fontSize: 12 }}>Errore: {data.error}</div>
        ) : rows.length === 0 ? (
          <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12 }}>
            Nessuna anomaly_flag corrispondente ai filtri. Le anomalie vengono create dal copilota AI via tool <code>flag_anomaly</code>.
          </div>
        ) : (
          <table className="tbl dense">
            <thead>
              <tr>
                <th style={{ width: 110 }}>Quando</th>
                <th>Titolo</th>
                <th style={{ width: 90 }}>Severity</th>
                <th style={{ width: 110 }}>Entity</th>
                <th>Entity ID</th>
                <th style={{ width: 90 }}>Stato</th>
                <th style={{ width: 100 }}>Flagged by</th>
                <th style={{ width: 60 }}/>
              </tr>
            </thead>
            <tbody>
              {rows.map((r) => (
                <tr key={r.id} className="clickable" onClick={() => setEditing(r)}>
                  <td className="mono" style={{ fontSize: 10.5 }}>
                    {new Date(r.createdAt).toLocaleString('it-IT', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
                  </td>
                  <td style={{ fontSize: 12, fontWeight: 500 }}>{r.title}</td>
                  <td><Chip kind={ANOM_SEV_TONES[r.severity] || 'info'}>{r.severity}</Chip></td>
                  <td style={{ fontSize: 11 }}>{r.entityType}</td>
                  <td><code style={{ fontSize: 10.5 }}>{r.entityId}</code></td>
                  <td><Chip kind={ANOM_STATE_TONES[r.state] || ''} dot>{ANOM_STATE_LABELS[r.state] || r.state}</Chip></td>
                  <td style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.flaggedBy || '(AI)'}</td>
                  <td style={{ textAlign: 'right' }}>
                    <Icon name="chevron-right" size={11}/>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>

      {editing && (
        <AnomalyResolveModal
          flag={editing}
          onClose={() => setEditing(null)}
          onSaved={() => { setEditing(null); reload(); }}
        />
      )}

      <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4, lineHeight: 1.5 }}>
        <Icon name="info" size={10}/> Le anomaly_flag sono create dall'AI tool <code>flag_anomaly</code> (FASE 11.F+11.H). Clicca una riga per <b>risolvere</b> (con nota obbligatoria) o <b>archiviare</b> come falso positivo. Lo stato è auditato (PATCH <code>/api/anomaly-flags/[id]</code> con diff field-level).
      </div>
    </div>
  );
}

function AnomalyResolveModal({ flag, onClose, onSaved }) {
  const { user, pushToast } = useStore();
  const [state, setState] = React.useState(flag.state);
  const [note, setNote] = React.useState(flag.resolutionNote || '');
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  const isDirty = state !== flag.state || (note || '') !== (flag.resolutionNote || '');
  const noteRequired = state === 'resolved';
  const noteValid = !noteRequired || (note.trim().length >= 3);

  async function handleSave() {
    if (!isDirty || saving || !noteValid) return;
    setSaving(true);
    setError(null);
    try {
      const body = { state };
      // Per dismiss e resolved invia note se presente; per open pulisce
      if (state === 'resolved' || state === 'dismissed') {
        body.resolutionNote = note.trim() || null;
      }
      const r = await fetch(`/api/anomaly-flags/${encodeURIComponent(flag.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msg = j?.error === 'validation_error'
          ? `Validazione: ${(j.issues || []).map((i) => `${(i.path || []).join('.')} ${i.message}`).join(' · ')}`
          : (j?.error || `HTTP ${r.status}`);
        setError(msg);
        return;
      }
      pushToast({
        title: state === 'resolved' ? 'Anomalia risolta' : state === 'dismissed' ? 'Anomalia archiviata' : 'Anomalia riaperta',
        desc: j?.changed === false ? 'Nessuna modifica' : 'Audit registrato',
        tone: state === 'open' ? 'info' : 'ok',
      });
      // Quick win sessione 63 — Broadcast event refresh Sidebar badge + Dashboard widget.
      // Skip su idempotent (count non cambia).
      if (j?.changed !== false) window.dispatchEvent(new Event('anomaly_count_changed'));
      onSaved();
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open
      onClose={onClose}
      title={`Anomaly · ${flag.title}`}
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
          <Btn variant="primary" size="sm" disabled={!isDirty || saving || !noteValid} onClick={handleSave}>
            {saving ? 'Salvataggio…' : (isDirty ? 'Applica' : 'Nessuna modifica')}
          </Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 12 }}>
        <div className="grid grid-2" style={{ gap: 10 }}>
          <div className="field"><label>Severity</label><div><Chip kind={ANOM_SEV_TONES[flag.severity]}>{flag.severity}</Chip></div></div>
          <div className="field"><label>Entity</label><div style={{ fontSize: 11 }}>{flag.entityType} · <code>{flag.entityId}</code></div></div>
        </div>
        <div className="field">
          <label>Descrizione</label>
          <div style={{ fontSize: 11.5, padding: 8, background: 'var(--bg-2)', borderRadius: 4, color: 'var(--text-2)' }}>
            {flag.description || <em style={{ color: 'var(--text-3)' }}>Nessuna descrizione</em>}
          </div>
        </div>
        <div className="field">
          <label>Stato</label>
          <div className="row" style={{ gap: 6 }}>
            <button type="button" className={`btn sm ${state === 'open' ? 'primary' : 'ghost'}`} onClick={() => setState('open')}>aperta</button>
            <button type="button" className={`btn sm ${state === 'resolved' ? 'primary' : 'ghost'}`} onClick={() => setState('resolved')}>risolvi</button>
            <button type="button" className={`btn sm ${state === 'dismissed' ? 'primary' : 'ghost'}`} onClick={() => setState('dismissed')}>archivia</button>
          </div>
        </div>
        {(state === 'resolved' || state === 'dismissed') && (
          <div className="field">
            <label>
              Resolution note {noteRequired && <span style={{ color: 'var(--err)' }}>* (min 3 char)</span>}
            </label>
            <textarea
              rows={4}
              value={note}
              onChange={(e) => setNote(e.target.value)}
              placeholder={state === 'resolved' ? 'Es. Fix applicato a vendor master, validato in workflow' : 'Opzionale per dismiss (es. falso positivo)'}
            />
          </div>
        )}
        {flag.state !== 'open' && flag.resolvedBy && (
          <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '6px 10px', background: 'var(--bg-2)', borderRadius: 4 }}>
            <Icon name="info" size={10}/> Risolta da <code>{flag.resolvedBy}</code> il {flag.resolvedAt ? new Date(flag.resolvedAt).toLocaleString('it-IT') : '—'}
          </div>
        )}
        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// CustTenants — FASE 50 (sessione): UI lista tenant + create modal con admin
// onboarding atomico (tenant + admin persona + auth_credential).
// Read da /api/tenants. Mutation POST /api/tenants per create + PATCH /[id] per
// edit + DELETE soft (tenant-default protetto server-side 403).
// ============================================================
const SUPERADMIN_ROLE_ID = 'SUPERADMIN';

// FASE 53.1 (sessione 54) — sync con lib/tenant-lifecycle-event.ts
const LIFECYCLE_EVENT_TYPES = ['onboarded', 'patched', 'soft_deleted', 'hard_deleted', 'exported', 'imported'];
const LIFECYCLE_EVENT_LABELS = {
  onboarded: 'Onboarding',
  patched: 'Update',
  soft_deleted: 'Disattivato (soft)',
  hard_deleted: 'Cancellato (hard, GDPR)',
  exported: 'Esportato',
  imported: 'Importato (restore)',
};
const LIFECYCLE_EVENT_TONES = {
  onboarded: 'ok',
  patched: '',
  soft_deleted: 'warn',
  hard_deleted: 'err',
  exported: '',
  imported: 'ok',
};

function CustTenants() {
  const { user, pushToast } = useStore();
  const [data, setData] = React.useState(null);
  const [stats, setStats] = React.useState(null); // tenant-stats response
  const [error, setError] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const [showImport, setShowImport] = React.useState(false);
  const [hardDeleting, setHardDeleting] = React.useState(null); // tenant in confirm phase
  const [editing, setEditing] = React.useState(null);
  const [revealedAdmin, setRevealedAdmin] = React.useState(null); // { tenant, admin, password (one-shot) }
  // FASE 53.1 (sessione 54) — Audit lifecycle ledger
  const [events, setEvents] = React.useState(null);
  const [evFilters, setEvFilters] = React.useState({ tenantId: '', eventType: '', since: '', until: '' });
  // FASE 53.2 (sessione 58) — Retention prune modal
  const [showPrune, setShowPrune] = React.useState(false);
  // FASE 55 (sessione 55) — Multi-superadmin promotion
  const [superadmins, setSuperadmins] = React.useState(null);
  const [showPromote, setShowPromote] = React.useState(false);

  const isSuperadmin = (user?.roleIds || []).includes(SUPERADMIN_ROLE_ID);

  const reload = React.useCallback(async () => {
    setData(null);
    setError(null);
    try {
      const r = await fetch('/api/tenants', {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) throw new Error('HTTP ' + r.status);
      const j = await r.json();
      setData(j.data || []);
    } catch (e) {
      setError(String(e?.message || e));
    }
  }, [user?.id]);

  // Stats cross-tenant: fetch solo se super-admin
  const reloadStats = React.useCallback(async () => {
    if (!isSuperadmin) return;
    setStats(null);
    try {
      const r = await fetch('/api/admin/tenant-stats', {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error === 'not_superadmin' ? 'not_superadmin' : `HTTP ${r.status}`);
      }
      const j = await r.json();
      setStats(j);
    } catch (e) {
      setStats({ data: [], totals: null, error: String(e?.message || e) });
    }
  }, [user?.id, isSuperadmin]);

  // Lifecycle audit reload (sessione 54)
  const reloadEvents = React.useCallback(async () => {
    if (!isSuperadmin) return;
    setEvents(null);
    try {
      const qs = new URLSearchParams();
      if (evFilters.tenantId) qs.set('tenantId', evFilters.tenantId);
      if (evFilters.eventType) qs.set('eventType', evFilters.eventType);
      if (evFilters.since) qs.set('since', evFilters.since);
      if (evFilters.until) qs.set('until', evFilters.until);
      qs.set('limit', '100');
      const r = await fetch(`/api/admin/tenant-lifecycle-events?${qs.toString()}`, {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error === 'not_superadmin' ? 'not_superadmin' : `HTTP ${r.status}`);
      }
      const j = await r.json();
      setEvents(j);
    } catch (e) {
      setEvents({ data: [], count: 0, error: String(e?.message || e) });
    }
  }, [user?.id, isSuperadmin, evFilters]);

  // Superadmin list reload (sessione 55)
  const reloadSuperadmins = React.useCallback(async () => {
    if (!isSuperadmin) return;
    setSuperadmins(null);
    try {
      const r = await fetch('/api/admin/superadmins', {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error === 'not_superadmin' ? 'not_superadmin' : `HTTP ${r.status}`);
      }
      const j = await r.json();
      setSuperadmins(j);
    } catch (e) {
      setSuperadmins({ data: [], count: 0, error: String(e?.message || e) });
    }
  }, [user?.id, isSuperadmin]);

  async function handleDemote(p) {
    if (!confirm(`Rimuovere il ruolo SUPERADMIN da "${p.name}"? La persona resterà nel sistema con i suoi altri ruoli.`)) return;
    try {
      const r = await fetch(`/api/admin/superadmins/${encodeURIComponent(p.id)}/demote`, {
        method: 'POST',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        credentials: 'same-origin',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msgs = {
          self_demote_forbidden: 'Non puoi demote te stesso (anti-lockout).',
          last_superadmin_protected: 'Impossibile demote: deve esistere almeno 1 SUPERADMIN attivo.',
          persona_not_found: 'Persona non trovata.',
          not_superadmin: 'Permesso negato (SUPERADMIN richiesto).',
        };
        pushToast({
          title: 'Demote rifiutato',
          desc: msgs[j.error] || j.error || `HTTP ${r.status}`,
          tone: 'err',
        });
        return;
      }
      pushToast({
        title: j.changed ? 'Ruolo SUPERADMIN rimosso' : 'Persona già non era SUPERADMIN',
        desc: p.name,
        tone: 'ok',
      });
      reloadSuperadmins();
    } catch (e) {
      pushToast({ title: 'Errore demote', desc: String(e), tone: 'err' });
    }
  }

  React.useEffect(() => { reload(); reloadStats(); }, [reload, reloadStats]);
  React.useEffect(() => { reloadEvents(); }, [reloadEvents]);
  React.useEffect(() => { reloadSuperadmins(); }, [reloadSuperadmins]);

  async function handleSoftDelete(t) {
    if (!confirm(`Disattivare il tenant "${t.name}"? Soft-delete: resterà tracciato ma non operativo.`)) return;
    try {
      const r = await fetch(`/api/tenants/${encodeURIComponent(t.id)}`, {
        method: 'DELETE',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        credentials: 'same-origin',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const isNotSuperadmin = j.error === 'not_superadmin';
        const isSystemProtected = j.error === 'system_tenant_protected';
        pushToast({
          title: isNotSuperadmin ? 'Permesso negato (SUPERADMIN richiesto)' :
                 isSystemProtected ? 'Tenant di sistema protetto' :
                 'Errore',
          desc: j.detail || j.error || `HTTP ${r.status}`,
          tone: 'err',
        });
        return;
      }
      pushToast({ title: 'Tenant disattivato', desc: t.name, tone: 'ok' });
      reload();
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e), tone: 'err' });
    }
  }

  async function handleExport(t, format = 'json') {
    try {
      const qs = format === 'zip' ? '?format=zip' : '';
      const r = await fetch(`/api/tenants/${encodeURIComponent(t.id)}/export${qs}`, {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        const isNotSuperadmin = j.error === 'not_superadmin';
        pushToast({
          title: isNotSuperadmin ? 'Permesso negato (SUPERADMIN richiesto)' : 'Errore export',
          desc: j.detail || j.error || `HTTP ${r.status}`,
          tone: 'err',
        });
        return;
      }
      // Triggera download via blob URL
      const blob = await r.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      const cd = r.headers.get('content-disposition') || '';
      const m = cd.match(/filename="([^"]+)"/);
      a.download = m ? m[1] : `veridanto-tenant-${t.id}.${format}`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
      // Per zip: leggi headers per descrizione blob count
      if (format === 'zip') {
        const blobsIncl = r.headers.get('x-veridanto-blobs-included') || '0';
        const blobsMiss = r.headers.get('x-veridanto-blobs-missing') || '0';
        const blobBytes = r.headers.get('x-veridanto-blob-bytes') || '0';
        const mb = (parseInt(blobBytes, 10) / 1_000_000).toFixed(2);
        pushToast({
          title: 'Export ZIP completato',
          desc: `${a.download} · ${blobsIncl} blob inclusi (${mb} MB)${blobsMiss !== '0' ? ` · ${blobsMiss} mancanti` : ''}`,
          tone: 'ok',
        });
      } else {
        pushToast({ title: 'Export JSON completato', desc: a.download, tone: 'ok' });
      }
    } catch (e) {
      pushToast({ title: 'Errore export', desc: String(e), tone: 'err' });
    }
  }

  const total = data?.length ?? 0;
  const activeCount = (data || []).filter((t) => t.active).length;
  const inactiveCount = total - activeCount;

  return (
    <div className="col" style={{ gap: 14 }}>
      {/* Banner role corrente */}
      <div style={{
        padding: '8px 12px',
        background: isSuperadmin ? 'color-mix(in oklch, var(--accent) 14%, var(--bg-1))' : 'var(--bg-2)',
        border: `1px solid ${isSuperadmin ? 'var(--accent)' : 'var(--line)'}`,
        borderRadius: 6,
        fontSize: 12,
        display: 'flex', alignItems: 'center', gap: 8,
      }}>
        <Icon name="shield" size={13}/>
        {isSuperadmin ? (
          <span><b>Modalità Super-Admin attiva</b> · operazioni cross-tenant abilitate (create, edit, soft-delete, statistiche aggregate)</span>
        ) : (
          <span><b>Visualizzazione read-only</b> · solo persona con role <code>SUPERADMIN</code> può creare/modificare tenant. Nuovo tenant + Edit + Disattiva sono disabilitati.</span>
        )}
      </div>

      <div className="grid grid-3" style={{ gap: 10 }}>
        <div className="card" style={{ padding: 12 }}>
          <div className="eyebrow">Tenant totali</div>
          <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4 }}>{total}</div>
        </div>
        <div className="card" style={{ padding: 12 }}>
          <div className="eyebrow">Attivi</div>
          <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: 'var(--ok)' }}>{activeCount}</div>
        </div>
        <div className="card" style={{ padding: 12 }}>
          <div className="eyebrow">Disattivati</div>
          <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: 'var(--text-3)' }}>{inactiveCount}</div>
        </div>
      </div>

      {/* Dashboard stats cross-tenant: visibile solo a super-admin */}
      {isSuperadmin && (
        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Statistiche cross-tenant (SUPERADMIN only)</div>
          <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
            {stats === null ? (
              <div style={{ padding: 12, color: 'var(--text-3)', fontSize: 11 }}>Caricamento stats…</div>
            ) : stats.error ? (
              <div style={{ padding: 12, color: 'var(--err)', fontSize: 11 }}>Errore stats: {stats.error}</div>
            ) : (
              <table className="tbl dense">
                <thead>
                  <tr>
                    <th style={{ width: 160 }}>Tenant</th>
                    <th className="num" style={{ width: 80 }}>Persona</th>
                    <th className="num" style={{ width: 80 }}>Project</th>
                    <th className="num" style={{ width: 80 }}>RdA</th>
                    <th className="num" style={{ width: 80 }}>Vendor</th>
                    <th className="num" style={{ width: 90 }}>Anomaly</th>
                    <th className="num" style={{ width: 90 }}>Audit log</th>
                    <th style={{ width: 70, textAlign: 'center' }}>Stato</th>
                  </tr>
                </thead>
                <tbody>
                  {(stats.data || []).map((t) => (
                    <tr key={t.tenantId}>
                      <td style={{ fontSize: 11 }}>
                        <div style={{ fontWeight: 500 }}>{t.tenantName}</div>
                        <code style={{ fontSize: 10, color: 'var(--text-3)' }}>{t.tenantId}</code>
                      </td>
                      <td className="num mono" style={{ fontSize: 11 }}>{t.counts.persona}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{t.counts.project}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{t.counts.rda}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{t.counts.vendor}</td>
                      <td className="num mono" style={{ fontSize: 11, color: t.counts.anomalyFlag > 0 ? 'var(--warn)' : 'var(--text-3)' }}>{t.counts.anomalyFlag}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{t.counts.auditLog}</td>
                      <td style={{ textAlign: 'center' }}><Chip kind={t.tenantActive ? 'ok' : ''} dot>{t.tenantActive ? 'attivo' : 'off'}</Chip></td>
                    </tr>
                  ))}
                  {stats.totals && (
                    <tr style={{ background: 'var(--bg-2)', fontWeight: 600 }}>
                      <td style={{ fontSize: 11 }}>TOTALI ({stats.totals.tenants})</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.persona}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.project}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.rda}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.vendor}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.anomalyFlag}</td>
                      <td className="num mono" style={{ fontSize: 11 }}>{stats.totals.auditLog}</td>
                      <td/>
                    </tr>
                  )}
                </tbody>
              </table>
            )}
          </div>
        </div>
      )}

      <div className="row" style={{ alignItems: 'center' }}>
        <div className="eyebrow">Tenant ({total})</div>
        <span className="spacer"/>
        <Btn
          variant="ghost"
          size="sm"
          disabled={!isSuperadmin}
          title={!isSuperadmin ? 'Solo SUPERADMIN può importare tenant' : 'Importa tenant da file JSON di export'}
          onClick={() => setShowImport(true)}
        >
          <Icon name="upload" size={11}/> Importa
        </Btn>
        <Btn
          variant="primary"
          size="sm"
          disabled={!isSuperadmin}
          title={!isSuperadmin ? 'Solo SUPERADMIN può creare nuovi tenant' : ''}
          onClick={() => setShowNew(true)}
        >
          <Icon name="plus" size={11}/> Nuovo tenant
        </Btn>
      </div>

      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        {data === null ? (
          <div style={{ padding: 16, textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>Caricamento…</div>
        ) : error ? (
          <div style={{ padding: 16, color: 'var(--err)', fontSize: 12 }}>Errore: {error}</div>
        ) : total === 0 ? (
          <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12 }}>Nessun tenant configurato.</div>
        ) : (
          <table className="tbl dense">
            <thead>
              <tr>
                <th style={{ width: 180 }}>ID</th>
                <th>Nome</th>
                <th style={{ width: 110 }}>Slug</th>
                <th>Description</th>
                <th style={{ width: 90 }}>Stato</th>
                <th style={{ width: 130 }}>Created</th>
                <th style={{ width: 100, textAlign: 'right' }}/>
              </tr>
            </thead>
            <tbody>
              {data.map((t) => (
                <tr key={t.id}>
                  <td><code style={{ fontSize: 11 }}>{t.id}</code></td>
                  <td style={{ fontWeight: 500 }}>{t.name}</td>
                  <td className="mono" style={{ fontSize: 11 }}>{t.slug}</td>
                  <td style={{ fontSize: 11, color: 'var(--text-2)' }}>{t.description || <em style={{color:'var(--text-3)'}}>—</em>}</td>
                  <td><Chip kind={t.active ? 'ok' : ''} dot>{t.active ? 'attivo' : 'disattivato'}</Chip></td>
                  <td className="mono" style={{ fontSize: 10.5 }}>{new Date(t.createdAt).toLocaleDateString('it-IT')}</td>
                  <td style={{ textAlign: 'right' }}>
                    <div className="row" style={{ gap: 4, justifyContent: 'flex-end' }}>
                      <Btn
                        variant="ghost"
                        size="sm"
                        disabled={!isSuperadmin}
                        title={!isSuperadmin ? 'Solo SUPERADMIN può esportare' : 'Esporta JSON snapshot (metadata-only)'}
                        onClick={() => handleExport(t, 'json')}
                      ><Icon name="download" size={10}/></Btn>
                      <Btn
                        variant="ghost"
                        size="sm"
                        disabled={!isSuperadmin}
                        title={!isSuperadmin ? 'Solo SUPERADMIN può esportare' : 'Esporta ZIP completo (JSON + file binary in MinIO)'}
                        onClick={() => handleExport(t, 'zip')}
                      >ZIP</Btn>
                      <Btn
                        variant="ghost"
                        size="sm"
                        disabled={!isSuperadmin}
                        title={!isSuperadmin ? 'Solo SUPERADMIN può modificare' : ''}
                        onClick={() => setEditing(t)}
                      >Edit</Btn>
                      {t.active && (
                        <Btn
                          variant="ghost"
                          size="sm"
                          disabled={!isSuperadmin}
                          onClick={() => handleSoftDelete(t)}
                          title={!isSuperadmin ? 'Solo SUPERADMIN può disattivare' : t.id === 'tenant-default' ? 'Tenant di sistema (protetto)' : 'Disattiva (soft)'}
                        >×</Btn>
                      )}
                      {t.id !== 'tenant-default' && (
                        <Btn
                          variant="ghost"
                          size="sm"
                          disabled={!isSuperadmin}
                          onClick={() => setHardDeleting(t)}
                          title={!isSuperadmin ? 'Solo SUPERADMIN può cancellare' : 'Hard-delete (GDPR, irreversibile)'}
                          style={{ color: 'var(--err)' }}
                        ><Icon name="trash" size={10}/></Btn>
                      )}
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>

      {showNew && (
        <NewTenantModal
          onClose={() => setShowNew(false)}
          onCreated={(payload) => {
            setShowNew(false);
            setRevealedAdmin(payload);
            reload();
          }}
        />
      )}

      {editing && (
        <TenantEditModal
          tenant={editing}
          onClose={() => setEditing(null)}
          onSaved={() => { setEditing(null); reload(); }}
        />
      )}

      {revealedAdmin && (
        <Modal open onClose={() => setRevealedAdmin(null)} title="Tenant creato — admin onboarded" size="md" footer={
          <Btn variant="primary" size="sm" onClick={() => setRevealedAdmin(null)}>Capito</Btn>
        }>
          <div className="col" style={{ gap: 12 }}>
            <div style={{ padding: 10, background: 'rgba(34,197,94,0.08)', border: '1px solid var(--ok)', borderRadius: 4, fontSize: 12 }}>
              ✓ Tenant <code>{revealedAdmin.tenant.id}</code> creato con admin <code>{revealedAdmin.admin.id}</code>.
              L'admin può ora fare login con email + password che hai configurato.
            </div>
            <div className="field"><label>Tenant ID</label><div className="mono" style={{padding: 8, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11}}>{revealedAdmin.tenant.id}</div></div>
            <div className="field"><label>Admin email (per login)</label><div className="mono" style={{padding: 8, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11, userSelect:'all'}}>{revealedAdmin.admin.email}</div></div>
            <div style={{ fontSize: 11, color: 'var(--text-3)' }}>
              La password admin non viene re-mostrata: comunicala fuori-banda all'admin nominato.
            </div>
          </div>
        </Modal>
      )}

      {hardDeleting && (
        <HardDeleteTenantModal
          tenant={hardDeleting}
          onClose={() => setHardDeleting(null)}
          onDeleted={(result) => {
            setHardDeleting(null);
            pushToast({
              title: 'Tenant cancellato (hard-delete)',
              desc: `${result.totalDeleted} righe rimosse`,
              tone: 'ok',
            });
            reload();
            reloadStats();
          }}
        />
      )}

      {showImport && (
        <ImportTenantModal
          onClose={() => setShowImport(false)}
          onImported={(result) => {
            setShowImport(false);
            // Sessione 60: zip mode → mostra anche blob restore counters
            const blobInfo = result.blobRestore
              ? ` · ${result.blobRestore.blobsRestored} blob (${(result.blobRestore.totalBytesRestored / 1024).toFixed(1)} KB)${result.blobRestore.blobsSkipped > 0 ? ` · ${result.blobRestore.blobsSkipped} skipped` : ''}${result.blobRestore.storageUnavailable ? ' · storage unavailable' : ''}`
              : '';
            const tone = result.blobRestore?.storageUnavailable ? 'warn' : 'ok';
            pushToast({
              title: result.blobRestore?.storageUnavailable ? 'Tenant importato (storage offline)' : 'Tenant importato',
              desc: `${result.data.id} (${result.totalInserted} righe${blobInfo})`,
              tone,
            });
            reload();
            reloadStats();
          }}
        />
      )}

      {showPromote && (
        <PromoteSuperadminModal
          onClose={() => setShowPromote(false)}
          onPromoted={(result) => {
            setShowPromote(false);
            pushToast({
              title: result.changed ? 'Super-admin promosso' : 'Persona già super-admin',
              desc: result.data.name,
              tone: 'ok',
            });
            reloadSuperadmins();
          }}
        />
      )}

      {showPrune && (
        <RetentionPruneModal
          onClose={() => setShowPrune(false)}
          onPruned={(result) => {
            setShowPrune(false);
            pushToast({
              title: result.dryRun ? 'Prune dry-run completato' : 'Prune eseguito',
              desc: `${result.deletedCount} righe ${result.dryRun ? 'da cancellare' : 'cancellate'} (retention ${result.retentionDays}gg)`,
              tone: 'ok',
            });
            reloadEvents();
          }}
        />
      )}

      {/* Super-admin management (sessione 55) — visibile solo a super-admin */}
      {isSuperadmin && (
        <div>
          <div className="row" style={{ alignItems: 'center', marginBottom: 6 }}>
            <div className="eyebrow">Super-admin management (cross-tenant)</div>
            <span className="spacer"/>
            <Btn variant="ghost" size="sm" onClick={reloadSuperadmins}>
              <Icon name="refresh" size={11}/> Refresh
            </Btn>
            <Btn variant="primary" size="sm" onClick={() => setShowPromote(true)}>
              <Icon name="plus" size={11}/> Promuovi nuovo super-admin
            </Btn>
          </div>
          <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
            {superadmins === null ? (
              <div style={{ padding: 12, color: 'var(--text-3)', fontSize: 11 }}>Caricamento super-admin…</div>
            ) : superadmins.error ? (
              <div style={{ padding: 12, color: 'var(--err)', fontSize: 11 }}>Errore: {superadmins.error}</div>
            ) : superadmins.count === 0 ? (
              <div style={{ padding: 12, color: 'var(--text-3)', fontSize: 11 }}>Nessuna persona con role SUPERADMIN.</div>
            ) : (
              <table className="tbl dense">
                <thead>
                  <tr>
                    <th style={{ width: 140 }}>Persona ID</th>
                    <th>Nome</th>
                    <th style={{ width: 220 }}>Email</th>
                    <th style={{ width: 130 }}>Tenant</th>
                    <th style={{ width: 70, textAlign: 'center' }}>Stato</th>
                    <th style={{ width: 90, textAlign: 'right' }}/>
                  </tr>
                </thead>
                <tbody>
                  {superadmins.data.map((p) => {
                    const isSelf = p.id === user?.id;
                    const isLastSA = superadmins.data.filter(x => x.active).length <= 1 && p.active;
                    const disableReason = isSelf
                      ? 'Non puoi demote te stesso (anti-lockout)'
                      : isLastSA
                        ? 'Ultimo SUPERADMIN attivo: impossibile demote'
                        : 'Rimuovi role SUPERADMIN';
                    return (
                      <tr key={p.id}>
                        <td><code style={{ fontSize: 11 }}>{p.id}</code></td>
                        <td style={{ fontWeight: 500 }}>{p.name}</td>
                        <td className="mono" style={{ fontSize: 10.5 }}>{p.email}</td>
                        <td className="mono" style={{ fontSize: 10.5 }}>
                          {p.tenantId ? <code>{p.tenantId}</code> : <em style={{color:'var(--text-3)'}}>—</em>}
                        </td>
                        <td style={{ textAlign: 'center' }}>
                          <Chip kind={p.active ? 'ok' : ''} dot>{p.active ? 'attivo' : 'off'}</Chip>
                        </td>
                        <td style={{ textAlign: 'right' }}>
                          <Btn
                            variant="ghost"
                            size="sm"
                            disabled={isSelf || isLastSA}
                            onClick={() => handleDemote(p)}
                            title={disableReason}
                          >Demote</Btn>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            )}
          </div>
          <div style={{ fontSize: 10, color: 'var(--text-3)', padding: '4px 8px' }}>
            {superadmins?.count != null && `${superadmins.count} super-admin attivi`} · safety: no self-demote, no last-SA-demote
          </div>
        </div>
      )}

      {/* FASE 22.A2 sessione 76 — Audit log hash chain integrity */}
      {isSuperadmin && (
        <AuditChainVerifyPanel />
      )}

      {/* FASE 22.A3 sessione 77 — Separation of Duties rules */}
      {isSuperadmin && (
        <SodRulesPanel />
      )}

      {/* Audit lifecycle (sessione 54) — visibile solo a super-admin */}
      {isSuperadmin && (
        <div>
          <div className="row" style={{ alignItems: 'center', marginBottom: 6 }}>
            <div className="eyebrow">Audit lifecycle (compliance GDPR/SOC2)</div>
            <span className="spacer"/>
            <Btn variant="ghost" size="sm" onClick={() => setShowPrune(true)} title="Prune retention policy">
              <Icon name="trash" size={11}/> Prune…
            </Btn>
            <Btn variant="ghost" size="sm" onClick={reloadEvents}>
              <Icon name="refresh" size={11}/> Refresh
            </Btn>
          </div>
          {/* Filtri */}
          <div className="card" style={{ padding: 8, marginBottom: 8 }}>
            <div className="row" style={{ gap: 8, flexWrap: 'wrap' }}>
              <div className="field" style={{ minWidth: 140 }}>
                <label style={{fontSize:10}}>Tipo evento</label>
                <select
                  value={evFilters.eventType}
                  onChange={(e) => setEvFilters((p) => ({ ...p, eventType: e.target.value }))}
                  style={{ fontSize: 11 }}
                >
                  <option value="">Tutti</option>
                  {LIFECYCLE_EVENT_TYPES.map((t) => (
                    <option key={t} value={t}>{LIFECYCLE_EVENT_LABELS[t]}</option>
                  ))}
                </select>
              </div>
              <div className="field" style={{ minWidth: 160 }}>
                <label style={{fontSize:10}}>Tenant target</label>
                <input
                  value={evFilters.tenantId}
                  onChange={(e) => setEvFilters((p) => ({ ...p, tenantId: e.target.value }))}
                  placeholder="es. tenant-default"
                  style={{ fontSize: 11, fontFamily: 'var(--font-mono)' }}
                />
              </div>
              <div className="field" style={{ minWidth: 140 }}>
                <label style={{fontSize:10}}>Da (incl.)</label>
                <input
                  type="date"
                  value={evFilters.since}
                  onChange={(e) => setEvFilters((p) => ({ ...p, since: e.target.value }))}
                  style={{ fontSize: 11 }}
                />
              </div>
              <div className="field" style={{ minWidth: 140 }}>
                <label style={{fontSize:10}}>A (escl.)</label>
                <input
                  type="date"
                  value={evFilters.until}
                  onChange={(e) => setEvFilters((p) => ({ ...p, until: e.target.value }))}
                  style={{ fontSize: 11 }}
                />
              </div>
              <div style={{ alignSelf: 'flex-end' }}>
                <Btn
                  variant="ghost"
                  size="sm"
                  onClick={() => setEvFilters({ tenantId: '', eventType: '', since: '', until: '' })}
                >Reset</Btn>
              </div>
            </div>
          </div>
          {/* Tabella eventi */}
          <div className="card" style={{ padding: 0, overflow: 'hidden', maxHeight: 360, overflowY: 'auto' }}>
            {events === null ? (
              <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12 }}>Caricamento eventi…</div>
            ) : events.error ? (
              <div style={{ padding: 16, color: 'var(--err)', fontSize: 12 }}>Errore: {events.error}</div>
            ) : events.count === 0 ? (
              <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12 }}>Nessun evento trovato per i filtri attivi.</div>
            ) : (
              <table className="tbl dense">
                <thead>
                  <tr>
                    <th style={{ width: 150 }}>Timestamp</th>
                    <th style={{ width: 150 }}>Evento</th>
                    <th style={{ width: 160 }}>Tenant target</th>
                    <th style={{ width: 130 }}>Caller</th>
                    <th style={{ width: 70, textAlign: 'right' }}>Durata</th>
                    <th>Meta</th>
                    <th style={{ width: 60, textAlign: 'center' }}>Esito</th>
                  </tr>
                </thead>
                <tbody>
                  {events.data.map((ev) => (
                    <tr key={ev.id}>
                      <td className="mono" style={{ fontSize: 10.5 }}>
                        {new Date(ev.ts).toLocaleString('it-IT', { dateStyle: 'short', timeStyle: 'medium' })}
                      </td>
                      <td>
                        <Chip kind={LIFECYCLE_EVENT_TONES[ev.eventType] || ''} dot>
                          {LIFECYCLE_EVENT_LABELS[ev.eventType] || ev.eventType}
                        </Chip>
                      </td>
                      <td className="mono" style={{ fontSize: 10.5 }}>
                        {ev.tenantId ? <code>{ev.tenantId}</code> : <em style={{color:'var(--text-3)'}}>—</em>}
                      </td>
                      <td className="mono" style={{ fontSize: 10.5 }}>{ev.actorPersonaId}</td>
                      <td className="num mono" style={{ fontSize: 10.5 }}>
                        {ev.durationMs != null ? `${ev.durationMs}ms` : '—'}
                      </td>
                      <td style={{ fontSize: 10, color: 'var(--text-2)', wordBreak: 'break-word' }}>
                        {ev.meta ? <code style={{fontSize:10}}>{JSON.stringify(ev.meta).slice(0, 200)}</code> : '—'}
                      </td>
                      <td style={{ textAlign: 'center' }}>
                        {ev.success ? <Chip kind="ok" dot>OK</Chip> : <Chip kind="err" dot>FAIL</Chip>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
          </div>
          <div style={{ fontSize: 10, color: 'var(--text-3)', padding: '4px 8px' }}>
            {events?.count != null && `${events.count} eventi`} · ledger append-only · `tenant_lifecycle_event` table
          </div>
        </div>
      )}

      <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4, lineHeight: 1.5 }}>
        <Icon name="info" size={10}/> POST <code>/api/tenants</code> crea atomicamente <code>tenant + persona admin + auth_credential</code>. <b>Esporta</b> <code>GET /api/tenants/[id]/export</code> snapshot JSON. <b>Importa</b> <code>POST /api/tenants/import</code> ricrea atomicamente da JSON. <b>Hard-delete</b> <code>DELETE /api/tenants/[id]?hard=true&confirm=&lt;id&gt;</code> rimuove cascade (GDPR). Il tenant <code>tenant-default</code> è il tenant di sistema e non può essere cancellato. <b>Audit lifecycle</b> append-only via <code>tenant_lifecycle_event</code> (sessione 54).
      </div>
    </div>
  );
}

function NewTenantModal({ onClose, onCreated }) {
  const { user, pushToast } = useStore();
  const [form, setForm] = React.useState({
    id: '',
    name: '',
    slug: '',
    description: '',
    adminName: '',
    adminEmail: '',
    adminPassword: '',
  });
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));

  // Auto-derive slug e id da name se non valorizzati manualmente
  function onNameChange(v) {
    set('name', v);
    const auto = v.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32);
    if (!form.slug) set('slug', auto);
    if (!form.id) set('id', auto.slice(0, 64));
  }

  const valid =
    form.id.trim().length >= 2 && /^[a-z][a-z0-9-]{1,63}$/.test(form.id) && form.id !== 'tenant-default' &&
    form.name.trim().length >= 1 &&
    /^[a-z][a-z0-9-]{1,32}$/.test(form.slug) &&
    form.adminName.trim().length >= 2 &&
    /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.adminEmail) &&
    form.adminPassword.length >= 8;

  async function handleCreate() {
    if (!valid || saving) return;
    setSaving(true); setError(null);
    try {
      const body = {
        id: form.id.trim(),
        name: form.name.trim(),
        slug: form.slug.trim(),
        description: form.description.trim() || null,
        admin: {
          name: form.adminName.trim(),
          email: form.adminEmail.trim().toLowerCase(),
          password: form.adminPassword,
          roleIds: ['admin'],
        },
      };
      const r = await fetch('/api/tenants', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msg = j.error === 'validation_error'
          ? `Validazione: ${(j.issues || []).map((i) => `${(i.path || []).join('.')} ${i.message}`).join(' · ')}`
          : j.error === 'not_superadmin'
            ? `Permesso negato: solo persona con role SUPERADMIN può eseguire questa operazione`
            : j.detail || j.error || `HTTP ${r.status}`;
        setError(msg);
        return;
      }
      onCreated({ tenant: j.data, admin: j.admin });
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open
      onClose={onClose}
      title="Nuovo tenant"
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleCreate}>
            {saving ? 'Creazione…' : 'Crea tenant + admin'}
          </Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div style={{ fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.5 }}>
          POST atomico: crea <b>tenant + persona admin + auth_credential</b> in singola transaction.
          L'admin può fare login con email + password subito dopo.
        </div>

        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Tenant identity</div>
          <div className="card" style={{ padding: 12 }}>
            <div className="field"><label>Nome <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.name} onChange={(e) => onNameChange(e.target.value)} placeholder="es. ACME Industries"/>
            </div>
            <div className="grid grid-2">
              <div className="field"><label>ID <span style={{color:'var(--err)'}}>*</span></label>
                <input value={form.id} onChange={(e) => set('id', e.target.value)} placeholder="acme-industries" style={{fontFamily:'var(--font-mono)'}}/>
                <div style={{ fontSize: 10, color: 'var(--text-3)' }}>lowercase + dash, no `tenant-default`</div>
              </div>
              <div className="field"><label>Slug <span style={{color:'var(--err)'}}>*</span></label>
                <input value={form.slug} onChange={(e) => set('slug', e.target.value)} placeholder="acme" style={{fontFamily:'var(--font-mono)'}}/>
                <div style={{ fontSize: 10, color: 'var(--text-3)' }}>max 33 char, usato per URL futuri</div>
              </div>
            </div>
            <div className="field"><label>Description</label>
              <input value={form.description} onChange={(e) => set('description', e.target.value)} placeholder="opzionale"/>
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Admin onboarding (login del primo utente)</div>
          <div className="card" style={{ padding: 12 }}>
            <div className="field"><label>Nome admin <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.adminName} onChange={(e) => set('adminName', e.target.value)} placeholder="es. Mario Rossi"/>
            </div>
            <div className="grid grid-2">
              <div className="field"><label>Email <span style={{color:'var(--err)'}}>*</span></label>
                <input type="email" value={form.adminEmail} onChange={(e) => set('adminEmail', e.target.value)} placeholder="admin@acme.local"/>
              </div>
              <div className="field"><label>Password <span style={{color:'var(--err)'}}>*</span></label>
                <input type="password" value={form.adminPassword} onChange={(e) => set('adminPassword', e.target.value)} placeholder="min 8 char"/>
              </div>
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
              <Icon name="info" size={10}/> La password viene cifrata bcrypt cost=12. Comunicala fuori-banda all'admin: non sarà più recuperabile.
            </div>
          </div>
        </div>

        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

function TenantEditModal({ tenant, onClose, onSaved }) {
  const { user, pushToast } = useStore();
  const [form, setForm] = React.useState({
    name: tenant.name,
    slug: tenant.slug,
    description: tenant.description || '',
    active: tenant.active,
  });
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
  const isDirty =
    form.name !== tenant.name ||
    form.slug !== tenant.slug ||
    (form.description || '') !== (tenant.description || '') ||
    form.active !== tenant.active;

  async function handleSave() {
    if (!isDirty || saving) return;
    setSaving(true); setError(null);
    try {
      const body = {};
      if (form.name !== tenant.name) body.name = form.name.trim();
      if (form.slug !== tenant.slug) body.slug = form.slug.trim();
      if ((form.description || '') !== (tenant.description || '')) body.description = form.description.trim() || null;
      if (form.active !== tenant.active) body.active = form.active;
      const r = await fetch(`/api/tenants/${encodeURIComponent(tenant.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msg = j.error === 'validation_error'
          ? `Validazione: ${(j.issues || []).map((i) => `${(i.path || []).join('.')} ${i.message}`).join(' · ')}`
          : j.error === 'not_superadmin'
            ? `Permesso negato: solo persona con role SUPERADMIN può eseguire questa operazione`
            : j.detail || j.error || `HTTP ${r.status}`;
        setError(msg);
        return;
      }
      pushToast({ title: 'Tenant aggiornato', tone: 'ok' });
      onSaved();
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal
      open
      onClose={onClose}
      title={`Tenant · ${tenant.name}`}
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
          <Btn variant="primary" size="sm" disabled={!isDirty || saving} onClick={handleSave}>
            {saving ? 'Salvataggio…' : (isDirty ? 'Salva' : 'Salvato')}
          </Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 12 }}>
        <div className="field"><label>ID (immutabile)</label>
          <div className="mono" style={{ padding: 8, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11 }}>{tenant.id}</div>
        </div>
        <div className="field"><label>Nome</label>
          <input value={form.name} onChange={(e) => set('name', e.target.value)}/>
        </div>
        <div className="field"><label>Slug</label>
          <input value={form.slug} onChange={(e) => set('slug', e.target.value)} style={{fontFamily:'var(--font-mono)'}}/>
        </div>
        <div className="field"><label>Description</label>
          <input value={form.description} onChange={(e) => set('description', e.target.value)}/>
        </div>
        <div className="field"><label>Stato</label>
          <select value={form.active ? 'on' : 'off'} onChange={(e) => set('active', e.target.value === 'on')}>
            <option value="on">Attivo</option>
            <option value="off">Disattivato</option>
          </select>
        </div>
        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// HardDeleteTenantModal — FASE 53 (sessione): typed-confirm anti-misclick.
// User deve typing l'id del tenant ESATTAMENTE prima di abilitare il bottone
// "Cancella definitivamente". DELETE /api/tenants/[id]?hard=true&confirm=<id>
// ============================================================
function HardDeleteTenantModal({ tenant, onClose, onDeleted }) {
  const { user, pushToast } = useStore();
  const [typedId, setTypedId] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const matches = typedId === tenant.id;

  async function execute() {
    if (!matches) return;
    setBusy(true);
    try {
      const r = await fetch(
        `/api/tenants/${encodeURIComponent(tenant.id)}?hard=true&confirm=${encodeURIComponent(tenant.id)}`,
        {
          method: 'DELETE',
          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
          credentials: 'same-origin',
        },
      );
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const isNotSuperadmin = j.error === 'not_superadmin';
        const isSystemProtected = j.error === 'system_tenant_protected';
        pushToast({
          title: isNotSuperadmin ? 'Permesso negato (SUPERADMIN richiesto)' :
                 isSystemProtected ? 'Tenant di sistema protetto' :
                 'Errore hard-delete',
          desc: j.detail || j.error || `HTTP ${r.status}`,
          tone: 'err',
        });
        setBusy(false);
        return;
      }
      onDeleted(j);
    } catch (e) {
      pushToast({ title: 'Errore hard-delete', desc: String(e), tone: 'err' });
      setBusy(false);
    }
  }

  return (
    <Modal open onClose={onClose} title="Hard-delete tenant — irreversibile" size="md" footer={(
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={busy}>Annulla</Btn>
        <Btn
          variant="primary"
          size="sm"
          disabled={!matches || busy}
          onClick={execute}
          style={matches ? { background: 'var(--err)', borderColor: 'var(--err)' } : undefined}
        >{busy ? 'Cancellazione in corso…' : 'Cancella definitivamente'}</Btn>
      </>
    )}>
      <div className="col" style={{ gap: 12 }}>
        <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 4, fontSize: 12, color: 'var(--err)' }}>
          ⚠ <b>Operazione irreversibile (GDPR data deletion)</b>: questa azione cancella in cascade tutte le righe del tenant nelle 37 tabelle tenant-scoped + tabelle satellite (project_milestone, project_document, alert, communication, archive_doc, signature_request). Nessun rollback possibile dopo conferma.
        </div>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          Tenant target:
          <ul style={{ marginTop: 6, paddingLeft: 16, fontSize: 11 }}>
            <li><b>ID:</b> <code>{tenant.id}</code></li>
            <li><b>Nome:</b> {tenant.name}</li>
            <li><b>Slug:</b> <code>{tenant.slug}</code></li>
            <li><b>Stato:</b> {tenant.active ? 'attivo' : 'disattivato'}</li>
          </ul>
        </div>
        <div className="field">
          <label>Per confermare, digita <code style={{ background: 'var(--bg-2)', padding: '1px 4px' }}>{tenant.id}</code></label>
          <input
            value={typedId}
            onChange={(e) => setTypedId(e.target.value)}
            placeholder={tenant.id}
            style={{ fontFamily: 'var(--font-mono)' }}
            autoFocus
            disabled={busy}
          />
          {typedId && !matches && (
            <div style={{ fontSize: 10.5, color: 'var(--err)', marginTop: 4 }}>
              Il valore digitato non matcha l'id del tenant.
            </div>
          )}
          {matches && (
            <div style={{ fontSize: 10.5, color: 'var(--ok)', marginTop: 4 }}>
              ✓ Conferma valida — bottone "Cancella" abilitato.
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// ImportTenantModal — FASE 53 (sessione): upload JSON + form id/slug/name
// target. POST /api/tenants/import con payload + targetTenant fields.
// ============================================================
function ImportTenantModal({ onClose, onImported }) {
  const { user, pushToast } = useStore();
  const [payload, setPayload] = React.useState(null); // JSON only — null per ZIP (server parse)
  const [zipFile, setZipFile] = React.useState(null); // sessione 60: File raw quando .zip
  const [zipMeta, setZipMeta] = React.useState(null); // {sourceTenantId, sourceName, sourceSlug} dedotti da filename
  const [filename, setFilename] = React.useState('');
  const [parseError, setParseError] = React.useState(null);
  const [mode, setMode] = React.useState('restore'); // FASE 56 (sessione): 'restore' | 'fork'
  const [form, setForm] = React.useState({ id: '', name: '', slug: '' });
  const [admin, setAdmin] = React.useState({ name: '', email: '', password: '' });
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  function set(k, v) { setForm((p) => ({ ...p, [k]: v })); }
  function setAd(k, v) { setAdmin((p) => ({ ...p, [k]: v })); }

  function onFile(e) {
    const f = e.target.files?.[0];
    if (!f) return;
    setFilename(f.name);
    setParseError(null);
    setPayload(null);
    setZipFile(null);
    setZipMeta(null);

    // Sessione 60: detect ZIP vs JSON via estensione + content-type
    const isZip = /\.zip$/i.test(f.name) || f.type === 'application/zip';
    if (isZip) {
      setZipFile(f);
      // Auto-derive da filename pattern `veridanto-tenant-<id>-<ts>.zip`
      // Solo per pre-popolare i field; il server farà la parse + Zod validation reale.
      const m = f.name.match(/^veridanto-tenant-(.+?)-\d{4}-\d{2}-\d{2}/i);
      const sourceId = m?.[1] ?? '';
      setZipMeta({ sourceTenantId: sourceId, sourceName: sourceId, sourceSlug: sourceId });
      const suffix = mode === 'fork' ? 'fork' : 'restored';
      if (sourceId) {
        setForm({
          id: `${sourceId}-${suffix}`.slice(0, 64),
          name: `${sourceId} (${suffix})`.slice(0, 120),
          slug: `${sourceId}-${suffix}`.slice(0, 33),
        });
      }
      return;
    }

    // JSON path (backward compat sessione 53)
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const j = JSON.parse(reader.result);
        if (j.version !== 1 || !j.tenant || !j.tables) {
          throw new Error('Payload non valido (manca version=1 o tenant/tables)');
        }
        setPayload(j);
        // Auto-derive id/name/slug suggested dal mode
        const suffix = mode === 'fork' ? 'fork' : 'restored';
        setForm({
          id: `${j.tenant.id}-${suffix}`.slice(0, 64),
          name: `${j.tenant.name} (${suffix})`.slice(0, 120),
          slug: `${j.tenant.slug}-${suffix}`.slice(0, 33),
        });
      } catch (err) {
        setParseError(String(err?.message || err));
        setPayload(null);
      }
    };
    reader.onerror = () => setParseError('Errore lettura file');
    reader.readAsText(f);
  }

  // Re-derive id/name/slug suggested quando cambia mode
  React.useEffect(() => {
    const suffix = mode === 'fork' ? 'fork' : 'restored';
    if (payload) {
      setForm({
        id: `${payload.tenant.id}-${suffix}`.slice(0, 64),
        name: `${payload.tenant.name} (${suffix})`.slice(0, 120),
        slug: `${payload.tenant.slug}-${suffix}`.slice(0, 33),
      });
    } else if (zipMeta?.sourceTenantId) {
      setForm({
        id: `${zipMeta.sourceTenantId}-${suffix}`.slice(0, 64),
        name: `${zipMeta.sourceName} (${suffix})`.slice(0, 120),
        slug: `${zipMeta.sourceSlug}-${suffix}`.slice(0, 33),
      });
    }
  }, [mode, payload, zipMeta]);

  async function execute() {
    if (!payload && !zipFile) return;
    setBusy(true);
    setError(null);
    try {
      let r;
      if (zipFile) {
        // Sessione 60 — ZIP mode multipart/form-data
        const fd = new FormData();
        fd.append('file', zipFile);
        fd.append('targetTenantId', form.id);
        fd.append('targetTenantName', form.name);
        fd.append('targetTenantSlug', form.slug);
        fd.append('mode', mode);
        if (mode === 'fork') {
          fd.append('adminName', admin.name);
          fd.append('adminEmail', admin.email);
          fd.append('adminPassword', admin.password);
        }
        r = await fetch('/api/tenants/import?format=zip', {
          method: 'POST',
          headers: {
            // NB: NON settare Content-Type per multipart — il browser lo fa con boundary
            ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}),
          },
          credentials: 'same-origin',
          body: fd,
        });
      } else {
        // JSON mode (backward compat sessione 53)
        const body = {
          payload,
          targetTenantId: form.id,
          targetTenantName: form.name,
          targetTenantSlug: form.slug,
          mode,
        };
        if (mode === 'fork') {
          body.admin = {
            name: admin.name,
            email: admin.email,
            password: admin.password,
          };
        }
        r = await fetch('/api/tenants/import', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}),
          },
          credentials: 'same-origin',
          body: JSON.stringify(body),
        });
      }
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const detail = j.detail || j.error || `HTTP ${r.status}`;
        const msgs = {
          tenant_exists: detail,
          slug_exists: detail,
          admin_email_exists: detail,
          not_superadmin: 'Permesso negato — solo SUPERADMIN può importare tenant.',
          validation_error: 'Dati non validi: verifica id/slug/email/password.',
          zip_invalid: 'File ZIP non valido o corrotto.',
          manifest_missing: 'Lo ZIP non contiene manifest.json.',
          manifest_invalid_json: 'Il manifest.json non è un JSON valido.',
          manifest_invalid_schema: 'Il manifest.json non rispetta lo schema export.',
          missing_file: 'File ZIP mancante.',
          invalid_multipart: 'Errore nel parsing multipart.',
        };
        setError(msgs[j.error] || detail);
        setBusy(false);
        return;
      }
      onImported(j);
    } catch (e) {
      setError(String(e?.message || e));
      setBusy(false);
    }
  }

  const tableCount = payload ? Object.keys(payload.tables || {}).length + Object.keys(payload.satellites || {}).length : 0;
  const totalRows = payload ? [
    ...Object.values(payload.tables || {}),
    ...Object.values(payload.satellites || {}),
  ].reduce((acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0), 0) : 0;

  const adminValid = mode !== 'fork' || (admin.name.length >= 2 && admin.email.includes('@') && admin.password.length >= 8);
  const fileReady = payload || zipFile;
  const canSubmit = fileReady && form.id && form.name && form.slug && adminValid && !busy;

  return (
    <Modal open onClose={onClose} title="Importa tenant da export (JSON o ZIP)" size="md" footer={(
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={busy}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!canSubmit} onClick={execute}>
          {busy ? 'Importazione in corso…' : 'Importa tenant'}
        </Btn>
      </>
    )}>
      <div className="col" style={{ gap: 12 }}>
        <div className="field">
          <label>File di export (JSON metadata-only oppure ZIP completo con blob)</label>
          <input type="file" accept="application/json,.json,application/zip,.zip" onChange={onFile} disabled={busy}/>
          {filename && (
            <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
              📄 {filename}{zipFile ? ' · ZIP completo (manifest + blob storage)' : ''}
            </div>
          )}
          {parseError && <div style={{ fontSize: 10.5, color: 'var(--err)', marginTop: 4 }}>⚠ {parseError}</div>}
        </div>

        {fileReady && (
          <div className="field">
            <label>Modalità import</label>
            <div className="col" style={{ gap: 6, fontSize: 11 }}>
              <label style={{ display: 'flex', alignItems: 'flex-start', gap: 6, cursor: 'pointer' }}>
                <input type="radio" name="import-mode" value="restore" checked={mode === 'restore'} onChange={() => setMode('restore')} disabled={busy} style={{ marginTop: 2 }}/>
                <div>
                  <b>Restore</b> (disaster recovery, stesso tenant)
                  <div style={{ color: 'var(--text-3)', fontSize: 10 }}>
                    Re-importa tutti i dati con id originali. Pre-condizione: il target deve essere VUOTO (es. dopo hard-delete del source). Nessun admin atomic creato — usa identità originali.
                  </div>
                </div>
              </label>
              <label style={{ display: 'flex', alignItems: 'flex-start', gap: 6, cursor: 'pointer' }}>
                <input type="radio" name="import-mode" value="fork" checked={mode === 'fork'} onChange={() => setMode('fork')} disabled={busy} style={{ marginTop: 2 }}/>
                <div>
                  <b>Fork</b> (clone tenant template, id diversi)
                  <div style={{ color: 'var(--text-3)', fontSize: 10 }}>
                    Clona solo config (org, classification, workflow, branding) con id riscritti `f-&lt;slug&gt;-...`. Skippa identità (persona/role/auth) + storia operativa (audit/log/runtime). Crea admin atomic per il nuovo tenant.
                  </div>
                </div>
              </label>
            </div>
          </div>
        )}

        {payload && (
          <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11 }}>
            <div><b>Source tenant:</b> <code>{payload.sourceTenantId}</code> ({payload.tenant.name})</div>
            <div><b>Exported at:</b> <code>{payload.exportedAt}</code></div>
            <div><b>Tables:</b> {tableCount} · <b>Total rows:</b> {totalRows}</div>
          </div>
        )}

        {zipFile && (
          <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11 }}>
            <div><b>ZIP completo</b> — il server validerà manifest + ricaricherà i blob in storage</div>
            <div><b>Source tenant (dedotto da filename):</b> <code>{zipMeta?.sourceTenantId || '(specificare manualmente)'}</code></div>
            <div><b>File size:</b> {(zipFile.size / 1024).toFixed(1)} KB</div>
          </div>
        )}

        {fileReady && (
          <>
            <div className="field"><label>ID tenant target</label>
              <input
                value={form.id}
                onChange={(e) => set('id', e.target.value.toLowerCase())}
                placeholder="es. acme-restored"
                style={{ fontFamily: 'var(--font-mono)' }}
                disabled={busy}
              />
              <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 2 }}>
                lowercase + numeri/dash, inizia per lettera, max 64. NON può essere <code>tenant-default</code>.
              </div>
            </div>
            <div className="field"><label>Nome tenant target</label>
              <input value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="es. ACME Corp (Restored)" disabled={busy}/>
            </div>
            <div className="field"><label>Slug tenant target</label>
              <input
                value={form.slug}
                onChange={(e) => set('slug', e.target.value.toLowerCase())}
                placeholder="es. acme-restored"
                style={{ fontFamily: 'var(--font-mono)' }}
                disabled={busy}
              />
              <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 2 }}>
                lowercase + numeri/dash, max 33.
              </div>
            </div>

            {mode === 'fork' && (
              <div className="card" style={{ padding: 10 }}>
                <div className="eyebrow" style={{ marginBottom: 6 }}>Admin del nuovo tenant fork</div>
                <div className="col" style={{ gap: 8 }}>
                  <div className="field"><label>Nome admin</label>
                    <input value={admin.name} onChange={(e) => setAd('name', e.target.value)} placeholder="es. Mario Rossi" disabled={busy}/>
                  </div>
                  <div className="field"><label>Email admin (per login)</label>
                    <input type="email" value={admin.email} onChange={(e) => setAd('email', e.target.value.toLowerCase())} placeholder="admin@acme-fork.local" disabled={busy} style={{ fontFamily: 'var(--font-mono)' }}/>
                  </div>
                  <div className="field"><label>Password admin (min 8 char)</label>
                    <input type="password" value={admin.password} onChange={(e) => setAd('password', e.target.value)} placeholder="••••••••" disabled={busy}/>
                  </div>
                </div>
              </div>
            )}
          </>
        )}

        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// PromoteSuperadminModal — FASE 55 (sessione 55): promuove persona a SUPERADMIN.
// Usa PersonaAutocomplete per cercare la persona target.
// ============================================================
function PromoteSuperadminModal({ onClose, onPromoted }) {
  const { user, pushToast } = useStore();
  const [selectedPersonaId, setSelectedPersonaId] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  async function execute() {
    if (!selectedPersonaId) return;
    setBusy(true);
    setError(null);
    try {
      const r = await fetch(`/api/admin/superadmins/${encodeURIComponent(selectedPersonaId)}/promote`, {
        method: 'POST',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        credentials: 'same-origin',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msgs = {
          persona_not_found: 'Persona non trovata.',
          persona_inactive: 'Persona inattiva: riattivala prima di promuovere.',
          not_superadmin: 'Permesso negato (SUPERADMIN richiesto).',
          invalid_persona_id: 'Persona id non valido.',
        };
        setError(msgs[j.error] || j.error || `HTTP ${r.status}`);
        setBusy(false);
        return;
      }
      onPromoted(j);
    } catch (e) {
      setError(String(e?.message || e));
      setBusy(false);
    }
  }

  return (
    <Modal open onClose={onClose} title="Promuovi nuova persona a SUPERADMIN" size="md" footer={(
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={busy}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!selectedPersonaId || busy} onClick={execute}>
          {busy ? 'Promozione…' : 'Promuovi a SUPERADMIN'}
        </Btn>
      </>
    )}>
      <div className="col" style={{ gap: 12 }}>
        <div style={{ padding: 10, background: 'color-mix(in oklch, var(--accent) 12%, var(--bg-1))', border: '1px solid var(--accent)', borderRadius: 4, fontSize: 12 }}>
          <Icon name="info" size={11}/> Il role SUPERADMIN abilita operazioni cross-tenant (creazione tenant, statistiche aggregate, hard-delete GDPR, gestione altri super-admin). La persona resta nel suo tenant ma può "vedere come" altri tenant via switcher.
        </div>
        <div className="field">
          <label>Persona da promuovere</label>
          <PersonaAutocomplete
            value={selectedPersonaId}
            onChange={(id) => setSelectedPersonaId(id || '')}
            placeholder="Cerca persona per nome o email…"
          />
          <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 4 }}>
            La promozione è idempotente: se la persona è già SUPERADMIN, l'operazione no-op.
          </div>
        </div>
        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// RetentionPruneModal — FASE 53.2 (sessione 58): pruning manuale del ledger
// `tenant_lifecycle_event` per retention policy GDPR. Default 6 anni (2190
// giorni). Dry-run prima di esecuzione reale.
// ============================================================
function RetentionPruneModal({ onClose, onPruned }) {
  const { user, pushToast } = useStore();
  const [retentionDays, setRetentionDays] = React.useState(2190); // RETENTION_DEFAULT_DAYS
  const [busy, setBusy] = React.useState(false);
  const [dryRunResult, setDryRunResult] = React.useState(null);
  const [error, setError] = React.useState(null);

  async function execute(dryRun) {
    setBusy(true);
    setError(null);
    if (dryRun) setDryRunResult(null);
    try {
      const r = await fetch('/api/admin/tenant-lifecycle-events/prune', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}),
        },
        credentials: 'same-origin',
        body: JSON.stringify({ retentionDays, dryRun }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msgs = {
          not_superadmin: 'Permesso negato (SUPERADMIN richiesto).',
          validation_error: 'retentionDays out of range [1..9125].',
        };
        setError(msgs[j.error] || j.detail || j.error || `HTTP ${r.status}`);
        setBusy(false);
        return;
      }
      if (dryRun) {
        setDryRunResult(j);
        setBusy(false);
      } else {
        onPruned(j);
      }
    } catch (e) {
      setError(String(e?.message || e));
      setBusy(false);
    }
  }

  return (
    <Modal open onClose={onClose} title="Retention prune — tenant_lifecycle_event" size="md" footer={(
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={busy}>Annulla</Btn>
        <Btn variant="ghost" size="sm" disabled={busy} onClick={() => execute(true)}>
          {busy ? '…' : 'Dry-run (count only)'}
        </Btn>
        <Btn
          variant="primary"
          size="sm"
          disabled={busy || !dryRunResult}
          onClick={() => execute(false)}
          style={{ background: 'var(--err)', borderColor: 'var(--err)' }}
        >{busy ? 'Pruning…' : 'Esegui prune (irreversibile)'}</Btn>
      </>
    )}>
      <div className="col" style={{ gap: 12 }}>
        <div style={{ padding: 10, background: 'color-mix(in oklch, var(--accent) 12%, var(--bg-1))', border: '1px solid var(--accent)', borderRadius: 4, fontSize: 12 }}>
          <Icon name="info" size={11}/> Cancella le righe del ledger lifecycle più vecchie di N giorni. Default 6 anni (2190gg) per compliance GDPR Art. 5(1)(e). Sempre eseguire <b>dry-run</b> prima per verificare il count.
        </div>
        <div className="field">
          <label>Retention days (1..9125)</label>
          <input
            type="number"
            min={1}
            max={9125}
            value={retentionDays}
            onChange={(e) => { setRetentionDays(parseInt(e.target.value, 10) || 0); setDryRunResult(null); }}
            disabled={busy}
            style={{ fontFamily: 'var(--font-mono)' }}
          />
          <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 2 }}>
            Es. 2190 = 6 anni (default GDPR), 365 = 1 anno, 30 = 1 mese.
          </div>
        </div>
        {dryRunResult && (
          <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11 }}>
            <div><b>Dry-run result:</b></div>
            <div style={{ marginTop: 4, fontFamily: 'var(--font-mono)' }}>
              <div>retentionDays: {dryRunResult.retentionDays}</div>
              <div>thresholdTs: {dryRunResult.thresholdTs}</div>
              <div>totalCountBefore: {dryRunResult.totalCountBefore}</div>
              <div style={{ color: dryRunResult.deletedCount > 0 ? 'var(--warn)' : 'var(--text-3)' }}>
                righe da cancellare: <b>{dryRunResult.deletedCount}</b>
              </div>
              <div>totalCountAfter (preview): {dryRunResult.totalCountAfter}</div>
            </div>
            {dryRunResult.deletedCount === 0 && (
              <div style={{ marginTop: 6, color: 'var(--text-3)', fontSize: 10 }}>
                Nessuna riga da pruning con questa retention. Niente da fare.
              </div>
            )}
          </div>
        )}
        {error && (
          <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            ⚠ {error}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// CustToolExecutions — Sessione 62 — UI append-only ledger AI tool executions
// ============================================================
//
// Read-only dashboard su `/api/tool-executions` (lib `tool-executed-audit`).
// KPI strip totale / per-status / write vs read / avgLatencyMs.
// Filtri: toolName · status · isWrite · dateFrom/To · actorPersonaId.
// Lista paginata con expand row per vedere input/output summary truncated.
//
// Nessuna mutation: l'audit ledger è immutabile by design (pattern
// `provenance-ledger-vs-audit-log` sessione 33). Per retention vedi futuro
// quick win prune (analogo a tenant_lifecycle_event sessione 58).

const TOOL_STATUS_TONES = { ok: 'ok', error: 'err', threw: 'err' };
const TOOL_STATUS_LABELS = { ok: 'success', error: 'errore', threw: 'crash' };

function CustToolExecutions() {
  const { user } = useStore();
  const [data, setData] = React.useState(null);
  const [filters, setFilters] = React.useState({
    toolName: '', status: '', actorPersonaId: '', isWrite: '',
  });
  const [expanded, setExpanded] = React.useState(null);

  const reload = React.useCallback(async () => {
    setData(null);
    try {
      const params = new URLSearchParams();
      if (filters.toolName) params.set('toolName', filters.toolName);
      if (filters.status) params.set('status', filters.status);
      if (filters.actorPersonaId) params.set('actorPersonaId', filters.actorPersonaId);
      if (filters.isWrite === 'true' || filters.isWrite === 'false') params.set('isWrite', filters.isWrite);
      params.set('limit', '200');
      const r = await fetch('/api/tool-executions?' + params.toString(), {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      setData(j);
    } catch (e) {
      setData({ data: [], total: 0, kpi: null, error: String(e?.message || e) });
    }
  }, [filters, user?.id]);

  React.useEffect(() => { reload(); }, [reload]);

  const kpi = data?.kpi;
  const rows = data?.data || [];
  const setF = (k, v) => setFilters((f) => ({ ...f, [k]: v }));

  // Top-3 tool by usage count (calc lato FE da rows visibili)
  const topTools = React.useMemo(() => {
    const counts = new Map();
    for (const r of rows) counts.set(r.toolName, (counts.get(r.toolName) || 0) + 1);
    return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
  }, [rows]);

  return (
    <div className="col" style={{ gap: 14 }}>
      {/* KPI strip */}
      {kpi && (
        <div className="grid grid-4" style={{ gap: 10 }}>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Totale execution</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4 }}>{kpi.total}</div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Status (ok / error / threw)</div>
            <div style={{ fontSize: 14, marginTop: 4 }}>
              <span style={{ color: 'var(--ok)' }}>{kpi.byStatus.ok}</span>
              <span style={{ color: 'var(--text-3)' }}> · </span>
              <span style={{ color: 'var(--err)' }}>{kpi.byStatus.error}</span>
              <span style={{ color: 'var(--text-3)' }}> · </span>
              <span style={{ color: 'var(--err)', fontWeight: 600 }}>{kpi.byStatus.threw}</span>
            </div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Write / Read</div>
            <div style={{ fontSize: 14, marginTop: 4 }}>
              <span style={{ color: 'var(--warn)' }}>{kpi.byIsWrite.write}</span>
              <span style={{ color: 'var(--text-3)' }}> write · </span>
              <span>{kpi.byIsWrite.read}</span>
              <span style={{ color: 'var(--text-3)' }}> read</span>
            </div>
          </div>
          <div className="card" style={{ padding: 12 }}>
            <div className="eyebrow">Latency media</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: kpi.avgLatencyMs > 1000 ? 'var(--warn)' : 'var(--text-1)' }}>
              {kpi.avgLatencyMs}<span style={{ fontSize: 12, color: 'var(--text-3)' }}> ms</span>
            </div>
          </div>
        </div>
      )}

      {/* Top tools chips (visible rows) */}
      {topTools.length > 0 && (
        <div className="row" style={{ gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
          <span className="eyebrow">Top tool (visible):</span>
          {topTools.map(([name, count]) => (
            <Chip key={name} kind="">{name} · {count}</Chip>
          ))}
        </div>
      )}

      {/* Filtri */}
      <div className="row" style={{ gap: 10, alignItems: 'flex-end', flexWrap: 'wrap' }}>
        <div className="field" style={{ minWidth: 160 }}>
          <label style={{ fontSize: 10 }}>Tool name</label>
          <input
            type="text"
            value={filters.toolName}
            onChange={(e) => setF('toolName', e.target.value)}
            placeholder="es. flag_anomaly"
            style={{ fontFamily: 'var(--font-mono)' }}
          />
        </div>
        <div className="field" style={{ minWidth: 120 }}>
          <label style={{ fontSize: 10 }}>Status</label>
          <select value={filters.status} onChange={(e) => setF('status', e.target.value)}>
            <option value="">— tutti —</option>
            <option value="ok">ok</option>
            <option value="error">error</option>
            <option value="threw">threw</option>
          </select>
        </div>
        <div className="field" style={{ minWidth: 120 }}>
          <label style={{ fontSize: 10 }}>Tipo</label>
          <select value={filters.isWrite} onChange={(e) => setF('isWrite', e.target.value)}>
            <option value="">— tutti —</option>
            <option value="true">write only</option>
            <option value="false">read only</option>
          </select>
        </div>
        <div className="field" style={{ minWidth: 160 }}>
          <label style={{ fontSize: 10 }}>Persona ID</label>
          <input
            type="text"
            value={filters.actorPersonaId}
            onChange={(e) => setF('actorPersonaId', e.target.value)}
            placeholder="es. u01"
            style={{ fontFamily: 'var(--font-mono)' }}
          />
        </div>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm" onClick={reload}>
          <Icon name="refresh" size={11}/> Reload
        </Btn>
      </div>

      {/* Tabella */}
      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        {data === null ? (
          <div style={{ padding: 16, textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>Caricamento…</div>
        ) : data.error ? (
          <div style={{ padding: 16, color: 'var(--err)', fontSize: 12 }}>Errore: {data.error}</div>
        ) : rows.length === 0 ? (
          <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12 }}>
            Nessuna tool execution corrispondente ai filtri. Le execution vengono registrate ad ogni <code>runTool()</code> dal dispatcher AI (FASE 11.C+).
          </div>
        ) : (
          <table className="tbl dense">
            <thead>
              <tr>
                <th style={{ width: 110 }}>Quando</th>
                <th style={{ width: 160 }}>Tool</th>
                <th style={{ width: 70 }}>Status</th>
                <th style={{ width: 60 }}>Tipo</th>
                <th style={{ width: 80 }}>Latency</th>
                <th style={{ width: 100 }}>Persona</th>
                <th>Error / summary</th>
                <th style={{ width: 30 }}/>
              </tr>
            </thead>
            <tbody>
              {rows.map((r) => (
                <React.Fragment key={r.id}>
                  <tr className="clickable" onClick={() => setExpanded(expanded === r.id ? null : r.id)}>
                    <td className="mono" style={{ fontSize: 10.5 }}>
                      {new Date(r.executedAt).toLocaleString('it-IT', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}
                    </td>
                    <td><code style={{ fontSize: 11 }}>{r.toolName}</code></td>
                    <td><Chip kind={TOOL_STATUS_TONES[r.status] || ''} dot>{TOOL_STATUS_LABELS[r.status] || r.status}</Chip></td>
                    <td>
                      <Chip kind={r.isWrite ? 'warn' : ''}>{r.isWrite ? 'WRITE' : 'read'}</Chip>
                    </td>
                    <td className="num mono" style={{ fontSize: 11 }}>
                      <span style={{ color: r.latencyMs > 1000 ? 'var(--warn)' : 'var(--text-2)' }}>{r.latencyMs} ms</span>
                    </td>
                    <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.actorPersonaId || '(system)'}</td>
                    <td style={{ fontSize: 11 }}>
                      {r.errorCode
                        ? <span style={{ color: 'var(--err)', fontFamily: 'var(--font-mono)', fontSize: 10.5 }}>{r.errorCode.slice(0, 80)}{r.errorCode.length > 80 ? '…' : ''}</span>
                        : <span style={{ color: 'var(--text-3)' }}>{(r.outputSummary || '').slice(0, 80)}{(r.outputSummary || '').length > 80 ? '…' : ''}</span>
                      }
                    </td>
                    <td style={{ textAlign: 'right' }}>
                      <Icon name={expanded === r.id ? 'chevron-down' : 'chevron-right'} size={11}/>
                    </td>
                  </tr>
                  {expanded === r.id && (
                    <tr>
                      <td colSpan={8} style={{ background: 'var(--bg-2)', padding: 12 }}>
                        <div className="grid grid-2" style={{ gap: 12 }}>
                          <div>
                            <div className="eyebrow">Input summary (truncated 500 char)</div>
                            <pre style={{ fontSize: 10.5, padding: 8, background: 'var(--bg-1)', borderRadius: 4, maxHeight: 200, overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
                              {r.inputSummary || <em style={{ color: 'var(--text-3)' }}>(no input)</em>}
                            </pre>
                          </div>
                          <div>
                            <div className="eyebrow">Output summary (truncated 500 char)</div>
                            <pre style={{ fontSize: 10.5, padding: 8, background: 'var(--bg-1)', borderRadius: 4, maxHeight: 200, overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
                              {r.outputSummary || <em style={{ color: 'var(--text-3)' }}>(no output)</em>}
                            </pre>
                          </div>
                        </div>
                        {r.errorCode && (
                          <div style={{ marginTop: 8 }}>
                            <div className="eyebrow">Error code (truncated 200 char)</div>
                            <pre style={{ fontSize: 10.5, padding: 8, background: 'color-mix(in oklch, var(--err) 8%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 4, color: 'var(--err)', whiteSpace: 'pre-wrap' }}>
                              {r.errorCode}
                            </pre>
                          </div>
                        )}
                        <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 8, fontFamily: 'var(--font-mono)' }}>
                          ID: {r.id} · tenant: {r.tenantId}
                        </div>
                      </td>
                    </tr>
                  )}
                </React.Fragment>
              ))}
            </tbody>
          </table>
        )}
      </div>

      <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4, lineHeight: 1.5 }}>
        <Icon name="info" size={10}/> Audit append-only di ogni <code>runTool()</code> invocato dal dispatcher AI (FASE 11.C). Input/output salvati come summary truncated 500 char (audit safe-by-design, mai prompt completi). Lo schema è in <code>tool_executed_audit</code> (sessione 62).
      </div>
    </div>
  );
}

// ============================================================================
// FASE 22.A2 (sessione 76) — Audit log hash chain integrity panel.
//
// Bottone "Verifica chain" che chiama GET /api/admin/audit/verify-chain
// (SUPERADMIN only) → mostra status OK/tampered + breakdown schema versions
// + tabella mismatch dettagliata. Default scansiona ultime 5000 righe;
// per verifica esaustiva usare CLI `tsx apps/web/scripts/audit-verify.ts`.
// ============================================================================
function AuditChainVerifyPanel() {
  const { user } = useStore();
  const [running, setRunning] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [limit, setLimit] = React.useState(5000);

  async function runVerify() {
    if (running) return;
    setRunning(true);
    setError(null);
    setResult(null);
    try {
      const url = new URL('/api/admin/audit/verify-chain', window.location.origin);
      url.searchParams.set('limit', String(limit));
      const r = await fetch(url.toString(), {
        credentials: 'same-origin',
        headers: { ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        cache: 'no-store',
      });
      const j = await r.json();
      if (!r.ok) {
        setError(j.error || `HTTP ${r.status}`);
        return;
      }
      setResult(j);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setRunning(false);
    }
  }

  return (
    <div style={{ marginBottom: 18 }} data-testid="audit-chain-verify-panel">
      <div className="row" style={{ alignItems: 'center', marginBottom: 6 }}>
        <div className="eyebrow">Audit log hash chain (tamper-evidence)</div>
        <span className="spacer"/>
        <div className="row" style={{ gap: 6, alignItems: 'center' }}>
          <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>Ultime</span>
          <select
            value={limit}
            onChange={(e) => setLimit(Number(e.target.value))}
            style={{ fontSize: 11, padding: '2px 6px' }}
            disabled={running}
            data-testid="audit-chain-verify-limit"
          >
            <option value={500}>500</option>
            <option value={1000}>1.000</option>
            <option value={5000}>5.000</option>
            <option value={10000}>10.000</option>
            <option value={50000}>50.000</option>
          </select>
          <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>righe</span>
          <Btn variant="primary" size="sm" onClick={runVerify} disabled={running} data-testid="audit-chain-verify-btn">
            <Icon name="shield" size={11}/> {running ? 'Verifica…' : 'Verifica chain'}
          </Btn>
        </div>
      </div>
      <div className="card" style={{ padding: 12 }}>
        {!result && !error && !running && (
          <div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.55 }}>
            Verifica integrità della hash chain SHA-256 dell'<code>audit_log</code>.
            Ogni riga calcola <code>hash_curr = sha256(hash_prev || canonical_payload)</code>;
            la verifica ricomputa la chain e segnala qualsiasi mismatch.
            Per audit completo su tutte le righe da terra (genesis): usa CLI
            <code> tsx apps/web/scripts/audit-verify.ts</code>.
          </div>
        )}
        {error && (
          <div style={{ padding: '8px 10px', border: '1px solid var(--err)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err)', fontSize: 12 }}>
            <strong>Errore:</strong> {error}
          </div>
        )}
        {result && (
          <div className="col" style={{ gap: 10 }} data-testid="audit-chain-verify-result">
            <div className="row" style={{ gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
              {result.ok ? (
                <Chip kind="ok" dot>chain integra</Chip>
              ) : (
                <Chip kind="err" dot data-testid="audit-chain-tampered-chip">tampered ({result.mismatchedRows.length})</Chip>
              )}
              <span style={{ fontSize: 11, color: 'var(--text-3)' }}>
                {result.totalRows} righe · range {result.scannedFrom}→{result.scannedTo} · {result.durationMs}ms
              </span>
              {result.windowed && <Chip>finestra parziale</Chip>}
            </div>
            <div className="row" style={{ gap: 6, fontSize: 11, color: 'var(--text-2)', flexWrap: 'wrap' }}>
              <span>Schema v1 (pre-22.B): legacy</span>
              <span>·</span>
              <span>v2-tenant: caller esplicit</span>
              <span>·</span>
              <span>v2-null: caller omette tenantId</span>
            </div>
            <code className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', wordBreak: 'break-all' }}>
              lastHash: {result.lastHash}
            </code>
            {result.mismatchedRows.length > 0 && (
              <div className="card" style={{ padding: 8, background: 'rgba(192,57,43,0.04)', border: '1px solid rgba(192,57,43,0.2)' }}>
                <div className="eyebrow" style={{ marginBottom: 4 }}>Mismatch dettaglio ({result.mismatchedRows.length})</div>
                <table className="tbl dense" style={{ fontSize: 11 }}>
                  <thead><tr>
                    <th style={{ width: 60 }}>id</th>
                    <th style={{ width: 140 }}>reason</th>
                    <th>expected (hash o ref)</th>
                    <th>actual</th>
                  </tr></thead>
                  <tbody>
                    {result.mismatchedRows.slice(0, 50).map((m) => (
                      <tr key={`${m.id}-${m.reason}`}>
                        <td className="mono">{m.id}</td>
                        <td><Chip kind={m.reason === 'hash_curr_mismatch' ? 'err' : 'warn'}>{m.reason}</Chip></td>
                        <td className="mono" style={{ fontSize: 10, wordBreak: 'break-all' }}>{m.expectedHash.slice(0, 32)}…</td>
                        <td className="mono" style={{ fontSize: 10, wordBreak: 'break-all' }}>{m.actualHash.slice(0, 32)}…</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                {result.mismatchedRows.length > 50 && (
                  <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
                    … e altre {result.mismatchedRows.length - 50} mismatch. Usa CLI per dump completo.
                  </div>
                )}
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================================
// FASE 22.A3 (sessione 77) — Separation of Duties rules panel.
//
// Lista rule attive + count inattive + bottoni: nuova rule, edit, toggle
// active. Default visibili solo active=true; toggle "Mostra disattivate".
// 3 scope (self_action, role_pair, role_action_block) + 2 mode (block, warn).
// ============================================================================
const SOD_SCOPES = ['self_action', 'role_pair', 'role_action_block'];
const SOD_SCOPE_LABELS = {
  self_action: 'Self-action',
  role_pair: 'Role pair',
  role_action_block: 'Role action block',
};
const SOD_MODES = ['block', 'warn'];
const SOD_ACTION_HINTS = [
  'rda.approve',
  'rda.reject',
  'rda.submit',
  'rda.order',
  'rda.*',
  'workflow.transition.approve',
  'workflow.transition.reject',
  'workflow.transition.*',
];

function SodRulesPanel() {
  const { user, pushToast } = useStore();
  const [rules, setRules] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [showInactive, setShowInactive] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const [editTarget, setEditTarget] = React.useState(null);

  async function reload() {
    setLoading(true);
    setError(null);
    try {
      const params = showInactive ? '?includeInactive=true' : '';
      const r = await fetch(`/api/config/sod-rules${params}`, {
        credentials: 'same-origin',
        cache: 'no-store',
      });
      const j = await r.json();
      if (!r.ok) {
        setError(j.error || `HTTP ${r.status}`);
        return;
      }
      setRules(j.data || []);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setLoading(false);
    }
  }

  React.useEffect(() => { reload(); /* eslint-disable-line */ }, [showInactive]);

  async function toggleActive(rule) {
    if (rule.active) {
      // Soft delete
      const r = await fetch(`/api/config/sod-rules/${encodeURIComponent(rule.id)}`, {
        method: 'DELETE',
        credentials: 'same-origin',
        headers: { ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        pushToast({ title: 'Errore', desc: j.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'SoD rule disattivata', tone: 'ok' });
    } else {
      // Re-activate via PATCH
      const r = await fetch(`/api/config/sod-rules/${encodeURIComponent(rule.id)}`, {
        method: 'PATCH',
        credentials: 'same-origin',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ active: true }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        pushToast({ title: 'Errore', desc: j.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'SoD rule riattivata', tone: 'ok' });
    }
    reload();
  }

  const activeCount = rules.filter((r) => r.active).length;
  const blockCount = rules.filter((r) => r.active && r.mode === 'block').length;
  const warnCount = rules.filter((r) => r.active && r.mode === 'warn').length;

  return (
    <div style={{ marginBottom: 18 }} data-testid="sod-rules-panel">
      <div className="row" style={{ alignItems: 'center', marginBottom: 6 }}>
        <div className="eyebrow">Separation of Duties (compliance enterprise)</div>
        <span className="spacer"/>
        <Chip kind={activeCount > 0 ? 'ok' : 'info'} dot>{activeCount} attive</Chip>
        {blockCount > 0 && <Chip kind="err">{blockCount} block</Chip>}
        {warnCount > 0 && <Chip kind="warn">{warnCount} warn</Chip>}
        <label style={{ fontSize: 11, color: 'var(--text-3)', marginLeft: 8 }}>
          <input
            type="checkbox"
            checked={showInactive}
            onChange={(e) => setShowInactive(e.target.checked)}
            data-testid="sod-rules-show-inactive"
          /> mostra disattivate
        </label>
        <Btn variant="primary" size="sm" onClick={() => setShowNew(true)} data-testid="sod-rules-new-btn">
          <Icon name="plus" size={11}/> Nuova rule
        </Btn>
        <Btn variant="ghost" size="sm" onClick={reload}>
          <Icon name="refresh" size={11}/>
        </Btn>
      </div>
      <div className="card" style={{ padding: 12 }}>
        {loading && <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento…</div>}
        {error && (
          <div style={{ padding: '8px 10px', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            <strong>Errore:</strong> {error}
          </div>
        )}
        {!loading && !error && rules.length === 0 && (
          <div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.55 }}>
            Nessuna rule SoD configurata. Le rule definiscono vincoli "stesso utente non può
            sia X che Y" (es. self-approval, role pair conflict, role action block) valutati
            pre-azione su <code>rda.approve</code>, <code>workflow.transition.*</code>, ecc.
          </div>
        )}
        {!loading && !error && rules.length > 0 && (
          <table className="tbl dense" style={{ fontSize: 11.5 }}>
            <thead><tr>
              <th>Nome</th>
              <th style={{ width: 110 }}>Scope</th>
              <th style={{ width: 70 }}>Mode</th>
              <th>Actions</th>
              <th style={{ width: 110 }}>Caller role</th>
              <th style={{ width: 110 }}>Origin role</th>
              <th style={{ width: 70 }}>Stato</th>
              <th style={{ width: 100 }}></th>
            </tr></thead>
            <tbody>
              {rules.map((r) => (
                <tr key={r.id} data-testid={`sod-rule-row-${r.id}`}>
                  <td>
                    <div style={{ fontWeight: 500 }}>{r.name}</div>
                    {r.description && <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.description}</div>}
                  </td>
                  <td><Chip>{SOD_SCOPE_LABELS[r.scope] || r.scope}</Chip></td>
                  <td><Chip kind={r.mode === 'block' ? 'err' : 'warn'}>{r.mode}</Chip></td>
                  <td className="mono" style={{ fontSize: 10.5 }}>
                    {(r.actionPatterns || []).slice(0, 3).map((p) => (
                      <Chip key={p} style={{ marginRight: 4 }}>{p}</Chip>
                    ))}
                    {(r.actionPatterns || []).length > 3 && <span style={{ color: 'var(--text-3)' }}>+{r.actionPatterns.length - 3}</span>}
                  </td>
                  <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-2)' }}>{r.callerRoleId || '—'}</td>
                  <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-2)' }}>{r.originRoleId || '—'}</td>
                  <td>
                    {r.active ? <Chip kind="ok" dot>attiva</Chip> : <Chip>off</Chip>}
                  </td>
                  <td>
                    <div className="row" style={{ gap: 4 }}>
                      <Btn variant="ghost" size="sm" onClick={() => setEditTarget(r)} data-testid={`sod-rule-edit-${r.id}`}>
                        <Icon name="edit" size={10}/>
                      </Btn>
                      <Btn variant="ghost" size="sm" onClick={() => toggleActive(r)} data-testid={`sod-rule-toggle-${r.id}`}>
                        {r.active ? 'Disattiva' : 'Riattiva'}
                      </Btn>
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
      {showNew && (
        <SodRuleModal
          mode="new"
          onClose={() => setShowNew(false)}
          onDone={() => { setShowNew(false); reload(); }}
        />
      )}
      {editTarget && (
        <SodRuleModal
          mode="edit"
          rule={editTarget}
          onClose={() => setEditTarget(null)}
          onDone={() => { setEditTarget(null); reload(); }}
        />
      )}
    </div>
  );
}

function SodRuleModal({ mode, rule, onClose, onDone }) {
  const { user, pushToast } = useStore();
  const isEdit = mode === 'edit';
  const [form, setForm] = React.useState({
    name: rule?.name || '',
    description: rule?.description || '',
    scope: rule?.scope || 'self_action',
    mode: rule?.mode || 'block',
    actionPatterns: (rule?.actionPatterns || ['rda.approve']).join(', '),
    callerRoleId: rule?.callerRoleId || '',
    originRoleId: rule?.originRoleId || '',
  });
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));

  const patterns = form.actionPatterns.split(',').map((s) => s.trim()).filter(Boolean);
  const valid = form.name.trim() && patterns.length > 0;

  async function handleSubmit() {
    if (!valid || submitting) return;
    setSubmitting(true);
    setError(null);
    const body = {
      name: form.name.trim(),
      description: form.description.trim() || null,
      scope: form.scope,
      mode: form.mode,
      actionPatterns: patterns,
      callerRoleId: form.callerRoleId.trim() || null,
      originRoleId: form.originRoleId.trim() || null,
    };
    try {
      const url = isEdit
        ? `/api/config/sod-rules/${encodeURIComponent(rule.id)}`
        : '/api/config/sod-rules';
      const r = await fetch(url, {
        method: isEdit ? 'PATCH' : 'POST',
        credentials: 'same-origin',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify(body),
      });
      const j = await r.json();
      if (!r.ok) {
        setError(j.error === 'validation_error'
          ? `Validazione: ${(j.issues || []).map((i) => i.message).join(' · ')}`
          : (j.error || `HTTP ${r.status}`));
        return;
      }
      pushToast({ title: isEdit ? 'SoD rule aggiornata' : 'SoD rule creata', tone: 'ok' });
      onDone();
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <Modal open onClose={onClose} title={isEdit ? `Modifica rule: ${rule.name}` : 'Nuova SoD rule'} size="lg" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={submitting}>Annulla</Btn>
        <Btn variant="primary" size="sm" onClick={handleSubmit} disabled={!valid || submitting} data-testid="sod-rule-submit">
          {submitting ? 'Salvataggio…' : (isEdit ? 'Salva modifiche' : 'Crea rule')}
        </Btn>
      </>
    }>
      <div className="col" style={{ gap: 12 }}>
        {error && (
          <div style={{ padding: '8px 10px', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)', fontSize: 12 }}>
            <strong>Errore:</strong> {error}
          </div>
        )}
        <div className="field"><label>Nome</label>
          <input value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="es. No self-approval RdA" data-testid="sod-rule-name"/>
        </div>
        <div className="field"><label>Descrizione</label>
          <textarea rows={2} value={form.description} onChange={(e) => set('description', e.target.value)} placeholder="Cosa blocca / quando si applica"/>
        </div>
        <div className="grid grid-2">
          <div className="field"><label>Scope</label>
            <select value={form.scope} onChange={(e) => set('scope', e.target.value)} data-testid="sod-rule-scope">
              {SOD_SCOPES.map((s) => <option key={s} value={s}>{SOD_SCOPE_LABELS[s]}</option>)}
            </select>
          </div>
          <div className="field"><label>Mode</label>
            <select value={form.mode} onChange={(e) => set('mode', e.target.value)} data-testid="sod-rule-mode">
              {SOD_MODES.map((m) => <option key={m} value={m}>{m}</option>)}
            </select>
          </div>
        </div>
        <div className="field"><label>Action patterns (CSV)</label>
          <input value={form.actionPatterns} onChange={(e) => set('actionPatterns', e.target.value)} placeholder="rda.approve, workflow.transition.*"/>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
            Es. <code>{SOD_ACTION_HINTS.join(', ')}</code>. Glob terminale <code>*</code> per match prefisso.
          </div>
        </div>
        {(form.scope === 'role_pair' || form.scope === 'role_action_block') && (
          <div className="grid grid-2">
            <div className="field"><label>Caller role (vuoto = qualsiasi)</label>
              <input value={form.callerRoleId} onChange={(e) => set('callerRoleId', e.target.value)} placeholder="es. BUYER"/>
            </div>
            {form.scope === 'role_pair' && (
              <div className="field"><label>Origin role (vuoto = qualsiasi)</label>
                <input value={form.originRoleId} onChange={(e) => set('originRoleId', e.target.value)} placeholder="es. BUYER"/>
              </div>
            )}
          </div>
        )}
      </div>
    </Modal>
  );
}

Object.assign(window, {
  NewTemplateModalLive, NewClauseModalLive, NewWorkflowModalLive, NewMatrixRuleModalLive,
  CustAnomalies, AnomalyResolveModal,
  CustTenants, NewTenantModal, TenantEditModal,
  HardDeleteTenantModal, ImportTenantModal,
  PromoteSuperadminModal, RetentionPruneModal,
  CustToolExecutions,
  AuditChainVerifyPanel,
  SodRulesPanel, SodRuleModal,
});
