// ============================================================
// Settings.jsx — provider AI completi + connettori + governance
// ============================================================

// Catalogo modelli disponibili (Claude + Gemini + DeepSeek).
// NB: il catalogo è una guida non esaustiva. L'utente può digitare un model id
// custom nel campo "Custom model" sotto la lista per usare modelli non listati
// (es. modelli appena rilasciati o varianti experimental). Il backend accetta
// qualsiasi stringa: il provider stesso valida che l'id esista.
const MODEL_CATALOG = {
  claude: [
    { id: 'claude-opus-4-7',       name: 'Claude Opus 4.7',      tier: 'Flagship',  ctx: '1M',   out: '32K',  cost: '$15 / $75',  best: 'Ragionamento estremo · context 1M token', recommended: true },
    { id: 'claude-sonnet-4-6',     name: 'Claude Sonnet 4.6',    tier: 'Balanced',  ctx: '200K', out: '64K',  cost: '$3 / $15',   best: 'Coding agentic + tool use multi-turn' },
    { id: 'claude-haiku-4-5',      name: 'Claude Haiku 4.5',     tier: 'Fast',      ctx: '200K', out: '8K',   cost: '$1 / $5',    best: 'Bassa latenza, task brevi' },
    { id: 'claude-opus-4-1',       name: 'Claude Opus 4.1',      tier: 'Flagship',  ctx: '200K', out: '32K',  cost: '$15 / $75',  best: 'Reasoning legacy, analisi complesse' },
    { id: 'claude-sonnet-4-5',     name: 'Claude Sonnet 4.5',    tier: 'Balanced',  ctx: '200K', out: '64K',  cost: '$3 / $15',   best: 'Stabile · coding e agenti' },
    { id: 'claude-sonnet-4',       name: 'Claude Sonnet 4',      tier: 'Balanced',  ctx: '200K', out: '64K',  cost: '$3 / $15',   best: 'Generazione di lungo formato' },
    { id: 'claude-opus-4',         name: 'Claude Opus 4',        tier: 'Flagship',  ctx: '200K', out: '32K',  cost: '$15 / $75',  best: 'Analisi tecniche avanzate' },
    { id: 'claude-sonnet-3-7',     name: 'Claude Sonnet 3.7',    tier: 'Balanced',  ctx: '200K', out: '64K',  cost: '$3 / $15',   best: 'Reasoning esteso legacy' },
    { id: 'claude-haiku-3-5',      name: 'Claude Haiku 3.5',     tier: 'Fast',      ctx: '200K', out: '8K',   cost: '$0.80 / $4', best: 'Classificazione, routing' },
  ],
  gemini: [
    { id: 'gemini-2-5-pro',        name: 'Gemini 2.5 Pro',       tier: 'Flagship',  ctx: '1M',   out: '64K',  cost: '$1.25 / $10', best: 'Context lungo, multimodale', recommended: true },
    { id: 'gemini-2-5-flash',      name: 'Gemini 2.5 Flash',     tier: 'Balanced',  ctx: '1M',   out: '64K',  cost: '$0.30 / $2.50', best: 'Throughput alto, reasoning' },
    { id: 'gemini-2-5-flash-lite', name: 'Gemini 2.5 Flash-Lite',tier: 'Fast',      ctx: '1M',   out: '64K',  cost: '$0.10 / $0.40', best: 'Volumi massivi, cost-sensitive' },
    { id: 'gemini-2-0-flash',      name: 'Gemini 2.0 Flash',     tier: 'Balanced',  ctx: '1M',   out: '8K',   cost: '$0.10 / $0.40', best: 'Multimodale real-time' },
    { id: 'gemini-2-0-flash-lite', name: 'Gemini 2.0 Flash-Lite',tier: 'Fast',      ctx: '1M',   out: '8K',   cost: '$0.075 / $0.30', best: 'Latenza minima' },
    { id: 'gemini-1-5-pro',        name: 'Gemini 1.5 Pro',       tier: 'Flagship',  ctx: '2M',   out: '8K',   cost: '$1.25 / $5',    best: 'Legacy · context 2M token (deprecato Q3-2026)' },
  ],
  deepseek: [
    { id: 'deepseek-chat',         name: 'DeepSeek V3.x · Chat (alias latest)', tier: 'Balanced',  ctx: '128K', out: '8K',   cost: '$0.27 / $1.10', best: 'Default consigliato · alias auto-aggiornato', recommended: true },
    { id: 'deepseek-reasoner',     name: 'DeepSeek R1 · Reasoner (alias latest)',tier: 'Flagship', ctx: '128K', out: '8K',   cost: '$0.55 / $2.19', best: 'Ragionamento esteso (chain-of-thought visibile)' },
    { id: 'deepseek-v3.2-exp',     name: 'DeepSeek V3.2 Experimental',          tier: 'Balanced',  ctx: '128K', out: '8K',   cost: '$0.27 / $1.10', best: 'Versione fissa V3.2 (no auto-update)' },
    { id: 'deepseek-v3.1',         name: 'DeepSeek V3.1',                       tier: 'Balanced',  ctx: '128K', out: '8K',   cost: '$0.27 / $1.10', best: 'Versione fissa V3.1 stabile' },
  ],
};

// Connettori disponibili — integrazioni enterprise (catalog).
// Sessione 70 — rimosso stato fittizio 'connected' + lastSync hardcoded.
// Tutti gli adattatori sono in stato 'available' finché non saranno cablati
// con un'integrazione reale (FASE 21 connettori prod). Lo stato `connected`
// va attivato dall'utente solo quando la chiave/credentials è configurata
// (oggi è solo una flag locale; futura iterazione: stato persistito su DB).
const CONNECTOR_CATALOG = [
  { cat: 'ERP & Finance', items: [
    { id: 'sap-s4',      name: 'SAP S/4HANA',        icon: 'factory',  status: 'available', detail: 'OData v2/v4 · CPI gateway' },
    { id: 'oracle-nc',   name: 'Oracle NetSuite',    icon: 'coins',    status: 'available', detail: 'OAuth 2.0 · TBA' },
    { id: 'sapb1',       name: 'SAP Business One',   icon: 'package',  status: 'available', detail: 'Service Layer REST' },
    { id: 'dynamics',    name: 'MS Dynamics 365 F&O',icon: 'package',  status: 'available', detail: 'OData · Entra ID' },
  ]},
  { cat: 'Project & PMO', items: [
    { id: 'primavera',   name: 'Primavera P6',       icon: 'calendar', status: 'available', detail: 'REST API' },
    { id: 'msproject',   name: 'Microsoft Project',  icon: 'calendar', status: 'available', detail: 'Project Online · Graph API' },
    { id: 'jira',        name: 'Atlassian Jira',     icon: 'check',    status: 'available', detail: 'OAuth 2.0 (3LO)' },
    { id: 'asana',       name: 'Asana',              icon: 'check',    status: 'available', detail: 'Personal Access Token' },
  ]},
  { cat: 'Storage & documentale', items: [
    { id: 'sharepoint',  name: 'SharePoint Online',  icon: 'cloud',    status: 'available', detail: 'Graph API · sites/drives' },
    { id: 'gdrive',      name: 'Google Drive',       icon: 'cloud',    status: 'available', detail: 'OAuth 2.0 · Drive API' },
    { id: 'box',         name: 'Box',                icon: 'box',      status: 'available', detail: 'JWT / OAuth' },
    { id: 'dropbox',     name: 'Dropbox Business',   icon: 'box',      status: 'available', detail: 'OAuth 2.0' },
    { id: 's3',          name: 'AWS S3',             icon: 'cloud',    status: 'available', detail: 'IAM · SSE-KMS' },
  ]},
  { cat: 'Comunicazione', items: [
    { id: 'teams',       name: 'Microsoft Teams',    icon: 'chat',     status: 'available', detail: 'Graph API · channel notifications' },
    { id: 'slack',       name: 'Slack',              icon: 'chat',     status: 'available', detail: 'Bot + slash commands' },
    { id: 'outlook',     name: 'Outlook / Exchange', icon: 'mail',     status: 'available', detail: 'Graph API · mailbox' },
    { id: 'gmail',       name: 'Gmail Workspace',    icon: 'mail',     status: 'available', detail: 'Service account DWD' },
  ]},
  { cat: 'Firma & legale', items: [
    { id: 'docusign',    name: 'DocuSign',           icon: 'signature',status: 'available', detail: 'CLM REST API' },
    { id: 'adobesign',   name: 'Adobe Acrobat Sign', icon: 'signature',status: 'available', detail: 'REST v6' },
    { id: 'aruba',       name: 'Aruba Firma',        icon: 'signature',status: 'available', detail: 'CAdES · FEQ' },
  ]},
  { cat: 'Fiscale & payments', items: [
    { id: 'sdi',         name: 'SDI · FatturaPA',    icon: 'receipt',  status: 'available', detail: 'Passthrough SdI' },
    { id: 'stripe',      name: 'Stripe',             icon: 'coins',    status: 'available', detail: 'Pagamenti milestone' },
    { id: 'cbi',         name: 'CBI Globe',          icon: 'coins',    status: 'available', detail: 'PSD2 · bonifici SCT' },
  ]},
  { cat: 'Identità & Directory', items: [
    { id: 'entra',       name: 'Microsoft Entra ID', icon: 'shield',   status: 'available', detail: 'SAML 2.0 · SCIM' },
    { id: 'okta',        name: 'Okta',               icon: 'shield',   status: 'available', detail: 'OIDC + SCIM' },
  ]},
];

function ConnectorCard({ c, onToggle }) {
  const isOn = c.status === 'connected';
  return (
    <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 12, background: 'var(--bg-1)' }}>
      <div className="row" style={{ gap: 10, alignItems: 'flex-start' }}>
        <div style={{
          width: 36, height: 36, borderRadius: 8, background: 'var(--bg-2)',
          display: 'grid', placeItems: 'center', border: '1px solid var(--line)', flexShrink: 0,
        }}>
          <Icon name={c.icon} size={16}/>
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="row" style={{ gap: 6, alignItems: 'center' }}>
            <div style={{ fontSize: 12.5, fontWeight: 600 }}>{c.name}</div>
            <span className="spacer"/>
            {isOn
              ? <Chip kind="ok" dot>connesso</Chip>
              : <Chip>disponibile</Chip>}
          </div>
          <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.detail}</div>
          <div className="row" style={{ gap: 4, marginTop: 8 }}>
            {isOn
              ? <>
                  <Btn variant="ghost" size="sm" onClick={() => onToggle(c)}>Disconnetti</Btn>
                  <Btn variant="ghost" size="sm"><Icon name="refresh" size={11}/> Sync</Btn>
                  <Btn variant="ghost" size="sm"><Icon name="settings" size={11}/></Btn>
                </>
              : <>
                  <Btn variant="primary" size="sm" onClick={() => onToggle(c)}><Icon name="link" size={11}/> Connetti</Btn>
                  <Btn variant="ghost" size="sm"><Icon name="info" size={11}/> Dettagli</Btn>
                </>}
          </div>
        </div>
      </div>
    </div>
  );
}

function ModelRow({ m, selected, onSelect }) {
  return (
    <div
      className={`clickable ${selected ? 'is-selected' : ''}`}
      onClick={() => onSelect(m.id)}
      style={{
        display: 'grid',
        gridTemplateColumns: '16px 1fr auto',
        gap: 10, alignItems: 'center',
        padding: '8px 10px',
        border: '1px solid ' + (selected ? 'var(--accent)' : 'var(--line)'),
        borderRadius: 8,
        background: selected ? 'color-mix(in oklch, var(--accent) 8%, var(--bg-1))' : 'var(--bg-1)',
      }}
    >
      <div style={{
        width: 14, height: 14, borderRadius: '50%',
        border: '2px solid ' + (selected ? 'var(--accent)' : 'var(--line)'),
        background: selected ? 'var(--accent)' : 'transparent',
        boxShadow: selected ? 'inset 0 0 0 3px var(--bg-1)' : 'none',
      }}/>
      <div>
        <div className="row" style={{ gap: 6, alignItems: 'center' }}>
          <div style={{ fontSize: 12.5, fontWeight: 600 }}>{m.name}</div>
          <Chip>{m.tier}</Chip>
          {m.recommended && <Chip kind="ai">consigliato</Chip>}
        </div>
        <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 2 }}>{m.best}</div>
      </div>
      <div style={{ textAlign: 'right', fontSize: 10.5, color: 'var(--text-2)', lineHeight: 1.4 }}>
        <div className="mono">ctx {m.ctx} · out {m.out}</div>
        <div className="mono" style={{ color: 'var(--text-3)', marginTop: 1 }}>{m.cost} /Mtok</div>
      </div>
    </div>
  );
}

/**
 * AiUsageCard — sostituisce la mock "Budget & consumo" del template Claude Design.
 * Legge stat reali dall'audit_log via /api/ai/usage (fase 2c.3 sprint 2b).
 *
 * Dati visualizzati: chiamate del mese, success/error/fallback rate, latenza
 * media, breakdown per provider+modello con last-used. Spesa e token sono
 * marcati "—" finché FASE 11 non estenderà l'audit AI con i token usage reali.
 */
function AiUsageCard() {
  const [usage, setUsage] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  const reload = React.useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch('/api/ai/usage', { cache: 'no-store' });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      setUsage(json?.data ?? null);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setLoading(false);
    }
  }, []);

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

  return (
    <div className="card" style={{ gridColumn: '1 / -1' }}>
      <div className="card-header">
        <div className="title">Consumo AI · mese {usage?.month || '—'}</div>
        <div className="actions">
          <Btn variant="ghost" size="sm" onClick={reload} disabled={loading} title="Ricarica da audit_log">
            <Icon name="refresh" size={11}/> {loading ? 'Carico…' : 'Aggiorna'}
          </Btn>
        </div>
      </div>
      <div className="card-body">
        {error && (
          <div style={{ color: 'var(--err)', fontSize: 12, marginBottom: 10 }}>⚠ {error}</div>
        )}
        {loading && !usage && (
          <div style={{ color: 'var(--text-3)', fontSize: 12 }}>Caricamento…</div>
        )}
        {usage && (
          <>
            <div className="grid grid-4" style={{ gap: 14 }}>
              <Stat
                label="Chiamate AI (mese)"
                value={String(usage.calls.total)}
                delta={`${usage.calls.success} ok · ${usage.calls.error} err`}
                tone={usage.calls.error === 0 ? 'flat' : 'down'}
              />
              <Stat
                label="Latenza media"
                value={usage.latencyMs.avg ? `${usage.latencyMs.avg}ms` : '—'}
                delta={usage.calls.total > 0 ? `${usage.calls.total} req` : 'no traffic'}
                tone="flat"
              />
              <Stat
                label="Retry tra provider"
                value={String(usage.calls.fallback)}
                delta={usage.calls.fallback > 0 ? 'auto-retry attivato' : 'mai usato'}
                tone={usage.calls.fallback > 0 ? 'down' : 'flat'}
              />
              <Stat
                label="Errori"
                value={String(usage.calls.error)}
                delta={usage.calls.total > 0 ? `${Math.round((usage.calls.error / usage.calls.total) * 100)}% del totale` : '—'}
                tone={usage.calls.error > 0 ? 'down' : 'flat'}
              />
            </div>
            {usage.byProvider.length > 0 ? (
              <div style={{ marginTop: 14 }}>
                <table className="tbl dense">
                  <thead><tr><th>Provider</th><th>Modello</th><th>Chiamate</th><th>OK</th><th>Errori</th><th>Fallback</th><th>Latenza media</th><th>Ultimo uso</th></tr></thead>
                  <tbody>
                    {usage.byProvider.map((row, i) => (
                      <tr key={i}>
                        <td><Chip kind="ai">{row.provider}</Chip></td>
                        <td className="mono" style={{ fontSize: 11 }}>{row.model || '—'}</td>
                        <td className="mono">{row.calls}</td>
                        <td className="mono" style={{ color: 'var(--ok)' }}>{row.success}</td>
                        <td className="mono" style={{ color: row.errors > 0 ? 'var(--err)' : 'var(--text-3)' }}>{row.errors}</td>
                        <td className="mono" style={{ color: row.fallback > 0 ? 'var(--warn)' : 'var(--text-3)' }}>{row.fallback}</td>
                        <td className="mono">{row.latencyAvgMs > 0 ? `${row.latencyAvgMs}ms` : '—'}</td>
                        <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{row.lastUsedAt ? new Date(row.lastUsedAt).toLocaleString('it-IT') : '—'}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            ) : (
              <div style={{ marginTop: 14, padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--text-3)', background: 'var(--bg-2)', borderRadius: 6 }}>
                Nessuna chiamata AI registrata questo mese. Configura una API key in alto e fai un ping per popolare le statistiche.
              </div>
            )}
            <div style={{ marginTop: 12, fontSize: 10.5, color: 'var(--text-3)', padding: '6px 8px' }}>
              <Icon name="info" size={10}/> Sorgente: <code>audit_log</code> dove <code>entityType IN ('ai.complete', 'ai.ping')</code>. Token usage e spesa stimata arriveranno con FASE 11 (estensione audit AI con counter input/output tokens).
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function Settings() {
  const { theme, setTheme, aiSettings, setAiSettings, pushToast, user } = useStore();
  const [tab, setTab] = React.useState('ai');

  // Sessione 83 — Email Provider state
  const [emailServer, setEmailServer] = React.useState(null);
  const [emailPresets, setEmailPresets] = React.useState([]);
  const [emailLoading, setEmailLoading] = React.useState(true);
  const [emailForm, setEmailForm] = React.useState({
    presetId: 'resend', // default Resend (consigliato su cloud: OVH/AWS/Azure bloccano SMTP outbound)
    providerKind: 'resend', // 'resend' | 'smtp'
    fromAddress: '',
    fromName: 'Veridanto',
    replyToAddress: '',
    smtpHost: 'smtp.gmail.com',
    smtpPort: 465,
    smtpSecure: true,
    smtpUser: '',
    smtpPassword: '',
    resendApiKey: '',
  });
  const [emailSaving, setEmailSaving] = React.useState(false);
  const [emailShowPassword, setEmailShowPassword] = React.useState(false);
  const [emailTestOpen, setEmailTestOpen] = React.useState(false);
  const [emailTestTo, setEmailTestTo] = React.useState('');
  const [emailTestSending, setEmailTestSending] = React.useState(false);
  const [emailTestResult, setEmailTestResult] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/settings/email', { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) return;
        const j = await r.json();
        if (cancelled) return;
        setEmailServer(j.data || null);
        setEmailPresets(j.presets || []);
        // Pre-populate form dai dati esistenti (esclusa password, mai esposta).
        // Permette update di singoli campi (es. solo password, solo from-address)
        // senza dover ricompilare tutto il form da zero.
        if (j.data) {
          const kind = j.data.provider === 'resend' ? 'resend' : 'smtp';
          setEmailForm((prev) => ({
            ...prev,
            presetId: kind === 'resend' ? 'resend' : (prev.presetId === 'resend' ? 'gmail' : prev.presetId),
            providerKind: kind,
            fromAddress: j.data.fromAddress || prev.fromAddress,
            fromName: j.data.fromName || prev.fromName,
            replyToAddress: j.data.replyToAddress || '',
            smtpHost: j.data.smtpHost || prev.smtpHost,
            smtpPort: j.data.smtpPort ?? prev.smtpPort,
            smtpSecure: j.data.smtpSecure ?? prev.smtpSecure,
            smtpUser: j.data.smtpUser || prev.smtpUser,
            // smtpPassword/resendApiKey restano vuoti: l'utente può lasciarli
            // così per mantenere il valore corrente, oppure compilare per cambiarli.
          }));
        }
      } catch {} finally { if (!cancelled) setEmailLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [user?.id]);

  const applyEmailPreset = (presetId) => {
    const p = emailPresets.find(x => x.id === presetId);
    if (!p) return;
    if (presetId === 'resend') {
      setEmailForm({ ...emailForm, presetId, providerKind: 'resend' });
      return;
    }
    setEmailForm({
      ...emailForm,
      presetId,
      providerKind: 'smtp',
      smtpHost: p.host || emailForm.smtpHost,
      smtpPort: p.port,
      smtpSecure: p.secure,
    });
  };

  const saveEmailConfig = async () => {
    const isResend = emailForm.providerKind === 'resend';
    // Validation diagnostica: elenca i campi mancanti per feedback specifico.
    // Secret (password / apiKey) obbligatorio solo se NON esiste già una config
    // attiva del provider scelto. In update può restare vuoto → backend riusa.
    const missing = [];
    if (!emailForm.fromAddress) missing.push('From address');
    if (isResend) {
      if (!emailServer && !emailForm.resendApiKey) missing.push('Resend API key');
    } else {
      if (!emailForm.smtpHost) missing.push('SMTP host');
      if (!emailForm.smtpUser) missing.push('SMTP user');
      if (!emailServer && !emailForm.smtpPassword) missing.push('SMTP password');
    }
    if (missing.length > 0) {
      pushToast({ title: 'Configurazione incompleta', desc: 'Campi mancanti: ' + missing.join(', '), tone: 'err' });
      return;
    }
    setEmailSaving(true);
    try {
      const body = isResend
        ? {
            provider: 'resend',
            fromAddress: emailForm.fromAddress,
            fromName: emailForm.fromName || null,
            replyToAddress: emailForm.replyToAddress || null,
            resend: { apiKey: emailForm.resendApiKey || '' },
          }
        : {
            provider: 'smtp',
            fromAddress: emailForm.fromAddress,
            fromName: emailForm.fromName || null,
            replyToAddress: emailForm.replyToAddress || null,
            smtp: {
              host: emailForm.smtpHost,
              port: parseInt(emailForm.smtpPort, 10) || 587,
              secure: !!emailForm.smtpSecure,
              user: emailForm.smtpUser,
              password: emailForm.smtpPassword || '',
            },
          };
      const r = await fetch('/api/settings/email', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify(body),
      });
      const j = await r.json();
      if (!r.ok) {
        const detail = j?.detail || j?.issues?.map(i => i.path?.join('.')).filter(Boolean).join(', ') || `HTTP ${r.status}`;
        pushToast({ title: 'Salvataggio email fallito', desc: detail, tone: 'err' });
        return;
      }
      setEmailServer(j.data);
      // NON svuoto smtpPassword dopo save: l'utente vuole conferma visiva di
      // cosa ha effettivamente salvato. Il toggle eye resta nello stato che
      // l'utente ha scelto. La password sparisce solo al reload pagina (security).
      pushToast({ title: 'Provider email salvato ✓', desc: `Config attiva: ${j.data.smtpHint}`, tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST fallito', tone: 'err' });
    } finally { setEmailSaving(false); }
  };

  // Sessione 83 — Onboarding wizard state
  const [tenantsList, setTenantsList] = React.useState([]);
  const [onbForm, setOnbForm] = React.useState({
    tenantId: '',
    tenantName: '',
    tenantSlug: '',
    tenantDescription: '',
    adminName: '',
    adminEmail: '',
    adminPassword: '',
    seedCapex: true,
    seedDocTypes: true,
    seedCalendars: true,
  });
  const [onbSaving, setOnbSaving] = React.useState(false);
  const [onbResult, setOnbResult] = React.useState(null);

  React.useEffect(() => {
    if (!isSuperadmin) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/admin/tenants', { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) return;
        const j = await r.json();
        if (!cancelled) setTenantsList(j.data || []);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [isSuperadmin, user?.id]);

  const createTenant = async () => {
    if (!onbForm.tenantId || !onbForm.tenantName || !onbForm.adminEmail || !onbForm.adminPassword) {
      pushToast({ title: 'Campi obbligatori', desc: 'Compila tenant id, name, admin email, password.', tone: 'err' });
      return;
    }
    setOnbSaving(true);
    setOnbResult(null);
    try {
      const r = await fetch('/api/admin/tenants', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          tenant: {
            id: onbForm.tenantId,
            name: onbForm.tenantName,
            slug: onbForm.tenantSlug || onbForm.tenantId,
            description: onbForm.tenantDescription || null,
          },
          admin: {
            name: onbForm.adminName || onbForm.adminEmail.split('@')[0],
            email: onbForm.adminEmail,
            password: onbForm.adminPassword,
            roleIds: ['SUPERADMIN'],
          },
          seedDefaults: {
            capexClasses: onbForm.seedCapex,
            docTypes: onbForm.seedDocTypes,
            calendars: onbForm.seedCalendars,
          },
        }),
      });
      const j = await r.json();
      if (!r.ok) {
        setOnbResult({ ok: false, error: j.error, detail: j.detail });
        pushToast({ title: `Onboarding fallito (${j.error})`, desc: j.detail?.slice(0, 120) || '', tone: 'err' });
        return;
      }
      setOnbResult({ ok: true, ...j.data });
      // Reset form
      setOnbForm({ ...onbForm, tenantId: '', tenantName: '', tenantSlug: '', tenantDescription: '', adminName: '', adminEmail: '', adminPassword: '' });
      // Refresh list
      const list = await fetch('/api/admin/tenants', { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
      const listJ = await list.json();
      setTenantsList(listJ.data || []);
      pushToast({ title: 'Tenant creato', desc: `${j.data.tenant.id} + admin ${j.data.admin.email}`, tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST fallito', tone: 'err' });
    } finally { setOnbSaving(false); }
  };

  const sendTestEmail = async () => {
    if (!emailTestTo) return;
    setEmailTestSending(true);
    setEmailTestResult(null);
    try {
      const r = await fetch('/api/settings/email/test-send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ to: emailTestTo, subject: 'Veridanto · Test email' }),
      });
      const j = await r.json();
      if (!r.ok) {
        setEmailTestResult({ ok: false, error: j?.error, detail: j?.detail });
        pushToast({ title: `Test email fallito (${j?.error})`, desc: j?.detail?.slice(0, 120) || '', tone: 'err' });
        return;
      }
      setEmailTestResult({ ok: true, ...j.data });
      pushToast({ title: 'Email di test inviata', desc: `${j.data.accepted.length} accepted · ${j.data.latencyMs}ms`, tone: 'ok' });
    } catch (err) {
      setEmailTestResult({ ok: false, error: 'network', detail: err?.message });
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fetch fallito', tone: 'err' });
    } finally { setEmailTestSending(false); }
  };

  // FASE 2b.AI — fonte di verità su provider/mode/model/temperature/key è il DB.
  // `aiServer` è l'ultimo DTO ricevuto dal server; `setAi` invia PATCH e ricarica.
  const [aiServer, setAiServer] = React.useState(null);
  const [aiServerLoading, setAiServerLoading] = React.useState(true);
  const [aiServerError, setAiServerError] = React.useState(null);
  const [claudeKeyDraft, setClaudeKeyDraft] = React.useState('');
  const [geminiKeyDraft, setGeminiKeyDraft] = React.useState('');
  const [deepseekKeyDraft, setDeepseekKeyDraft] = React.useState('');
  const [savingKey, setSavingKey] = React.useState(null); // 'claude' | 'gemini' | 'deepseek' | null
  const [pingState, setPingState] = React.useState({ provider: null, status: 'idle', text: '', latencyMs: 0, error: null });

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const dto = await window.fetchAiSettings();
        if (cancelled) return;
        setAiServer(dto);
        setAiServerError(null);
        // sync dello shadow localStorage (no chiavi)
        setAiSettings({
          provider: dto.primaryProvider,
          mode: dto.mode,
          claudeModel: dto.claudeModel,
          geminiModel: dto.geminiModel,
          deepseekModel: dto.deepseekModel,
          temperature: dto.temperature,
        });
      } catch (err) {
        if (!cancelled) setAiServerError(err?.message || 'Errore caricamento');
      } finally {
        if (!cancelled) setAiServerLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  /**
   * Patch parziale verso /api/settings/ai. Aggiorna sia lo stato server che
   * lo shadow `aiSettings` localStorage. In caso di errore mostra un toast.
   */
  const setAi = async (patch) => {
    try {
      const dto = await window.patchAiSettings(patch, user?.id);
      setAiServer(dto);
      setAiSettings({
        provider: dto.primaryProvider,
        mode: dto.mode,
        claudeModel: dto.claudeModel,
        geminiModel: dto.geminiModel,
        deepseekModel: dto.deepseekModel,
        temperature: dto.temperature,
      });
    } catch (err) {
      pushToast({ title: 'Errore salvataggio AI', desc: err?.message || 'PATCH fallita', tone: 'err' });
    }
  };

  const KEY_FIELD = { claude: 'claudeKey', gemini: 'geminiKey', deepseek: 'deepseekKey' };

  const saveKey = async (provider) => {
    const draft =
      provider === 'claude' ? claudeKeyDraft :
      provider === 'gemini' ? geminiKeyDraft :
      deepseekKeyDraft;
    if (!draft.trim()) return;
    setSavingKey(provider);
    try {
      const dto = await window.patchAiSettings(
        { [KEY_FIELD[provider]]: draft.trim() },
        user?.id,
      );
      setAiServer(dto);
      if (provider === 'claude') setClaudeKeyDraft('');
      else if (provider === 'gemini') setGeminiKeyDraft('');
      else setDeepseekKeyDraft('');
      pushToast({ title: `Chiave ${provider} salvata`, desc: 'Cifrata e archiviata in DB.', tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore salvataggio chiave', desc: err?.message || 'PATCH fallita', tone: 'err' });
    } finally {
      setSavingKey(null);
    }
  };

  const removeKey = async (provider) => {
    if (!window.confirm(`Rimuovere la chiave ${provider} dal database?`)) return;
    setSavingKey(provider);
    try {
      const dto = await window.patchAiSettings(
        { [KEY_FIELD[provider]]: null },
        user?.id,
      );
      setAiServer(dto);
      pushToast({ title: `Chiave ${provider} rimossa`, tone: 'warn' });
    } catch (err) {
      pushToast({ title: 'Errore rimozione', desc: err?.message || 'PATCH fallita', tone: 'err' });
    } finally {
      setSavingKey(null);
    }
  };

  const runPing = async (provider) => {
    setPingState({ provider, status: 'pending', text: '', latencyMs: 0, error: null });
    try {
      const data = await window.pingAi(provider, undefined, user?.id);
      setPingState({ provider, status: 'ok', text: data.text, latencyMs: data.latencyMs, error: null });
      pushToast({ title: `${provider} OK`, desc: `${data.model} · ${data.latencyMs}ms`, tone: 'ok' });
    } catch (err) {
      setPingState({ provider, status: 'err', text: '', latencyMs: 0, error: err?.message || 'Errore' });
      pushToast({ title: `${provider} fallito`, desc: err?.message?.slice(0, 200) || 'Errore', tone: 'err' });
    }
  };

  // stato locale connectors (override del catalogo)
  const [connectors, setConnectors] = React.useState(() => {
    const out = {};
    CONNECTOR_CATALOG.forEach(g => g.items.forEach(c => { out[c.id] = c.status; }));
    return out;
  });
  const toggleConnector = (c) => {
    const next = connectors[c.id] === 'connected' ? 'available' : 'connected';
    setConnectors({ ...connectors, [c.id]: next });
    pushToast({
      title: next === 'connected' ? `${c.name} connesso` : `${c.name} disconnesso`,
      desc: next === 'connected' ? 'Integrazione attiva. Prima sync pianificata.' : 'Integrazione disattivata. Dati locali mantenuti.',
      tone: next === 'connected' ? 'ok' : 'warn',
    });
  };

  const connectedCount = Object.values(connectors).filter(s => s === 'connected').length;
  const totalCount = Object.keys(connectors).length;

  const currentClaude = aiServer?.claudeModel || aiSettings.claudeModel || 'claude-sonnet-4-5';
  const currentGemini = aiServer?.geminiModel || aiSettings.geminiModel || 'gemini-2-5-pro';
  const currentDeepseek = aiServer?.deepseekModel || aiSettings.deepseekModel || 'deepseek-chat';
  const currentMode = aiServer?.mode || aiSettings.mode || 'hybrid';
  const currentProvider = aiServer?.primaryProvider || aiSettings.provider || 'claude';
  const currentTemp = aiServer?.temperature ?? aiSettings.temperature ?? 0.3;
  const hasClaudeKey = aiServer?.hasClaudeKey ?? false;
  const hasGeminiKey = aiServer?.hasGeminiKey ?? false;
  const hasDeepseekKey = aiServer?.hasDeepseekKey ?? false;

  const isSuperadmin = Array.isArray(user?.roleIds) && user.roleIds.includes('SUPERADMIN');
  const tabs = [
    { id: 'ai',        label: 'Provider AI',     icon: 'sparkle' },
    { id: 'email',     label: 'Provider Email',  icon: 'mail' },
    { id: 'gmail-inbound', label: 'Gmail inbound', icon: 'mail' },
    ...(isSuperadmin ? [{ id: 'onboarding', label: 'Onboarding tenant', icon: 'plus' }] : []),
    { id: 'connect',   label: 'Connettori',      icon: 'link', badge: `${connectedCount}/${totalCount}` },
    { id: 'ui',        label: 'Interfaccia',     icon: 'palette' },
    { id: 'gov',       label: 'Governance',      icon: 'shield' },
    { id: 'notif',     label: 'Notifiche',       icon: 'bell' },
    { id: 'sessions',  label: 'Sessioni attive', icon: 'shield' },
    { id: 'delegations', label: 'Deleghe uscenti', icon: 'users' },
    { id: 'org',       label: 'Organizzazione',  icon: 'users' },
  ];

  return (
    <div className="page fade-in">
      <div className="page-header">
        <div>
          <div className="eyebrow">Amministrazione</div>
          <h1 className="page-title">Impostazioni</h1>
          <div className="page-sub">Provider AI, integrazioni di sistema, sicurezza, notifiche e retention. Modalità live richiede una API key configurata; hybrid abilita auto-retry tra provider.</div>
        </div>
        <div className="actions">
          <Chip kind={currentMode==='live'?'ai':'info'} dot>
            {currentMode==='live'?'AI · Solo primario':'AI · Auto-retry'}
          </Chip>
          {aiServerError && <Chip kind="err" dot>BE non raggiungibile</Chip>}
          <Chip kind={connectedCount > 0 ? 'ok' : 'info'} dot>{connectedCount} connettori attivi</Chip>
        </div>
      </div>

      {/* Tab nav */}
      <div className="row" style={{ gap: 2, borderBottom: '1px solid var(--line)', marginBottom: 14, overflowX: 'auto' }}>
        {tabs.map(t => (
          <button
            key={t.id}
            className="btn ghost sm"
            onClick={() => setTab(t.id)}
            style={{
              padding: '9px 14px',
              borderRadius: 0,
              borderBottom: '2px solid ' + (tab === t.id ? 'var(--accent)' : 'transparent'),
              color: tab === t.id ? 'var(--text-1)' : 'var(--text-2)',
              fontWeight: tab === t.id ? 600 : 400,
              whiteSpace: 'nowrap',
            }}
          >
            <Icon name={t.icon} size={12}/> {t.label}
            {t.badge && <span style={{ marginLeft: 6, fontSize: 10, color: 'var(--text-3)' }} className="mono">{t.badge}</span>}
          </button>
        ))}
      </div>

      {/* ==================== TAB: AI ==================== */}
      {tab === 'ai' && (
        <div className="grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(380px, 1fr))', gap: 14 }}>
          {/* Claude */}
          <ProviderKeyCard
            color="#D97757"
            name="Anthropic Claude"
            placeholder="sk-ant-api03-..."
            endpoint="api.anthropic.com/v1 · residency: US / EU mirror"
            hasKey={hasClaudeKey}
            keyHint={aiServer?.claudeKeyHint}
            draft={claudeKeyDraft}
            setDraft={setClaudeKeyDraft}
            saving={savingKey === 'claude'}
            onSave={() => saveKey('claude')}
            onRemove={() => removeKey('claude')}
            onPing={() => runPing('claude')}
            pingState={pingState.provider === 'claude' ? pingState : null}
            modelLabel="Modello di default · Claude"
            models={MODEL_CATALOG.claude}
            currentModel={currentClaude}
            onSelectModel={(id) => setAi({ claudeModel: id })}
            disabled={aiServerLoading}
          />

          {/* Gemini */}
          <ProviderKeyCard
            color="#4285F4"
            name="Google Gemini"
            placeholder="AIzaSy..."
            endpoint="generativelanguage.googleapis.com/v1beta · residency: EU-Belgium"
            hasKey={hasGeminiKey}
            keyHint={aiServer?.geminiKeyHint}
            draft={geminiKeyDraft}
            setDraft={setGeminiKeyDraft}
            saving={savingKey === 'gemini'}
            onSave={() => saveKey('gemini')}
            onRemove={() => removeKey('gemini')}
            onPing={() => runPing('gemini')}
            pingState={pingState.provider === 'gemini' ? pingState : null}
            modelLabel="Modello di default · Gemini"
            models={MODEL_CATALOG.gemini}
            currentModel={currentGemini}
            onSelectModel={(id) => setAi({ geminiModel: id })}
            disabled={aiServerLoading}
          />

          {/* DeepSeek */}
          <ProviderKeyCard
            color="#5e6ad2"
            name="DeepSeek"
            placeholder="sk-..."
            endpoint="api.deepseek.com/v1 · OpenAI-compatible · residency: PRC"
            hasKey={hasDeepseekKey}
            keyHint={aiServer?.deepseekKeyHint}
            draft={deepseekKeyDraft}
            setDraft={setDeepseekKeyDraft}
            saving={savingKey === 'deepseek'}
            onSave={() => saveKey('deepseek')}
            onRemove={() => removeKey('deepseek')}
            onPing={() => runPing('deepseek')}
            pingState={pingState.provider === 'deepseek' ? pingState : null}
            modelLabel="Modello di default · DeepSeek"
            models={MODEL_CATALOG.deepseek}
            currentModel={currentDeepseek}
            onSelectModel={(id) => setAi({ deepseekModel: id })}
            disabled={aiServerLoading}
          />

          {/* Runtime & routing */}
          <div className="card" style={{ gridColumn: '1 / -1' }}>
            <div className="card-header"><div className="title">Runtime & routing</div><div className="desc">Come l'orchestratore sceglie il provider ad ogni chiamata</div></div>
            <div className="card-body">
              <div className="grid grid-3" style={{ gap: 14 }}>
                <div className="field">
                  <label>Modalità di esecuzione</label>
                  <div className="row" style={{ gap: 4, flexWrap: 'wrap' }}>
                    <button
                      className={`btn sm ${currentMode==='hybrid'?'primary':'ghost'}`}
                      onClick={async () => {
                        if (!hasClaudeKey && !hasGeminiKey && !hasDeepseekKey) {
                          pushToast({ title: 'API key richiesta', desc: 'Inserisci almeno una chiave AI prima di salvare la modalità.', tone: 'warn' });
                          return;
                        }
                        await setAi({ mode: 'hybrid' });
                      }}
                    >Auto-retry tra provider</button>
                    <button
                      className={`btn sm ${currentMode==='live'?'primary':'ghost'}`}
                      onClick={async () => {
                        if (!hasClaudeKey && !hasGeminiKey && !hasDeepseekKey) {
                          pushToast({ title: 'API key richiesta', desc: 'Inserisci almeno una chiave AI prima di salvare la modalità.', tone: 'warn' });
                          return;
                        }
                        await setAi({ mode: 'live' });
                      }}
                    >Solo provider primario</button>
                  </div>
                  <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>
                    Auto-retry: se il provider primario fallisce, prova gli altri configurati. Solo primario: errore esplicito se il primario fallisce.
                  </div>
                </div>

                <div className="field">
                  <label>Provider predefinito</label>
                  <div className="row" style={{ gap: 4, flexWrap: 'wrap' }}>
                    <button className={`btn sm ${currentProvider==='claude'?'primary':'ghost'}`} onClick={() => setAi({ primaryProvider: 'claude' })}><span style={{ width: 8, height: 8, background: '#D97757', borderRadius: '50%', display: 'inline-block', marginRight: 4 }}/>Claude</button>
                    <button className={`btn sm ${currentProvider==='gemini'?'primary':'ghost'}`} onClick={() => setAi({ primaryProvider: 'gemini' })}><span style={{ width: 8, height: 8, background: '#4285F4', borderRadius: '50%', display: 'inline-block', marginRight: 4 }}/>Gemini</button>
                    <button className={`btn sm ${currentProvider==='deepseek'?'primary':'ghost'}`} onClick={() => setAi({ primaryProvider: 'deepseek' })}><span style={{ width: 8, height: 8, background: '#5e6ad2', borderRadius: '50%', display: 'inline-block', marginRight: 4 }}/>DeepSeek</button>
                    <button className={`btn sm ${currentProvider==='auto'?'primary':'ghost'}`} onClick={() => setAi({ primaryProvider: 'auto' })}>Auto</button>
                  </div>
                  <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>Auto: il router sceglie in base a costo/latenza/tipo di task.</div>
                </div>

                <div className="field">
                  <label>Temperature · default</label>
                  <input type="range" min="0" max="1" step="0.1" value={currentTemp} onChange={(e) => setAi({ temperature: Number(e.target.value) })} style={{ width: '100%' }}/>
                  <div className="row" style={{ justifyContent: 'space-between', fontSize: 10.5, color: 'var(--text-3)' }}>
                    <span>0 · deterministico</span><span className="mono">{currentTemp.toFixed(1)}</span><span>1 · creativo</span>
                  </div>
                </div>
              </div>

              <div style={{ marginTop: 14 }}>
                <div className="eyebrow" style={{ marginBottom: 6 }}>Routing per tipo di task</div>
                <table className="tbl dense">
                  <thead><tr><th>Task</th><th>Modello</th><th>Fallback</th><th style={{width:80}}>Costo/msg</th><th style={{width:80}}>Latenza</th></tr></thead>
                  <tbody>
                    {[
                      { t: 'Estrazione campi da PDF',     m: 'Claude Haiku 4.5',    f: 'Gemini 2.5 Flash-Lite', c: '€0.002', l: '~800ms' },
                      { t: 'Redazione capitolati/RdA',    m: 'Claude Sonnet 4.5',   f: 'Gemini 2.5 Pro',        c: '€0.08',  l: '~6s' },
                      { t: 'Analisi rischi e anomalie',   m: 'Claude Opus 4.1',     f: 'Gemini 2.5 Pro',        c: '€0.25',  l: '~12s' },
                      { t: 'Ricerca semantica docs',      m: 'Gemini 2.5 Flash',    f: 'Claude Haiku 4.5',      c: '€0.003', l: '~400ms' },
                      { t: 'Riassunto verbali riunione',  m: 'Claude Sonnet 4.5',   f: 'Gemini 2.5 Flash',      c: '€0.04',  l: '~3s' },
                      { t: 'Classificazione documenti',   m: 'Claude Haiku 3.5',    f: 'Gemini 2.0 Flash-Lite', c: '€0.001', l: '~300ms' },
                    ].map((r, i) => (
                      <tr key={i}>
                        <td>{r.t}</td>
                        <td><Chip kind="ai">{r.m}</Chip></td>
                        <td style={{ color: 'var(--text-2)' }}>{r.f}</td>
                        <td className="mono">{r.c}</td>
                        <td className="mono" style={{ color: 'var(--text-2)' }}>{r.l}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>

              <div style={{ marginTop: 14 }} className="grid grid-3">
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox" defaultChecked/> Cache delle risposte (24h)
                  </label>
                </div>
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox" defaultChecked/> Retry automatico su fallback
                  </label>
                </div>
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox" defaultChecked/> PII masking pre-prompt
                  </label>
                </div>
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox" defaultChecked/> Log prompt/risposta (retention 24m)
                  </label>
                </div>
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox"/> Streaming UI per risposte lunghe
                  </label>
                </div>
                <div className="field">
                  <label className="row" style={{ gap: 6 }}>
                    <input type="checkbox" defaultChecked/> Rate limit: 60 req/min · 200K tok/ora
                  </label>
                </div>
              </div>
            </div>
          </div>

          {/* Budget & usage — dati reali da audit_log /api/ai/usage */}
          <AiUsageCard/>

          {/* Sessione 65 — AI override personale (per persona, in localStorage) */}
          <AiPersonalOverrideCard userId={user?.id} userName={user?.name}/>
        </div>
      )}

      {/* ==================== TAB: Email Provider (Sessione 83) ==================== */}
      {tab === 'email' && (
        <div className="grid" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header">
              <div className="title"><Icon name="mail" size={12}/> Provider Email Transactional</div>
              <div className="actions">
                {emailServer && <Chip kind="ok" dot data-testid="email-config-active">Attivo · {emailServer.provider}</Chip>}
                {!emailServer && !emailLoading && <Chip kind="warn" dot data-testid="email-config-missing">Non configurato</Chip>}
                <Btn variant="ghost" size="sm" onClick={() => setEmailTestOpen(true)} disabled={!emailServer || emailLoading} data-testid="email-test-btn">
                  <Icon name="send" size={11}/> Invia test
                </Btn>
              </div>
            </div>
            <div className="card-body">
              <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginBottom: 10 }}>
                Configura un provider SMTP per invio email transazionali (notifiche, reset password, dispatch SLA).
                Per <b>Gmail/Google Workspace</b> serve una <a href="https://support.google.com/accounts/answer/185833" target="_blank" rel="noopener" style={{color:'var(--accent)'}}>App Password</a> (richiede 2FA attivo).
                Per <b>Microsoft 365</b> SMTP AUTH va abilitato sul mailbox.
              </div>

              {emailServer && (
                <div style={{ padding: 10, background: 'var(--bg-2)', border: '1px solid var(--line)', borderRadius: 8, marginBottom: 12, fontSize: 11.5 }} data-testid="email-config-current">
                  <div className="row" style={{ justifyContent: 'space-between' }}>
                    <span><b>From:</b> {emailServer.fromName ? `"${emailServer.fromName}" <${emailServer.fromAddress}>` : emailServer.fromAddress}</span>
                    <span className="mono" style={{ color: 'var(--text-3)' }}>{emailServer.smtpHint}</span>
                  </div>
                  {emailServer.replyToAddress && <div style={{ color: 'var(--text-3)', marginTop: 4 }}>Reply-to: {emailServer.replyToAddress}</div>}
                </div>
              )}

              <div className="row" style={{ gap: 10, marginBottom: 10 }}>
                <div className="field" style={{ flex: 1 }}>
                  <label>Preset</label>
                  <select value={emailForm.presetId} onChange={(e) => applyEmailPreset(e.target.value)} data-testid="email-preset-select">
                    {emailPresets.map((p) => (<option key={p.id} value={p.id}>{p.label}</option>))}
                  </select>
                </div>
              </div>
              {emailForm.presetId && emailPresets.find(p => p.id === emailForm.presetId)?.hint && (
                <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 10, padding: 8, background: 'var(--bg)', borderRadius: 4, borderLeft: '2px solid var(--accent)' }}>
                  💡 {emailPresets.find(p => p.id === emailForm.presetId)?.hint}
                </div>
              )}

              <div className="grid grid-2" style={{ gap: 10 }}>
                <div className="field">
                  <label>From address *</label>
                  <input value={emailForm.fromAddress} onChange={(e) => setEmailForm({...emailForm, fromAddress: e.target.value})} placeholder={emailForm.providerKind === 'resend' ? 'onboarding@resend.dev' : 'no-reply@veridanto.com'} data-testid="email-from-input"/>
                </div>
                <div className="field">
                  <label>From name (display)</label>
                  <input value={emailForm.fromName} onChange={(e) => setEmailForm({...emailForm, fromName: e.target.value})} placeholder="Veridanto Notifiche"/>
                </div>
                <div className="field">
                  <label>Reply-to (opzionale)</label>
                  <input value={emailForm.replyToAddress} onChange={(e) => setEmailForm({...emailForm, replyToAddress: e.target.value})} placeholder="support@veridanto.com"/>
                </div>
                {emailForm.providerKind === 'resend' && (
                  <div className="field">
                    <label>Resend API key {emailServer && emailServer.provider === 'resend' ? '' : '*'}</label>
                    <div style={{ position: 'relative' }}>
                      <input
                        type={emailShowPassword ? 'text' : 'password'}
                        value={emailForm.resendApiKey}
                        onChange={(e) => setEmailForm({...emailForm, resendApiKey: e.target.value})}
                        placeholder={emailServer && emailServer.provider === 'resend' ? 'Lascia vuoto per mantenere quella attuale' : 're_xxxxxxxxxxxxxxxxxxxxxxxx'}
                        data-testid="email-resend-apikey-input"
                        style={{ paddingRight: 36, width: '100%' }}
                      />
                      <button
                        type="button"
                        onClick={() => setEmailShowPassword(v => !v)}
                        aria-label={emailShowPassword ? 'Nascondi API key' : 'Mostra API key'}
                        title={emailShowPassword ? 'Nascondi API key' : 'Mostra API key'}
                        data-testid="email-resend-apikey-toggle"
                        style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)', background: 'transparent', border: 0, padding: 6, cursor: 'pointer', color: 'var(--text-3)', display: 'flex', alignItems: 'center' }}
                      >
                        <Icon name={emailShowPassword ? 'eye-off' : 'eye'} size={14}/>
                      </button>
                    </div>
                  </div>
                )}
                {emailForm.providerKind === 'smtp' && (
                <>
                <div className="field">
                  <label>SMTP host *</label>
                  <input value={emailForm.smtpHost} onChange={(e) => setEmailForm({...emailForm, smtpHost: e.target.value})} placeholder="smtp.gmail.com"/>
                </div>
                <div className="field">
                  <label>SMTP port *</label>
                  <input type="number" value={emailForm.smtpPort} onChange={(e) => setEmailForm({...emailForm, smtpPort: e.target.value})} placeholder="465"/>
                </div>
                <div className="field">
                  <label>SMTP secure (SSL implicit)</label>
                  <select value={emailForm.smtpSecure ? 'true' : 'false'} onChange={(e) => setEmailForm({...emailForm, smtpSecure: e.target.value === 'true'})}>
                    <option value="true">SSL/TLS implicit (porta 465)</option>
                    <option value="false">STARTTLS / cleartext (porta 587/25)</option>
                  </select>
                </div>
                <div className="field">
                  <label>SMTP user *</label>
                  <input value={emailForm.smtpUser} onChange={(e) => setEmailForm({...emailForm, smtpUser: e.target.value})} placeholder="user@gmail.com" data-testid="email-user-input"/>
                </div>
                <div className="field">
                  <label>SMTP password / App password {emailServer ? '' : '*'}</label>
                  <div style={{ position: 'relative' }}>
                    <input
                      type={emailShowPassword ? 'text' : 'password'}
                      value={emailForm.smtpPassword}
                      onChange={(e) => setEmailForm({...emailForm, smtpPassword: e.target.value})}
                      placeholder={emailServer ? 'Lascia vuoto per mantenere quella attuale' : 'xxxx xxxx xxxx xxxx'}
                      data-testid="email-password-input"
                      style={{ paddingRight: 36, width: '100%' }}
                    />
                    <button
                      type="button"
                      onClick={async () => {
                        // Se sto per MOSTRARE (toggle off→on) e il campo è vuoto
                        // ma esiste una config attiva → fetch la password decifrata
                        // dal server e popola il campo. Audit log lato BE.
                        if (!emailShowPassword && !emailForm.smtpPassword && emailServer) {
                          try {
                            const r = await fetch('/api/settings/email/reveal-password', {
                              method: 'POST',
                              headers: { 'X-Actor-Persona-Id': user?.id || '' },
                            });
                            if (r.ok) {
                              const j = await r.json();
                              if (j?.data?.password) {
                                setEmailForm(f => ({ ...f, smtpPassword: j.data.password }));
                              }
                            } else {
                              const j = await r.json().catch(() => ({}));
                              pushToast({ title: 'Reveal fallito', desc: j?.error || `HTTP ${r.status}`, tone: 'err' });
                              return;
                            }
                          } catch (err) {
                            pushToast({ title: 'Errore di rete', desc: err?.message || 'reveal fallito', tone: 'err' });
                            return;
                          }
                        }
                        setEmailShowPassword(v => !v);
                      }}
                      aria-label={emailShowPassword ? 'Nascondi password' : 'Mostra password'}
                      title={emailShowPassword ? 'Nascondi password' : 'Mostra password'}
                      data-testid="email-password-toggle"
                      style={{
                        position: 'absolute',
                        right: 4,
                        top: '50%',
                        transform: 'translateY(-50%)',
                        background: 'transparent',
                        border: 0,
                        padding: 6,
                        cursor: 'pointer',
                        color: 'var(--text-3)',
                        display: 'flex',
                        alignItems: 'center',
                      }}
                    >
                      <Icon name={emailShowPassword ? 'eye-off' : 'eye'} size={14}/>
                    </button>
                  </div>
                </div>
                </>
                )}
              </div>

              <div className="row" style={{ marginTop: 14, gap: 8, justifyContent: 'flex-end' }}>
                <Btn variant="primary" size="sm" onClick={saveEmailConfig} disabled={emailSaving} data-testid="email-save-btn">
                  <Icon name="check" size={11}/> {emailSaving ? 'Salvataggio…' : (emailServer ? 'Aggiorna provider' : 'Salva configurazione')}
                </Btn>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* Email test send modal */}
      {emailTestOpen && (
        <Modal open={emailTestOpen} onClose={() => { setEmailTestOpen(false); setEmailTestResult(null); }} title="Invia email di test" size="sm" footer={
          <>
            <Btn variant="ghost" size="sm" onClick={() => { setEmailTestOpen(false); setEmailTestResult(null); }}>Chiudi</Btn>
            <Btn variant="primary" size="sm" onClick={sendTestEmail} disabled={emailTestSending || !emailTestTo} data-testid="email-test-submit">
              <Icon name="send" size={11}/> {emailTestSending ? 'Invio…' : 'Invia'}
            </Btn>
          </>
        }>
          <div className="col" style={{ gap: 10 }}>
            <div className="field">
              <label>Destinatario email *</label>
              <input value={emailTestTo} onChange={(e) => setEmailTestTo(e.target.value)} placeholder="recipient@example.com" data-testid="email-test-to-input"/>
            </div>
            <div style={{ fontSize: 11, color: 'var(--text-3)' }}>
              Verrà inviata una mail di test usando la config SMTP attiva del tenant. Audit metadata-only registrato.
            </div>
            {emailTestResult && (
              <div style={{ padding: 10, borderRadius: 6, background: emailTestResult.ok ? 'rgba(40,160,80,0.1)' : 'rgba(220,80,80,0.1)', border: `1px solid ${emailTestResult.ok ? 'var(--ok)' : 'var(--err)'}`, fontSize: 11.5 }} data-testid="email-test-result">
                {emailTestResult.ok ? (
                  <>
                    <div style={{ fontWeight: 600, color: 'var(--ok)' }}>✓ Inviata</div>
                    <div className="mono" style={{ fontSize: 10.5, marginTop: 4 }}>{emailTestResult.messageId}</div>
                    <div style={{ fontSize: 10.5, marginTop: 4, color: 'var(--text-3)' }}>Accepted: {emailTestResult.accepted?.length || 0} · Rejected: {emailTestResult.rejected?.length || 0} · Latency: {emailTestResult.latencyMs}ms</div>
                  </>
                ) : (
                  <>
                    <div style={{ fontWeight: 600, color: 'var(--err)' }}>✗ {emailTestResult.error}</div>
                    <div className="mono" style={{ fontSize: 10.5, marginTop: 4, wordBreak: 'break-all' }}>{emailTestResult.detail}</div>
                  </>
                )}
              </div>
            )}
          </div>
        </Modal>
      )}

      {/* ==================== TAB: Gmail Inbound OAuth (Sessione 88 / FASE 15.B) ==================== */}
      {tab === 'gmail-inbound' && (
        <GmailInboundCard />
      )}

      {/* ==================== TAB: Onboarding Tenant (SUPERADMIN, Sessione 83) ==================== */}
      {tab === 'onboarding' && isSuperadmin && (
        <div className="grid" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header">
              <div className="title"><Icon name="plus" size={12}/> Onboarding nuovo tenant</div>
              <div className="actions">
                <Chip kind="warn" dot>SUPERADMIN only</Chip>
              </div>
            </div>
            <div className="card-body">
              <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginBottom: 12 }}>
                Crea un nuovo tenant (cliente isolato) con admin persona dedicata e seed default opzionali.
                Operazione atomica in transaction: in caso di errore tutto rollback.
              </div>

              <div className="eyebrow" style={{ marginBottom: 8 }}>Tenant</div>
              <div className="grid grid-2" style={{ gap: 10 }}>
                <div className="field">
                  <label>Tenant ID * (lowercase, kebab-case)</label>
                  <input value={onbForm.tenantId} onChange={(e) => setOnbForm({...onbForm, tenantId: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-')})} placeholder="acme-mfg" data-testid="onb-tenant-id"/>
                </div>
                <div className="field">
                  <label>Tenant name *</label>
                  <input value={onbForm.tenantName} onChange={(e) => setOnbForm({...onbForm, tenantName: e.target.value})} placeholder="ACME Manufacturing SpA" data-testid="onb-tenant-name"/>
                </div>
                <div className="field">
                  <label>Slug (default = tenant ID)</label>
                  <input value={onbForm.tenantSlug} onChange={(e) => setOnbForm({...onbForm, tenantSlug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-')})} placeholder={onbForm.tenantId || 'acme-mfg'}/>
                </div>
                <div className="field">
                  <label>Descrizione (opzionale)</label>
                  <input value={onbForm.tenantDescription} onChange={(e) => setOnbForm({...onbForm, tenantDescription: e.target.value})} placeholder="Cliente CAPEX 2026"/>
                </div>
              </div>

              <div className="eyebrow" style={{ marginTop: 16, marginBottom: 8 }}>Admin persona iniziale</div>
              <div className="grid grid-2" style={{ gap: 10 }}>
                <div className="field">
                  <label>Nome admin *</label>
                  <input value={onbForm.adminName} onChange={(e) => setOnbForm({...onbForm, adminName: e.target.value})} placeholder="Mario Rossi" data-testid="onb-admin-name"/>
                </div>
                <div className="field">
                  <label>Email admin *</label>
                  <input type="email" value={onbForm.adminEmail} onChange={(e) => setOnbForm({...onbForm, adminEmail: e.target.value})} placeholder="admin@acme-mfg.com" data-testid="onb-admin-email"/>
                </div>
                <div className="field">
                  <label>Password admin * (≥ 8 char)</label>
                  <input type="password" value={onbForm.adminPassword} onChange={(e) => setOnbForm({...onbForm, adminPassword: e.target.value})} placeholder="••••••••" data-testid="onb-admin-password"/>
                </div>
                <div className="field">
                  <label>Ruolo</label>
                  <input value="SUPERADMIN" readOnly style={{ opacity: 0.7 }}/>
                </div>
              </div>

              <div className="eyebrow" style={{ marginTop: 16, marginBottom: 8 }}>Seed default opzionali</div>
              <div className="row" style={{ gap: 18 }}>
                <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
                  <input type="checkbox" checked={onbForm.seedCapex} onChange={(e) => setOnbForm({...onbForm, seedCapex: e.target.checked})}/>
                  4 capex_classes (growth/maintenance/compliance/it_ot)
                </label>
                <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
                  <input type="checkbox" checked={onbForm.seedDocTypes} onChange={(e) => setOnbForm({...onbForm, seedDocTypes: e.target.checked})}/>
                  6 doc_types core (BUS_CASE/WBS/RISK_REG/SPEC_TECH/OFFERTA/CONTRACT)
                </label>
                <label className="row" style={{ gap: 6, fontSize: 11.5, cursor: 'pointer' }}>
                  <input type="checkbox" checked={onbForm.seedCalendars} onChange={(e) => setOnbForm({...onbForm, seedCalendars: e.target.checked})}/>
                  Calendario IT standard
                </label>
              </div>

              <div className="row" style={{ marginTop: 16, justifyContent: 'flex-end', gap: 8 }}>
                <Btn variant="primary" size="sm" onClick={createTenant} disabled={onbSaving} data-testid="onb-submit">
                  <Icon name="check" size={11}/> {onbSaving ? 'Onboarding…' : 'Crea tenant + admin'}
                </Btn>
              </div>

              {onbResult && (
                <div style={{ marginTop: 14, padding: 12, borderRadius: 8, background: onbResult.ok ? 'rgba(40,160,80,0.1)' : 'rgba(220,80,80,0.1)', border: `1px solid ${onbResult.ok ? 'var(--ok)' : 'var(--err)'}`, fontSize: 11.5 }} data-testid="onb-result">
                  {onbResult.ok ? (
                    <>
                      <div style={{ fontWeight: 600, color: 'var(--ok)' }}>✓ Tenant creato</div>
                      <div className="mono" style={{ fontSize: 10.5, marginTop: 6 }}>
                        Tenant: {onbResult.tenant.id} ({onbResult.tenant.name})<br/>
                        Admin: {onbResult.admin.email} (persona id: {onbResult.admin.id})<br/>
                        Seed: {onbResult.seedStats.capexClassesCreated} capex · {onbResult.seedStats.docTypesCreated} doc_types · {onbResult.seedStats.calendarsCreated} calendar
                      </div>
                    </>
                  ) : (
                    <>
                      <div style={{ fontWeight: 600, color: 'var(--err)' }}>✗ {onbResult.error}</div>
                      <div className="mono" style={{ fontSize: 10.5, marginTop: 4, wordBreak: 'break-all' }}>{onbResult.detail}</div>
                    </>
                  )}
                </div>
              )}
            </div>
          </div>

          {/* Tenant list */}
          <div className="card">
            <div className="card-header">
              <div className="title"><Icon name="users" size={12}/> Tenant attivi ({tenantsList.length})</div>
            </div>
            <div className="card-body">
              <table className="tbl dense" data-testid="onb-tenants-list">
                <thead><tr><th>Tenant ID</th><th>Nome</th><th>Slug</th><th>Created</th><th>Stato</th></tr></thead>
                <tbody>
                  {tenantsList.length === 0 ? (
                    <tr><td colSpan={5} style={{ textAlign: 'center', fontSize: 11, color: 'var(--text-3)', padding: 20 }}>Nessun tenant. Crea il primo con il form sopra.</td></tr>
                  ) : tenantsList.map(t => (
                    <tr key={t.id}>
                      <td className="mono" style={{ fontSize: 11.5 }}>{t.id}</td>
                      <td style={{ fontWeight: 500 }}>{t.name}</td>
                      <td className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{t.slug}</td>
                      <td style={{ fontSize: 11, color: 'var(--text-3)' }}>{t.createdAt?.slice(0,10)}</td>
                      <td><Chip kind={t.active ? 'ok' : ''} dot>{t.active ? 'attivo' : 'off'}</Chip></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      )}

      {/* ==================== TAB: Connettori ==================== */}
      {tab === 'connect' && (
        <div className="grid" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header">
              <div className="title"><Icon name="link" size={12}/> Integrazioni enterprise</div>
              <div className="actions">
                <Btn variant="ghost" size="sm"><Icon name="search" size={11}/> Cerca</Btn>
                <Btn variant="ghost" size="sm"><Icon name="refresh" size={11}/> Sync tutti</Btn>
                <Btn variant="primary" size="sm"><Icon name="plus" size={11}/> Nuovo connettore custom</Btn>
              </div>
            </div>
            <div className="card-body">
              <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 12, padding: 10, background: 'var(--bg-2)', borderRadius: 8, border: '1px solid var(--line)' }}>
                <Icon name="info" size={11}/> {connectedCount} su {totalCount} connettori attivi. L'AI può leggere e scrivere sui sistemi abilitati; ogni azione richiede approvazione umana (human-in-the-loop).
              </div>
              {CONNECTOR_CATALOG.map(group => (
                <div key={group.cat} style={{ marginBottom: 18 }}>
                  <div className="eyebrow" style={{ marginBottom: 8 }}>{group.cat}</div>
                  <div className="grid grid-2" style={{ gap: 10 }}>
                    {group.items.map(c => (
                      <ConnectorCard key={c.id} c={{ ...c, status: connectors[c.id] }} onToggle={toggleConnector}/>
                    ))}
                  </div>
                </div>
              ))}
            </div>
          </div>

          <div className="grid grid-2" style={{ gap: 14 }}>
            <div className="card">
              <div className="card-header"><div className="title">Webhook in uscita</div></div>
              <div className="card-body">
                <div style={{ padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--text-3)', background: 'var(--bg-2)', borderRadius: 6 }}>
                  Nessun webhook configurato.
                  <div style={{ fontSize: 10.5, marginTop: 4 }}>Gestione completa in arrivo con FASE 21 (integrazioni esterne).</div>
                </div>
                <div style={{ marginTop: 10 }}>
                  <Btn variant="ghost" size="sm" disabled title="Disponibile in FASE 21"><Icon name="plus" size={11}/> Aggiungi webhook</Btn>
                </div>
              </div>
            </div>
            <div className="card">
              <div className="card-header"><div className="title">API Tokens</div></div>
              <div className="card-body">
                <div style={{ padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--text-3)', background: 'var(--bg-2)', borderRadius: 6 }}>
                  Nessun token API generato.
                  <div style={{ fontSize: 10.5, marginTop: 4 }}>Gestione completa in arrivo con FASE 22 (hardening + multi-tenancy).</div>
                </div>
                <div style={{ marginTop: 10 }}>
                  <Btn variant="ghost" size="sm" disabled title="Disponibile in FASE 22"><Icon name="plus" size={11}/> Genera token</Btn>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* ==================== TAB: UI ==================== */}
      {tab === 'ui' && (
        <div className="grid grid-2" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header"><div className="title">Preferenze interfaccia</div></div>
            <div className="card-body">
              <div className="field">
                <label>Tema</label>
                <div className="row" style={{ gap: 4 }}>
                  <button className={`btn sm ${theme==='dark'?'primary':'ghost'}`} onClick={() => setTheme('dark')}>Dark</button>
                  <button className={`btn sm ${theme==='light'?'primary':'ghost'}`} onClick={() => setTheme('light')}>Light</button>
                  <button className="btn sm ghost">Auto (sistema)</button>
                </div>
              </div>
              <div className="field">
                <label>Lingua</label>
                <select defaultValue="it">
                  <option value="it">Italiano</option>
                  <option value="en">English</option>
                  <option value="de">Deutsch</option>
                  <option value="fr">Français</option>
                </select>
              </div>
              <div className="field">
                <label>Fuso orario</label>
                <select defaultValue="rome"><option value="rome">Europe/Rome (UTC+1)</option><option value="utc">UTC</option></select>
              </div>
              <div className="field">
                <label>Densità</label>
                <div className="row" style={{ gap: 4 }}>
                  <button className="btn sm primary">Compatta (Bloomberg)</button>
                  <button className="btn sm ghost">Media</button>
                  <button className="btn sm ghost">Comfort</button>
                </div>
              </div>
              <div className="field">
                <label>Formato date</label>
                <div className="row" style={{ gap: 4 }}>
                  <button className="btn sm primary">DD/MM/YYYY</button>
                  <button className="btn sm ghost">YYYY-MM-DD</button>
                </div>
              </div>
              <div className="field">
                <label>Valuta di riferimento</label>
                <div className="row" style={{ gap: 4 }}>
                  <button className="btn sm primary">EUR €</button>
                  <button className="btn sm ghost">USD $</button>
                  <button className="btn sm ghost">GBP £</button>
                </div>
              </div>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><div className="title">Scorciatoie da tastiera</div></div>
            <div className="card-body" style={{ fontSize: 12.5 }}>
              {[
                ['⌘ K', 'Command palette globale'],
                ['⌘ /', 'Apri/chiudi Copilot'],
                ['⌘ B', 'Toggle sidebar'],
                ['G + D', 'Vai a Dashboard'],
                ['G + P', 'Vai a Progetti'],
                ['G + R', 'Vai a RdA'],
                ['N', 'Nuova entità (contestuale)'],
                ['?', 'Mostra questa lista'],
              ].map(([k, d]) => (
                <div key={k} className="row" style={{ justifyContent: 'space-between', padding: '7px 0', borderBottom: '1px dashed var(--line)' }}>
                  <span>{d}</span>
                  <kbd style={{ fontFamily: 'var(--mono)', fontSize: 11, padding: '2px 6px', border: '1px solid var(--line)', borderRadius: 4, background: 'var(--bg-2)', color: 'var(--text-2)' }}>{k}</kbd>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}

      {/* ==================== TAB: Governance ==================== */}
      {tab === 'gov' && (
        <div className="grid grid-2" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header"><div className="title"><Icon name="shield" size={12}/> Sicurezza & compliance</div></div>
            <div className="card-body" style={{ fontSize: 12.5, lineHeight: 1.8 }}>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Data residency</span><span className="mono" style={{ color: 'var(--text-2)' }}>EU · Frankfurt (eu-central-1)</span></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Backup giornaliero</span><Chip kind="ok" dot>attivo · 35gg</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Encryption at rest</span><Chip kind="ok" dot>AES-256 · KMS</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Encryption in transit</span><Chip kind="ok" dot>TLS 1.3</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>SSO SAML · Entra ID</span><Chip kind="info" dot>opzionale (no enforcement MVP)</Chip></div>
              <MfaTenantStatusRow />
              <MfaPersonalStatusRow user={user} />
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>ISO 27001 / SOC 2</span><Chip kind="ok" dot>certificato</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0' }}><span>GDPR DPA</span><Chip kind="ok" dot>firmato · 2025-11</Chip></div>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><div className="title">Governance AI</div></div>
            <div className="card-body" style={{ fontSize: 12.5, lineHeight: 1.8 }}>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Human-in-the-loop</span><Chip kind="ok" dot>obbligatorio scritture</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>PII masking pre-prompt</span><Chip kind="ok" dot>attivo</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Training opt-out</span><Chip kind="ok" dot>no-train clause</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Retention prompt/risposte</span><span className="mono" style={{ color: 'var(--text-2)' }}>24 mesi</span></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Audit log AI</span><Chip kind="ok" dot>immutabile</Chip></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}><span>Red-team reviews</span><span className="mono" style={{ color: 'var(--text-2)' }}>trimestrali</span></div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0' }}><span>EU AI Act · categoria</span><Chip kind="info" dot>limited risk</Chip></div>
            </div>
          </div>
          <div className="card" style={{ gridColumn: '1 / -1' }}>
            <div className="card-header"><div className="title">Audit log · ultime 24h</div><div className="actions"><Btn variant="ghost" size="sm"><Icon name="download" size={11}/> Export CSV</Btn></div></div>
            <div className="card-body">
              <table className="tbl dense">
                <thead><tr><th style={{width:120}}>Timestamp</th><th>Utente</th><th>Azione</th><th>Risorsa</th><th>IP</th><th>Esito</th></tr></thead>
                <tbody>
                  <tr><td className="mono">09:42:11</td><td>m.rossi</td><td>rda.approve</td><td className="mono">RDA-00418</td><td className="mono">10.12.4.81</td><td><Chip kind="ok" dot>ok</Chip></td></tr>
                  <tr><td className="mono">09:31:02</td><td>ai-agent</td><td>doc.draft</td><td className="mono">DOC-8812</td><td className="mono">internal</td><td><Chip kind="ok" dot>ok</Chip></td></tr>
                  <tr><td className="mono">09:18:55</td><td>g.bianchi</td><td>settings.update</td><td className="mono">ai.model=sonnet-4.5</td><td className="mono">10.12.4.102</td><td><Chip kind="ok" dot>ok</Chip></td></tr>
                  <tr><td className="mono">08:56:40</td><td>l.verdi</td><td>sal.reject</td><td className="mono">SAL-207-04</td><td className="mono">87.14.22.9</td><td><Chip kind="warn" dot>override</Chip></td></tr>
                  <tr><td className="mono">08:12:09</td><td>system</td><td>connector.sync</td><td className="mono">sap-s4</td><td className="mono">internal</td><td><Chip kind="ok" dot>ok</Chip></td></tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>
      )}

      {/* ==================== TAB: Notifiche (FASE 73 — live persistite per persona) ==================== */}
      {tab === 'notif' && <NotifichePersonalTab user={user} pushToast={pushToast} />}

      {/* ==================== TAB: Sessioni attive (FASE 53.3 sessione 58) ==================== */}
      {tab === 'sessions' && <SessionsTab />}

      {tab === 'delegations' && <DelegationsTab />}

      {/* ==================== TAB: Organizzazione ==================== */}
      {tab === 'org' && (
        <div className="grid" style={{ gap: 14 }}>
          <div className="card">
            <div className="card-header"><div className="title">Tenant Industrial Demo SpA</div><div className="desc">Multi-plant · 4 siti produttivi · 120 utenti attivi</div></div>
            <div className="card-body">
              <div className="grid grid-4">
                <Stat label="Utenti totali" value="120"/>
                <Stat label="Ruoli configurati" value="8"/>
                <Stat label="Plant attivi" value="4"/>
                <Stat label="Vendor accreditati" value="312"/>
              </div>
            </div>
          </div>
          <div className="grid grid-2" style={{ gap: 14 }}>
            <div className="card">
              <div className="card-header"><div className="title">Ruoli & permessi</div><div className="actions"><Btn variant="ghost" size="sm"><Icon name="plus" size={11}/> Nuovo ruolo</Btn></div></div>
              <div className="card-body">
                <table className="tbl dense">
                  <thead><tr><th>Ruolo</th><th>Utenti</th><th>Scope</th></tr></thead>
                  <tbody>
                    <tr><td>CFO / Direzione</td><td>3</td><td><Chip>tutto · read</Chip></td></tr>
                    <tr><td>Project Manager CAPEX</td><td>12</td><td><Chip>progetti assegnati</Chip></td></tr>
                    <tr><td>Procurement / Buyer</td><td>8</td><td><Chip>RdA, vendor</Chip></td></tr>
                    <tr><td>Controller</td><td>5</td><td><Chip>budget, SAL</Chip></td></tr>
                    <tr><td>Engineering lead</td><td>9</td><td><Chip>specs, capitolati</Chip></td></tr>
                    <tr><td>Plant manager</td><td>4</td><td><Chip>sito assegnato</Chip></td></tr>
                    <tr><td>Vendor esterno</td><td>76</td><td><Chip>proprio perimetro</Chip></td></tr>
                    <tr><td>Admin</td><td>3</td><td><Chip kind="err">full</Chip></td></tr>
                  </tbody>
                </table>
              </div>
            </div>
            <div className="card">
              <div className="card-header"><div className="title">Plant / Siti produttivi</div></div>
              <div className="card-body">
                <table className="tbl dense">
                  <thead><tr><th>Plant</th><th>Responsabile</th><th>Progetti attivi</th></tr></thead>
                  <tbody>
                    <tr><td>Torino · sede</td><td>G. Bianchi</td><td className="mono">7</td></tr>
                    <tr><td>Modena · stabilimento</td><td>A. Russo</td><td className="mono">6</td></tr>
                    <tr><td>Bari · nuovo sito</td><td>M. Ferrari</td><td className="mono">4</td></tr>
                    <tr><td>Poznań · PL</td><td>K. Nowak</td><td className="mono">3</td></tr>
                  </tbody>
                </table>
              </div>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><div className="title">Fatturazione & piano</div></div>
            <div className="card-body">
              <div className="grid grid-4">
                <Stat label="Piano" value="Enterprise"/>
                <Stat label="Rinnovo" value="2026-09-30"/>
                <Stat label="Seat utilizzati" value="120 / 150"/>
                <Stat label="Fatturato 2026 YTD" value="€ 84.2K"/>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
/**
 * Card per provider AI: gestisce input chiave (con hint quando già salvata),
 * salva/rimuovi via PATCH, esegue ping reale al provider, mostra esito test.
 *
 * Lo stato `pingState` è esterno e condiviso tra le card (un solo ping alla volta).
 */
function ProviderKeyCard({
  color, name, placeholder, endpoint,
  hasKey, keyHint, draft, setDraft,
  saving, onSave, onRemove, onPing, pingState,
  modelLabel, models, currentModel, onSelectModel, disabled,
}) {
  return (
    <div className="card">
      <div className="card-header">
        <div className="title">
          <span style={{ width: 10, height: 10, background: color, borderRadius: '50%', display: 'inline-block', marginRight: 6 }}/>
          {name}
        </div>
        <Chip kind={hasKey ? 'ok' : ''} dot>{hasKey ? 'API configurata' : 'non configurato'}</Chip>
      </div>
      <div className="card-body">
        <div className="field">
          <label>API key</label>
          <input
            type="password"
            placeholder={hasKey && !draft ? `${keyHint || '••••'} (lascia vuoto per non modificare)` : placeholder}
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            disabled={disabled || saving}
          />
          <div className="row" style={{ gap: 6, marginTop: 6 }}>
            <Btn variant="primary" size="sm" disabled={disabled || saving || !draft.trim()} onClick={onSave}>
              {saving ? 'Salvataggio…' : (hasKey ? 'Aggiorna chiave' : 'Salva chiave')}
            </Btn>
            {hasKey && (
              <Btn variant="ghost" size="sm" disabled={disabled || saving} onClick={onRemove}>
                Rimuovi
              </Btn>
            )}
            <span className="spacer"/>
            <Btn variant="ghost" size="sm" disabled={!hasKey || pingState?.status === 'pending'} onClick={onPing}>
              {pingState?.status === 'pending' ? 'Test in corso…' : 'Test connessione'}
            </Btn>
          </div>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 8 }}>Endpoint: {endpoint}</div>
          {pingState?.status === 'ok' && (
            <div style={{ marginTop: 8, padding: '8px 10px', border: '1px solid var(--ok, #2e7d32)', borderRadius: 6, background: 'rgba(46,125,50,0.08)', fontSize: 11.5 }}>
              <strong>OK</strong> · {pingState.latencyMs}ms ·  <span style={{ color: 'var(--text-2)' }}>{pingState.text.slice(0, 200)}</span>
            </div>
          )}
          {pingState?.status === 'err' && (
            <div style={{ marginTop: 8, padding: '8px 10px', border: '1px solid var(--err, #c0392b)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', fontSize: 11.5, color: 'var(--err, #c0392b)' }}>
              <strong>Errore</strong>: {String(pingState.error).slice(0, 300)}
            </div>
          )}
        </div>
        <div className="eyebrow" style={{ marginTop: 14, marginBottom: 6 }}>{modelLabel}</div>
        <div style={{ display: 'grid', gap: 6 }}>
          {models.map(m => (
            <ModelRow key={m.id} m={m} selected={currentModel === m.id} onSelect={onSelectModel}/>
          ))}
        </div>
        <CustomModelInput currentModel={currentModel} models={models} onSelect={onSelectModel} disabled={disabled}/>
      </div>
    </div>
  );
}

/**
 * Custom model input — permette di digitare un model id non presente nel
 * catalogo (es. modelli appena rilasciati, varianti experimental, alias di
 * provider). Visibile come dettaglio espandibile sotto la lista. Quando il
 * `currentModel` scelto NON è nel catalogo, l'input è visibile in default
 * (mostra il valore custom attivo). Altrimenti collapsed.
 */
function CustomModelInput({ currentModel, models, onSelect, disabled }) {
  const inCatalog = React.useMemo(() => models.some(m => m.id === currentModel), [models, currentModel]);
  const [expanded, setExpanded] = React.useState(!inCatalog);
  const [draft, setDraft] = React.useState(inCatalog ? '' : currentModel);

  React.useEffect(() => {
    if (!inCatalog) {
      setExpanded(true);
      setDraft(currentModel);
    }
  }, [currentModel, inCatalog]);

  function apply() {
    const v = (draft || '').trim();
    if (!v) return;
    onSelect(v);
  }

  return (
    <div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px dashed var(--border-1)' }}>
      <button
        type="button"
        onClick={() => setExpanded(e => !e)}
        style={{
          background: 'transparent', border: 0, padding: 0, cursor: 'pointer',
          fontSize: 11, color: 'var(--text-3)', display: 'inline-flex', alignItems: 'center', gap: 4,
        }}
      >
        <span style={{ fontFamily: 'monospace' }}>{expanded ? '▼' : '▶'}</span>
        Modello custom (non in catalogo)
        {!inCatalog && (
          <span style={{ marginLeft: 4, fontSize: 10, color: 'var(--brand-1)', fontWeight: 600 }}>· attivo</span>
        )}
      </button>
      {expanded && (
        <div style={{ marginTop: 8 }}>
          <input
            type="text"
            placeholder="es. claude-opus-4-7-20260115, gemini-3-pro-exp, deepseek-v3.2-exp..."
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            disabled={disabled}
            style={{ width: '100%', padding: '6px 8px', fontSize: 11.5, fontFamily: 'monospace' }}
          />
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6 }}>
            <Btn variant="ghost" size="sm" disabled={disabled || !draft.trim() || draft.trim() === currentModel} onClick={apply}>
              Usa modello custom
            </Btn>
            <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
              Il provider valida l'id. Errori 404/invalid_model arriveranno da BE.
            </span>
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================
// SessionsTab — FASE 53.3 (sessione 58): info sessione corrente + revoca tutte
// le altre. BE già esistente (sessione 38: jwt-blacklist + /api/auth/revoke).
// ============================================================
function SessionsTab() {
  const { user, pushToast } = useStore();
  const [info, setInfo] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);

  const reload = React.useCallback(async () => {
    setError(null);
    try {
      const r = await fetch('/api/auth/sessions', {
        credentials: 'same-origin',
        cache: 'no-store',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        if (j.error === 'unauthenticated' || j.error === 'invalid_session' || j.error === 'session_revoked') {
          setError('Sessione non valida — esegui logout e ri-login.');
        } else {
          setError(j.error || `HTTP ${r.status}`);
        }
        return;
      }
      setInfo(j);
    } catch (e) {
      setError(String(e?.message || e));
    }
  }, []);

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

  async function revokeAll() {
    if (!confirm('Revocare tutte le sessioni di questa persona (incluse le altre device)?\nDovrai effettuare nuovamente il login dopo questa azione.')) return;
    setBusy(true);
    try {
      const r = await fetch('/api/auth/revoke', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({}),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        if (j.error === 'redis_unavailable') {
          pushToast({ title: 'Redis non disponibile', desc: 'Riprova più tardi.', tone: 'err' });
        } else if (j.error === 'revocation_disabled') {
          pushToast({ title: 'Revoca disabilitata', desc: 'JWT_REVOCATION_ENABLED=false', tone: 'warn' });
        } else {
          pushToast({ title: 'Errore revoca', desc: j.detail || j.error || `HTTP ${r.status}`, tone: 'err' });
        }
        setBusy(false);
        return;
      }
      pushToast({
        title: 'Sessioni revocate',
        desc: `Tutti i token di questa persona sono ora invalidi (TTL ${j.revoked?.ttlSec ?? '?'}s).`,
        tone: 'ok',
      });
      // Lascio il caller logged-in finché il prossimo /me/auth check non fallisce
      setBusy(false);
      reload();
    } catch (e) {
      pushToast({ title: 'Errore revoca', desc: String(e), tone: 'err' });
      setBusy(false);
    }
  }

  return (
    <div className="grid" style={{ gap: 14 }}>
      <div className="card">
        <div className="card-header">
          <div className="title">Sessione corrente</div>
          <div className="desc">JWT cookie sid. La sessione è stateless: il sistema non traccia sessioni multiple in DB. La revoca invalida TUTTI i token di questa persona via Redis blacklist (FASE 22.E).</div>
        </div>
        <div className="card-body">
          {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>
          )}
          {!info && !error && (
            <div style={{ fontSize: 11, color: 'var(--text-3)' }}>Caricamento…</div>
          )}
          {info && (
            <div className="col" style={{ gap: 8, fontSize: 11.5 }}>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>Persona</span>
                <span><b>{info.persona.name}</b> · <code style={{ fontSize: 10.5 }}>{info.persona.email}</code></span>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>Token id (jti)</span>
                <code style={{ fontSize: 10.5 }}>{info.session.jti}</code>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>Emesso a (iat)</span>
                <code style={{ fontSize: 10.5 }}>{new Date(info.session.iatIso).toLocaleString('it-IT')}</code>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>Scade a (exp)</span>
                <code style={{ fontSize: 10.5 }}>{new Date(info.session.expIso).toLocaleString('it-IT')}</code>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>Ultimo login</span>
                <code style={{ fontSize: 10.5 }}>{info.lastLoginAt ? new Date(info.lastLoginAt).toLocaleString('it-IT') : '—'}</code>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                <span style={{ color: 'var(--text-3)' }}>IP corrente</span>
                <code style={{ fontSize: 10.5 }}>{info.currentRequest.ip || '—'}</code>
              </div>
              <div className="row" style={{ justifyContent: 'space-between', padding: '4px 0' }}>
                <span style={{ color: 'var(--text-3)' }}>User-Agent</span>
                <code style={{ fontSize: 10, maxWidth: '60%', textAlign: 'right', wordBreak: 'break-word' }}>
                  {info.currentRequest.userAgent || '—'}
                </code>
              </div>
            </div>
          )}
        </div>
      </div>

      <div className="card">
        <div className="card-header">
          <div className="title">Revoca tutte le sessioni</div>
          <div className="desc">Invalida tutti i token JWT della persona corrente (incluse le altre device). Necessario fare di nuovo login.</div>
        </div>
        <div className="card-body">
          <Btn
            variant="primary"
            size="sm"
            disabled={busy || !info}
            onClick={revokeAll}
            style={{ background: 'var(--err)', borderColor: 'var(--err)' }}
          >{busy ? 'Revocando…' : '🔒 Revoca tutte le sessioni'}</Btn>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// AiPersonalOverrideCard — Sessione 65 (chiusura FASE 20)
// ============================================================
// Override personale di mode + primaryProvider per il current user.
// Persistenza in localStorage `lgs.ai.user.<personaId>.preferences`.
// Header proposti per future BE consumption: X-AI-Mode-Override,
// X-AI-Provider-Override (oggi solo persistenza locale).
//
// Default = no override → fallback su config tenant (ai_credentials).
// Sessione 69 — rimosso 'simulated' (cleanup pilot/simulated)
const AI_OVERRIDE_MODES = ['default', 'live', 'hybrid'];
const AI_OVERRIDE_PROVIDERS = ['default', 'auto', 'claude', 'gemini', 'deepseek'];
const AI_OVERRIDE_LS_KEY = (personaId) => `lgs.ai.user.${personaId}.preferences`;

function loadAiPersonalOverride(personaId) {
  if (!personaId) return { modeOverride: 'default', providerOverride: 'default' };
  try {
    const raw = localStorage.getItem(AI_OVERRIDE_LS_KEY(personaId));
    if (!raw) return { modeOverride: 'default', providerOverride: 'default' };
    const j = JSON.parse(raw);
    return {
      modeOverride: AI_OVERRIDE_MODES.includes(j.modeOverride) ? j.modeOverride : 'default',
      providerOverride: AI_OVERRIDE_PROVIDERS.includes(j.providerOverride) ? j.providerOverride : 'default',
    };
  } catch {
    return { modeOverride: 'default', providerOverride: 'default' };
  }
}

function saveAiPersonalOverride(personaId, prefs) {
  if (!personaId) return;
  try {
    localStorage.setItem(AI_OVERRIDE_LS_KEY(personaId), JSON.stringify(prefs));
  } catch {
    // localStorage may be unavailable (quota / SSR fallback)
  }
}

// Sessione 88 (FASE 15.B) — Gmail OAuth2 inbound credential card.
// SUPERADMIN-only operativo (UI è readonly per altri ruoli).
function GmailInboundCard() {
  const { user, pushToast } = useStore();
  const [status, setStatus] = React.useState(null); // null=loading, {connected:false}, {connected:true, ...}
  const [busy, setBusy] = React.useState(false);
  const [pullBusy, setPullBusy] = React.useState(false);
  const [lastPullResult, setLastPullResult] = React.useState(null);

  async function loadStatus() {
    try {
      const r = await fetch('/api/settings/gmail-oauth/status', {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (r.status === 403) { setStatus({ forbidden: true }); return; }
      if (!r.ok) { setStatus({ connected: false }); return; }
      const j = await r.json();
      setStatus(j);
    } catch {
      setStatus({ connected: false, error: 'network' });
    }
  }

  React.useEffect(() => {
    loadStatus();
    // Check query string per OAuth callback feedback
    const params = new URLSearchParams(window.location.search);
    const oauthState = params.get('gmail-oauth');
    if (oauthState === 'success') {
      const account = params.get('account');
      pushToast({ title: 'Gmail connesso', desc: `Account ${account || 'autorizzato'} pronto per inbound.`, tone: 'ok' });
      // Cleanup URL
      window.history.replaceState({}, '', window.location.pathname);
    } else if (oauthState === 'denied') {
      pushToast({ title: 'Consent OAuth negato', desc: 'L\'utente Google ha rifiutato l\'autorizzazione.', tone: 'warn' });
      window.history.replaceState({}, '', window.location.pathname);
    } else if (oauthState === 'exchange_failed' || oauthState === 'persist_failed' || oauthState === 'invalid' || oauthState === 'invalid_state') {
      const detail = params.get('detail');
      pushToast({ title: `OAuth ${oauthState}`, desc: detail || 'Errore durante il flow OAuth.', tone: 'err' });
      window.history.replaceState({}, '', window.location.pathname);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  async function handleConnect() {
    if (busy) return;
    setBusy(true);
    try {
      const r = await fetch('/api/settings/gmail-oauth/init', {
        credentials: 'same-origin',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'OAuth non configurato', desc: j?.detail || j?.error || 'Verifica env vars GMAIL_OAUTH_*', tone: 'err' });
        return;
      }
      // Redirect a Google consent
      window.location.href = j.url;
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setBusy(false);
    }
  }

  async function handleDisconnect() {
    if (busy) return;
    if (!confirm('Disconnettere la credenziale Gmail? Lo storico mail già importate non viene toccato, ma le nuove mail non verranno più pullate.')) return;
    setBusy(true);
    try {
      const r = await fetch('/api/settings/gmail-oauth/status', {
        method: 'DELETE',
        credentials: 'same-origin',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'Disconnessione fallita', desc: j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'Gmail disconnesso', desc: `Account ${j.accountEmail} disconnesso.`, tone: 'ok' });
      await loadStatus();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setBusy(false);
    }
  }

  async function handlePullNow() {
    if (pullBusy) return;
    setPullBusy(true);
    setLastPullResult(null);
    try {
      const r = await fetch('/api/admin/gmail-inbound/pull-now', {
        method: 'POST',
        credentials: 'same-origin',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'Pull fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      setLastPullResult(j.results);
      const totalInserted = (j.results || []).reduce((a, r) => a + (r.inserted || 0), 0);
      const totalFailed = (j.results || []).reduce((a, r) => a + (r.failed || 0), 0);
      pushToast({
        title: 'Pull completato',
        desc: `${totalInserted} nuove mail · ${totalFailed} errori. Hook auto-classify in esecuzione async.`,
        tone: totalFailed > 0 ? 'warn' : 'ok',
      });
      await loadStatus();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setPullBusy(false);
    }
  }

  if (status === null) {
    return <div className="card"><div className="card-body" style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento stato Gmail…</div></div>;
  }

  if (status.forbidden) {
    return (
      <div className="card">
        <div className="card-header">
          <div className="title"><Icon name="mail" size={12}/> Gmail inbound</div>
          <div className="actions"><Chip kind="warn" dot>SUPERADMIN only</Chip></div>
        </div>
        <div className="card-body" style={{ fontSize: 12, color: 'var(--text-2)' }}>
          La configurazione Gmail OAuth richiede ruolo SUPERADMIN. Chiedi a un amministratore di sistema.
        </div>
      </div>
    );
  }

  return (
    <div className="grid" style={{ gap: 14 }}>
      <div className="card">
        <div className="card-header">
          <div>
            <div className="title"><Icon name="mail" size={12}/> Gmail Inbound · OAuth2</div>
            <div className="desc">Pull periodico (ogni 2 min) di mail da Gmail via Gmail API REST. Le mail vengono auto-link a project/RDA/vendor e classificate dall'AI per signal (delay/risk/action/...).</div>
          </div>
          <div className="actions">
            {status.connected ? (
              <Chip kind="ok" dot data-testid="gmail-oauth-connected">Connesso</Chip>
            ) : (
              <Chip kind="info" dot data-testid="gmail-oauth-not-connected">Non connesso</Chip>
            )}
          </div>
        </div>
        <div className="card-body">
          {!status.connected ? (
            <div className="col" style={{ gap: 10 }}>
              <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
                Connetti un account Gmail (es. <code className="mono">inboundveridanto@gmail.com</code>) per
                abilitare l'ingestion automatica. Il refresh token sarà cifrato AES-256-GCM at rest
                e mai esposto al FE.
              </div>
              <div style={{ fontSize: 11, color: 'var(--text-3)', padding: 10, background: 'var(--bg-2)', borderRadius: 6 }}>
                <Icon name="info" size={11}/> <b>Prerequisiti</b>: env vars <code className="mono">GMAIL_OAUTH_CLIENT_ID</code>,
                <code className="mono"> GMAIL_OAUTH_CLIENT_SECRET</code>, <code className="mono">GMAIL_OAUTH_REDIRECT_URI</code> impostate.
                Project Google Cloud con Gmail API abilitata + OAuth consent screen configurato.
              </div>
              <div>
                <Btn variant="primary" size="sm" onClick={handleConnect} disabled={busy} data-action="gmail-oauth-connect">
                  <Icon name="link" size={11}/> {busy ? 'Inizializzazione…' : 'Connetti account Gmail'}
                </Btn>
              </div>
            </div>
          ) : (
            <div className="col" style={{ gap: 10 }}>
              <div className="grid grid-3" style={{ gap: 10 }}>
                <div><div className="eyebrow">Account</div><div className="mono" style={{ fontSize: 12 }}>{status.accountEmail}</div></div>
                <div><div className="eyebrow">Refresh token hint</div><div className="mono" style={{ fontSize: 12 }}>{status.refreshTokenHint}</div></div>
                <div><div className="eyebrow">Mail importate</div><div style={{ fontSize: 14, fontWeight: 500 }}>{status.totalMessagesImported}</div></div>
              </div>
              <div className="grid grid-3" style={{ gap: 10 }}>
                <div><div className="eyebrow">Ultimo sync</div><div className="mono" style={{ fontSize: 11 }}>{status.lastSyncAt || '—'}</div></div>
                <div><div className="eyebrow">History ID</div><div className="mono" style={{ fontSize: 11 }}>{status.lastHistoryId || '—'}</div></div>
                <div><div className="eyebrow">Connesso il</div><div className="mono" style={{ fontSize: 11 }}>{status.connectedAt ? new Date(status.connectedAt).toLocaleString('it-IT') : '—'}</div></div>
              </div>
              {status.lastError && (
                <div style={{ padding: 10, background: 'color-mix(in oklch, var(--err) 12%, var(--bg-1))', border: '1px solid var(--err)', borderRadius: 4, fontSize: 11.5 }}>
                  <Icon name="warning_tri" size={11}/> Ultimo errore: <code className="mono">{status.lastError}</code>
                </div>
              )}
              <div className="row" style={{ gap: 8, marginTop: 6 }}>
                <Btn variant="ai" size="sm" onClick={handlePullNow} disabled={pullBusy} data-action="gmail-pull-now">
                  <Icon name="refresh" size={11}/> {pullBusy ? 'Pull in corso…' : 'Pull adesso (manuale)'}
                </Btn>
                <Btn variant="ghost" size="sm" onClick={handleDisconnect} disabled={busy} data-action="gmail-oauth-disconnect">
                  <Icon name="x" size={11}/> Disconnetti
                </Btn>
              </div>
            </div>
          )}
        </div>
      </div>

      {lastPullResult && lastPullResult.length > 0 && (
        <div className="card">
          <div className="card-header"><div className="title">Risultato ultimo pull manuale</div></div>
          <div className="card-body">
            <table className="tbl dense" style={{ width: '100%' }}>
              <thead>
                <tr>
                  <th>Account</th><th className="num">Fetched</th><th className="num">Inserted</th><th className="num">Duplicates</th><th className="num">Failed</th><th className="num">Ms</th>
                </tr>
              </thead>
              <tbody>
                {lastPullResult.map((r, i) => (
                  <tr key={i}>
                    <td className="mono" style={{ fontSize: 11 }}>{r.accountEmail}</td>
                    <td className="num">{r.totalFetched}</td>
                    <td className="num"><Chip kind={r.inserted > 0 ? 'ok' : 'info'}>{r.inserted}</Chip></td>
                    <td className="num">{r.duplicates}</td>
                    <td className="num">{r.failed > 0 ? <Chip kind="err">{r.failed}</Chip> : r.failed}</td>
                    <td className="num">{r.durationMs}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            {lastPullResult[0]?.errors?.length > 0 && (
              <div style={{ marginTop: 8 }}>
                <div className="eyebrow">Errori (primi {lastPullResult[0].errors.length})</div>
                <ul style={{ fontSize: 11, color: 'var(--text-3)', paddingLeft: 18 }}>
                  {lastPullResult[0].errors.map((e, i) => <li key={i} className="mono">{e}</li>)}
                </ul>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

function AiPersonalOverrideCard({ userId, userName }) {
  const [prefs, setPrefs] = React.useState(() => loadAiPersonalOverride(userId));

  React.useEffect(() => {
    setPrefs(loadAiPersonalOverride(userId));
  }, [userId]);

  function update(field, value) {
    const next = { ...prefs, [field]: value };
    setPrefs(next);
    saveAiPersonalOverride(userId, next);
  }

  function reset() {
    const next = { modeOverride: 'default', providerOverride: 'default' };
    setPrefs(next);
    saveAiPersonalOverride(userId, next);
  }

  const isCustom =
    prefs.modeOverride !== 'default' || prefs.providerOverride !== 'default';

  return (
    <div className="card" style={{ gridColumn: '1 / -1' }} data-testid="ai-personal-override">
      <div className="card-header">
        <div>
          <div className="title">Override personale · {userName || 'utente'}</div>
          <div className="desc">
            Imposta una preferenza individuale di modalità/provider AI per le tue chiamate.
            Lascia su <strong>default</strong> per usare la configurazione del tenant.
            Salvataggio locale (no DB).
          </div>
        </div>
        <div className="actions">
          {isCustom && (
            <Btn variant="ghost" size="sm" onClick={reset}>
              <Icon name="refresh" size={11}/> Reset a default
            </Btn>
          )}
          <Chip kind={isCustom ? 'ai' : 'info'} dot>
            {isCustom ? 'override attivo' : 'segue tenant'}
          </Chip>
        </div>
      </div>
      <div className="card-body">
        <div className="grid grid-2" style={{ gap: 14 }}>
          <div className="field">
            <label>Modalità di esecuzione (override)</label>
            <div className="row" style={{ gap: 4, flexWrap: 'wrap' }}>
              {AI_OVERRIDE_MODES.map((m) => (
                <button
                  key={m}
                  className={`btn sm ${prefs.modeOverride === m ? 'primary' : 'ghost'}`}
                  onClick={() => update('modeOverride', m)}
                  data-mode={m}
                >
                  {m === 'default' ? 'Default tenant' :
                   m === 'live' ? 'Live solo primario' : 'Hybrid auto-retry'}
                </button>
              ))}
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>
              Solo le chiamate AI lanciate da te useranno questa modalità. Worker/cron usano sempre il default tenant.
            </div>
          </div>

          <div className="field">
            <label>Provider preferito (override)</label>
            <div className="row" style={{ gap: 4, flexWrap: 'wrap' }}>
              {AI_OVERRIDE_PROVIDERS.map((p) => (
                <button
                  key={p}
                  className={`btn sm ${prefs.providerOverride === p ? 'primary' : 'ghost'}`}
                  onClick={() => update('providerOverride', p)}
                  data-provider={p}
                >
                  {p === 'default' ? 'Default tenant' : p === 'auto' ? 'Auto' : p[0].toUpperCase() + p.slice(1)}
                </button>
              ))}
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>
              Override applicato solo se la chiave del provider scelto è configurata sul tenant.
            </div>
          </div>
        </div>

        <div style={{
          marginTop: 12,
          padding: 10,
          borderRadius: 4,
          background: 'var(--bg-2)',
          fontSize: 11,
          color: 'var(--text-2)',
          fontFamily: 'var(--font-mono)',
        }}>
          <strong>Storage:</strong> <code>{AI_OVERRIDE_LS_KEY(userId || '<persona>')}</code>
          <br/>
          <strong>Header BE proposti</strong> (futuro): <code>X-AI-Mode-Override</code>,{' '}
          <code>X-AI-Provider-Override</code>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// DelegationsTab — Sessione 65 — Deleghe uscenti del current user
// ============================================================
// Mostra le delegations dove `fromPersonaId === user.id`. Read-only:
// la creazione/modifica delle deleghe resta in Customizing (admin-only).
//
// Stato delega derivato:
//  - 'active' se now() in [fromDate, toDate] AND row.active=true
//  - 'scheduled' se now() < fromDate
//  - 'expired' se now() > toDate
//  - 'inactive' se row.active=false
function DelegationsTab() {
  const { user, seed, pushToast } = useStore();
  const [rows, setRows] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [createOpen, setCreateOpen] = React.useState(false);
  const [reloadKey, setReloadKey] = React.useState(0);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/config/delegations', {
          credentials: 'same-origin',
          cache: 'no-store',
          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        });
        if (!r.ok) {
          if (!cancelled) setError(`HTTP ${r.status}`);
          return;
        }
        const j = await r.json();
        if (!cancelled) setRows(Array.isArray(j.data) ? j.data : []);
      } catch (e) {
        if (!cancelled) setError(String(e?.message || e));
      }
    })();
    return () => { cancelled = true; };
  }, [user?.id, reloadKey]);

  function deriveState(d) {
    if (!d.active) return { state: 'inactive', label: 'inattiva', tone: '' };
    const now = Date.now();
    const from = d.fromDate ? new Date(d.fromDate).getTime() : 0;
    const to = d.toDate ? new Date(d.toDate).getTime() : Number.POSITIVE_INFINITY;
    if (now < from) return { state: 'scheduled', label: 'pianificata', tone: 'info' };
    if (now > to) return { state: 'expired', label: 'scaduta', tone: '' };
    return { state: 'active', label: 'attiva', tone: 'ok' };
  }

  // Filter solo le deleghe uscenti (fromPersonaId === user.id).
  const myOutgoing = rows
    ? rows.filter((d) => d.fromPersonaId === user?.id || d.fromPersona === user?.id)
    : null;

  return (
    <div className="card">
      <div className="card-header">
        <div>
          <div className="title">Deleghe uscenti · {user?.name}</div>
          <div className="desc">
            Sostituzioni che hai impostato (es. ferie, malattia). FASE 19 — creazione self-service.
          </div>
        </div>
        <div className="actions">
          <Btn variant="primary" size="sm" onClick={() => setCreateOpen(true)} data-action="create-delegation">
            <Icon name="plus" size={11}/> Crea delega
          </Btn>
        </div>
      </div>
      <CreateDelegationModal
        open={createOpen}
        onClose={() => setCreateOpen(false)}
        onCreated={() => { setReloadKey((k) => k + 1); setCreateOpen(false); pushToast({ title: 'Delega creata', tone: 'ok' }); }}
        currentUser={user}
        personas={seed?.PERSONAS || []}
      />

      <div className="card-body">
        {error && <div style={{ fontSize: 12, color: 'var(--err)' }}>Errore: {error}</div>}
        {!error && myOutgoing === null && (
          <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento deleghe…</div>
        )}
        {!error && myOutgoing !== null && myOutgoing.length === 0 && (
          <div style={{ fontSize: 12, color: 'var(--text-3)', padding: 16, textAlign: 'center' }}>
            Nessuna delega uscente impostata.
          </div>
        )}
        {!error && myOutgoing && myOutgoing.length > 0 && (
          <table className="tbl">
            <thead>
              <tr>
                <th>Sostituto</th>
                <th>Periodo</th>
                <th>Motivazione</th>
                <th style={{ width: 110 }}>Stato</th>
              </tr>
            </thead>
            <tbody>
              {myOutgoing.map((d) => {
                const st = deriveState(d);
                return (
                  <tr key={d.id}>
                    <td>
                      <div style={{ fontWeight: 500 }}>{d.toUser || d.toPersonaId}</div>
                      {d.toPersonaId !== d.toUser && (
                        <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{d.toPersonaId}</div>
                      )}
                    </td>
                    <td className="mono" style={{ fontSize: 11 }}>
                      {d.fromDate ? new Date(d.fromDate).toLocaleDateString('it-IT') : '—'}
                      {' → '}
                      {d.toDate ? new Date(d.toDate).toLocaleDateString('it-IT') : '∞'}
                    </td>
                    <td style={{ fontSize: 11.5, color: 'var(--text-2)' }}>
                      {d.reason || '—'}
                    </td>
                    <td>
                      <Chip kind={st.tone}>{st.label}</Chip>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

// ============================================================
// FASE 19 — CreateDelegationModal: form self-service per delega temporanea
// ============================================================
function CreateDelegationModal({ open, onClose, onCreated, currentUser, personas }) {
  const today = new Date().toISOString().slice(0, 10);
  const tomorrow = new Date(Date.now() + 86400 * 1000).toISOString().slice(0, 10);
  const oneWeek = new Date(Date.now() + 7 * 86400 * 1000).toISOString().slice(0, 10);

  const [toPersonaId, setToPersonaId] = React.useState('');
  const [fromDate, setFromDate] = React.useState(tomorrow);
  const [toDate, setToDate] = React.useState(oneWeek);
  const [reason, setReason] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    if (open) {
      setToPersonaId('');
      setFromDate(tomorrow);
      setToDate(oneWeek);
      setReason('');
      setError(null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  if (!open) return null;

  const eligible = (personas || []).filter((p) => p.id !== currentUser?.id && (p.active !== false));
  const dateInvalid = fromDate >= toDate;
  const canSubmit = toPersonaId && fromDate && toDate && !dateInvalid && !submitting;

  const submit = async () => {
    if (!canSubmit || !currentUser?.id) return;
    setSubmitting(true);
    setError(null);
    try {
      const r = await fetch('/api/config/delegations', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': currentUser.id },
        body: JSON.stringify({
          fromPersonaId: currentUser.id,
          toPersonaId,
          fromDate,
          toDate,
          reason: reason.trim() || null,
          active: true,
        }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        const issue = j.issues?.[0]?.message || j.error || 'HTTP ' + r.status;
        throw new Error(issue);
      }
      onCreated && onCreated(j.data);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && onClose()} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.4)', display: 'grid', placeItems: 'center', zIndex: 1000 }}>
      <div className="modal" style={{ background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 8, width: 480, padding: 18 }} data-modal="create-delegation">
        <div className="row" style={{ marginBottom: 14 }}>
          <h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Nuova delega uscente</h3>
          <span className="spacer"/>
          <button onClick={onClose} style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: 'var(--text-3)' }}>
            <Icon name="x" size={14}/>
          </button>
        </div>

        <div className="col" style={{ gap: 12 }}>
          <div className="field">
            <label className="eyebrow" style={{ marginBottom: 4, display: 'block' }}>Sostituto *</label>
            <div data-field="toPersonaId">
              <PersonaAutocomplete
                value={toPersonaId || null}
                onChange={(id) => setToPersonaId(id || '')}
                personasFallback={personas || []}
                excludeIds={currentUser?.id ? [currentUser.id] : []}
                placeholder="Cerca sostituto…"
              />
            </div>
            {eligible.length === 0 && (
              <div style={{ fontSize: 11, color: 'var(--warn)', marginTop: 4 }}>Nessuna persona disponibile come sostituto.</div>
            )}
          </div>

          <div className="row" style={{ gap: 12 }}>
            <div className="field" style={{ flex: 1 }}>
              <label className="eyebrow" style={{ marginBottom: 4, display: 'block' }}>Da *</label>
              <input type="date" value={fromDate} min={today} onChange={(e) => setFromDate(e.target.value)} style={{ width: '100%', fontSize: 12 }} data-field="fromDate"/>
            </div>
            <div className="field" style={{ flex: 1 }}>
              <label className="eyebrow" style={{ marginBottom: 4, display: 'block' }}>A *</label>
              <input type="date" value={toDate} min={fromDate} onChange={(e) => setToDate(e.target.value)} style={{ width: '100%', fontSize: 12 }} data-field="toDate"/>
            </div>
          </div>
          {dateInvalid && (
            <div style={{ fontSize: 11, color: 'var(--err)' }}>La data di fine deve essere dopo l'inizio.</div>
          )}

          <div className="field">
            <label className="eyebrow" style={{ marginBottom: 4, display: 'block' }}>Motivazione (opzionale)</label>
            <textarea
              rows={3}
              maxLength={500}
              value={reason}
              onChange={(e) => setReason(e.target.value)}
              placeholder="es. Ferie estive · Trasferta cliente · Malattia"
              style={{ width: '100%', fontSize: 12 }}
              data-field="reason"
            />
            <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 2 }}>{reason.length}/500</div>
          </div>

          {error && (
            <div style={{ padding: 8, fontSize: 11.5, color: 'var(--err)', background: 'color-mix(in oklch, var(--err) 6%, transparent)', borderRadius: 4 }}>
              {error}
            </div>
          )}
        </div>

        <div className="row" style={{ marginTop: 16, gap: 8, justifyContent: 'flex-end' }}>
          <Btn variant="ghost" size="sm" onClick={onClose} disabled={submitting}>Annulla</Btn>
          <Btn variant="primary" size="sm" onClick={submit} disabled={!canSubmit} data-action="submit-delegation">
            {submitting ? 'Salvataggio…' : 'Crea delega'}
          </Btn>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// FASE 73 — NotifichePersonalTab: preferences live per current user
// ============================================================
function NotifichePersonalTab({ user, pushToast }) {
  const [catalog, setCatalog] = React.useState([]);
  const [prefs, setPrefs] = React.useState({}); // by eventCode
  const [loading, setLoading] = React.useState(true);
  const [savingCode, setSavingCode] = React.useState(null);
  const [error, setError] = React.useState(null);

  const reload = React.useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const r = await fetch('/api/notification-preferences', {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      setCatalog(j.catalog || []);
      const map = {};
      for (const p of (j.data || [])) map[p.eventCode] = p;
      setPrefs(map);
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setLoading(false);
    }
  }, [user?.id]);

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

  const upsertPref = async (eventCode, partial) => {
    if (!user?.id || savingCode) return;
    setSavingCode(eventCode);
    try {
      const existing = prefs[eventCode];
      const catalogItem = catalog.find((c) => c.code === eventCode);
      const defaults = catalogItem?.defaultChannels || ['in_app'];
      const body = {
        eventCode,
        channels: existing?.channels ?? defaults,
        quietStart: existing?.quietStart ?? null,
        quietEnd: existing?.quietEnd ?? null,
        digestMode: existing?.digestMode ?? 'realtime',
        escalateAfterMinutes: existing?.escalateAfterMinutes ?? 0,
        ...partial,
      };
      const r = await fetch('/api/notification-preferences', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user.id },
        credentials: 'same-origin',
        body: JSON.stringify(body),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      if (j.changed) {
        setPrefs((p) => ({ ...p, [eventCode]: j.data }));
        pushToast({ title: 'Preferenza salvata', desc: eventCode, tone: 'ok' });
      }
    } catch (e) {
      pushToast({ title: 'Errore salvataggio', desc: String(e?.message || e).slice(0, 120), tone: 'err' });
    } finally {
      setSavingCode(null);
    }
  };

  const toggleChannel = (eventCode, channel) => {
    const existing = prefs[eventCode];
    const catalogItem = catalog.find((c) => c.code === eventCode);
    const current = existing?.channels ?? catalogItem?.defaultChannels ?? ['in_app'];
    const next = current.includes(channel)
      ? current.filter((c) => c !== channel)
      : [...current, channel];
    upsertPref(eventCode, { channels: next });
  };

  return (
    <div className="card">
      <div className="card-header">
        <div>
          <div className="title">Le mie preferenze notifiche · {user?.name}</div>
          <div className="desc">Toggle per evento × canale persistiti per persona. Default da catalog se mai customizzati.</div>
        </div>
        <div className="actions">
          <Btn variant="ghost" size="sm" onClick={reload} disabled={loading}><Icon name="refresh" size={11}/> Aggiorna</Btn>
        </div>
      </div>
      <div className="card-body">
        {error && <div style={{ color: 'var(--err)', fontSize: 12, marginBottom: 10 }}>Errore: {error}</div>}
        {loading ? <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento…</div> : (
          <table className="tbl dense">
            <thead>
              <tr>
                <th>Evento</th>
                <th style={{ width: 70, textAlign: 'center' }}>In-app</th>
                <th style={{ width: 70, textAlign: 'center' }}>Email</th>
                <th style={{ width: 70, textAlign: 'center' }}>Teams</th>
                <th style={{ width: 70, textAlign: 'center' }}>SMS</th>
                <th style={{ width: 110 }}>Digest</th>
                <th style={{ width: 80, textAlign: 'center' }}>Stato</th>
              </tr>
            </thead>
            <tbody>
              {catalog.map((c) => {
                const pref = prefs[c.code];
                const channels = pref?.channels ?? c.defaultChannels;
                const isCustom = !!pref;
                const isSaving = savingCode === c.code;
                return (
                  <tr key={c.code} data-pref-event={c.code}>
                    <td>
                      <div style={{ fontWeight: 500, fontSize: 12 }}>{c.label}</div>
                      <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{c.code}</div>
                    </td>
                    {['in_app', 'email', 'teams', 'sms'].map((ch) => (
                      <td key={ch} style={{ textAlign: 'center' }}>
                        <input
                          type="checkbox"
                          checked={channels.includes(ch)}
                          disabled={isSaving}
                          onChange={() => toggleChannel(c.code, ch)}
                          data-channel={ch}
                        />
                      </td>
                    ))}
                    <td>
                      <select
                        value={pref?.digestMode ?? 'realtime'}
                        disabled={isSaving}
                        onChange={(e) => upsertPref(c.code, { digestMode: e.target.value })}
                        style={{ fontSize: 11, width: '100%' }}
                      >
                        <option value="realtime">realtime</option>
                        <option value="daily">daily</option>
                        <option value="weekly">weekly</option>
                        <option value="off">off</option>
                      </select>
                    </td>
                    <td style={{ textAlign: 'center' }}>
                      {isSaving ? <span className="mono" style={{ fontSize: 10, color: 'var(--accent)' }}>…</span>
                        : isCustom ? <Chip kind="ok" dot>salvato</Chip>
                        : <Chip kind="info" dot>default</Chip>}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
        <div style={{ marginTop: 10, fontSize: 10.5, color: 'var(--text-3)' }}>
          <Icon name="info" size={10}/> Modifiche persistite via POST /api/notification-preferences (audit log + diff field-level).
          Default ereditati dal catalog: in_app sempre, email/teams/sms variabile per evento.
        </div>
      </div>
    </div>
  );
}

// ============================================================
// FASE 73 — MfaTenantStatusRow / MfaPersonalStatusRow (honest, no hardcoded)
// ============================================================
function MfaTenantStatusRow() {
  const [stats, setStats] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/admin/mfa/stats', { credentials: 'same-origin', cache: 'no-store' });
        const j = await r.json();
        if (!cancelled && r.ok) setStats(j.data);
      } catch { /* silent */ }
    })();
    return () => { cancelled = true; };
  }, []);
  if (!stats) return (
    <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}>
      <span>MFA tenant coverage</span>
      <span className="mono" style={{ color: 'var(--text-3)' }}>—</span>
    </div>
  );
  const tone = stats.coveragePct >= 80 ? 'ok' : stats.coveragePct >= 40 ? 'warn' : 'err';
  return (
    <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)' }}>
      <span>MFA tenant coverage</span>
      <Chip kind={tone} dot>{stats.mfaVerified}/{stats.totalPersonas} verified ({stats.coveragePct}%)</Chip>
    </div>
  );
}

function MfaPersonalStatusRow({ user }) {
  const { pushToast } = useStore();
  const [status, setStatus] = React.useState(null);
  const [showSetup, setShowSetup] = React.useState(false);
  const [showDisable, setShowDisable] = React.useState(false);

  async function reload() {
    if (!user?.id) return;
    try {
      const r = await fetch('/api/auth/mfa/status', { credentials: 'same-origin', cache: 'no-store', headers: { 'X-Actor-Persona-Id': user.id } });
      const j = await r.json();
      if (r.ok) setStatus(j.data);
    } catch { /* silent */ }
  }

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      await reload();
      if (cancelled) setStatus(null);
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line
  }, [user?.id]);

  if (!status) return null;
  const kind = status.verified ? 'ok' : status.method !== 'none' ? 'warn' : 'info';
  const label = status.verified
    ? `${status.method} verified · ${status.backupCodesAvailable} backup`
    : status.method !== 'none'
      ? `${status.method} setup pending`
      : 'non attivata';

  return (
    <>
      <div className="row" style={{ justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px dashed var(--line)', alignItems:'center' }}>
        <span>MFA tua ({user?.name})</span>
        <div className="row" style={{ gap: 6 }}>
          <Chip kind={kind} dot data-testid="mfa-personal-status-chip">{label}</Chip>
          {!status.verified && (
            <Btn variant="primary" size="sm" onClick={() => setShowSetup(true)} data-testid="mfa-setup-btn">
              <Icon name="lock" size={11}/> Configura MFA
            </Btn>
          )}
          {status.verified && (
            <Btn variant="ghost" size="sm" onClick={() => setShowDisable(true)} data-testid="mfa-disable-btn">
              Disabilita
            </Btn>
          )}
        </div>
      </div>
      {showSetup && (
        <MfaSetupWizardModal
          user={user}
          confirmReset={status.method !== 'none'}
          onClose={() => setShowSetup(false)}
          onDone={() => { setShowSetup(false); reload(); pushToast({ title: 'MFA attivata', desc: 'Codici di backup salvati. Conservali in luogo sicuro.', tone: 'ok' }); }}
        />
      )}
      {showDisable && (
        <MfaDisableModal
          user={user}
          onClose={() => setShowDisable(false)}
          onDone={() => { setShowDisable(false); reload(); pushToast({ title: 'MFA disattivata', tone: 'ok' }); }}
        />
      )}
    </>
  );
}

// FASE 22.X (sessione 75 A1) — Setup wizard 3 step:
//   1. POST /api/auth/mfa/setup-init → QR + secret
//   2. Utente scansiona QR + inserisce primo code 6-digit
//   3. POST /api/auth/mfa/setup-verify → backup codes plaintext (mostra UNA volta)
function MfaSetupWizardModal({ user, confirmReset, onClose, onDone }) {
  const [step, setStep] = React.useState('init'); // init | scan | done
  const [initData, setInitData] = React.useState(null); // { otpauthUrl, qrCodeDataUrl, secretBase32 }
  const [code, setCode] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [backupCodes, setBackupCodes] = React.useState(null);

  React.useEffect(() => {
    if (step !== 'init' || !user?.id) return;
    (async () => {
      setSubmitting(true); setError(null);
      try {
        const r = await fetch('/api/auth/mfa/setup-init', {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user.id },
          body: JSON.stringify(confirmReset ? { confirmReset: true } : {}),
        });
        const j = await r.json();
        if (!r.ok) {
          setError(j.message || j.error || `HTTP ${r.status}`);
          return;
        }
        setInitData(j);
        setStep('scan');
      } catch (e) {
        setError(String(e?.message || e));
      } finally {
        setSubmitting(false);
      }
    })();
    // eslint-disable-next-line
  }, [user?.id, confirmReset]);

  async function handleVerify() {
    if (!/^\d{6}$/.test(code) || submitting) return;
    setSubmitting(true); setError(null);
    try {
      const r = await fetch('/api/auth/mfa/setup-verify', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user.id },
        body: JSON.stringify({ code }),
      });
      const j = await r.json();
      if (!r.ok) {
        setError(j.error === 'invalid_code' ? 'Codice errato. Verifica orologio device + riprova.' : (j.error || `HTTP ${r.status}`));
        return;
      }
      setBackupCodes(j.backupCodes || []);
      setStep('done');
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSubmitting(false);
    }
  }

  function copyBackupCodes() {
    if (!backupCodes) return;
    navigator.clipboard?.writeText(backupCodes.join('\n')).catch(()=>{});
  }

  return (
    <Modal open onClose={step === 'done' ? null : onClose} title="Configura MFA (TOTP)" size="lg" footer={
      step === 'init' ? (
        <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
      ) : step === 'scan' ? (
        <>
          <Btn variant="ghost" size="sm" onClick={onClose} disabled={submitting}>Annulla</Btn>
          <Btn variant="primary" size="sm" onClick={handleVerify} disabled={!/^\d{6}$/.test(code) || submitting} data-testid="mfa-setup-verify-btn">
            {submitting ? 'Verifica…' : 'Verifica e attiva'}
          </Btn>
        </>
      ) : (
        <Btn variant="primary" size="sm" onClick={onDone} data-testid="mfa-setup-done-btn">Ho salvato i codici</Btn>
      )
    }>
      {error && (
        <div style={{ padding: '10px 12px', border: '1px solid var(--err)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err)', fontSize: 12, marginBottom: 10 }}>
          <strong>Errore:</strong> {error}
        </div>
      )}
      {step === 'init' && (
        <div style={{ padding: 12, fontSize: 12, color: 'var(--text-3)' }}>
          Generazione QR code in corso…
        </div>
      )}
      {step === 'scan' && initData && (
        <div className="col" style={{ gap: 12 }}>
          <div style={{ fontSize: 12.5, lineHeight: 1.55 }}>
            <strong>Step 1:</strong> apri l'app authenticator (Google Authenticator, Microsoft Authenticator, Authy, 1Password…) e scansiona il QR code.
          </div>
          <div style={{ display: 'flex', justifyContent: 'center', padding: 10, background: 'white', borderRadius: 8 }}>
            <img src={initData.qrCodeDataUrl} alt="QR code MFA" width={240} height={240} data-testid="mfa-qr"/>
          </div>
          <div style={{ fontSize: 11, color: 'var(--text-3)', textAlign: 'center' }}>
            Non riesci a scansionare? Inserisci manualmente questo secret:
          </div>
          <code style={{ fontFamily: 'var(--font-mono)', fontSize: 12, background: 'var(--bg-2)', padding: '6px 10px', borderRadius: 4, textAlign: 'center', wordBreak: 'break-all' }} data-testid="mfa-secret-manual">
            {initData.secretBase32}
          </code>
          <div className="sep"/>
          <div style={{ fontSize: 12.5 }}>
            <strong>Step 2:</strong> inserisci il codice 6 cifre mostrato dall'app:
          </div>
          <input
            type="text"
            inputMode="numeric"
            pattern="\d{6}"
            maxLength={6}
            autoComplete="one-time-code"
            value={code}
            onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
            placeholder="123456"
            style={{ fontFamily: 'var(--font-mono)', fontSize: 18, textAlign: 'center', letterSpacing: 4 }}
            data-testid="mfa-setup-code-input"
          />
        </div>
      )}
      {step === 'done' && backupCodes && (
        <div className="col" style={{ gap: 12 }}>
          <div style={{ padding: 10, background: 'rgba(255,193,7,0.1)', border: '1px solid rgba(255,193,7,0.4)', borderRadius: 6, fontSize: 12.5, color: 'var(--text-1)' }}>
            <strong>⚠ Salva ORA questi codici di backup.</strong> Verranno mostrati UNA SOLA VOLTA. Servono per accedere se perdi l'authenticator. Ogni codice è single-use.
          </div>
          <div className="grid grid-2" style={{ gap: 6 }} data-testid="mfa-backup-codes">
            {backupCodes.map((c, i) => (
              <code key={i} style={{ fontFamily: 'var(--font-mono)', fontSize: 13, background: 'var(--bg-2)', padding: '8px 12px', borderRadius: 4, textAlign: 'center', letterSpacing: 1 }}>
                {c}
              </code>
            ))}
          </div>
          <Btn variant="ghost" size="sm" onClick={copyBackupCodes}>
            <Icon name="copy" size={11}/> Copia tutti
          </Btn>
        </div>
      )}
    </Modal>
  );
}

// FASE 22.X (sessione 75 A1) — Disable MFA con re-conferma password.
function MfaDisableModal({ user, onClose, onDone }) {
  const [password, setPassword] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);

  async function handleDisable() {
    if (!password || submitting) return;
    setSubmitting(true); setError(null);
    try {
      const r = await fetch('/api/auth/mfa/disable', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user.id },
        body: JSON.stringify({ password }),
      });
      const j = await r.json();
      if (!r.ok) {
        setError(j.error === 'invalid_password' ? 'Password errata' : (j.error || `HTTP ${r.status}`));
        return;
      }
      onDone();
    } catch (e) {
      setError(String(e?.message || e));
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <Modal open onClose={onClose} title="Disabilita MFA" size="md" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={submitting}>Annulla</Btn>
        <Btn variant="primary" size="sm" onClick={handleDisable} disabled={!password || submitting} data-testid="mfa-disable-confirm-btn">
          {submitting ? 'Disabilitazione…' : 'Disabilita MFA'}
        </Btn>
      </>
    }>
      <div className="col" style={{ gap: 12 }}>
        {error && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err)', fontSize: 12 }}>
            <strong>Errore:</strong> {error}
          </div>
        )}
        <div style={{ fontSize: 12.5, lineHeight: 1.55 }}>
          Inserisci la tua password per confermare la disabilitazione di MFA. Dopo la conferma:
          <ul style={{ marginTop: 6, paddingLeft: 20 }}>
            <li>il secret TOTP corrente viene eliminato;</li>
            <li>i codici di backup vengono invalidati;</li>
            <li>il prossimo login richiede solo la password.</li>
          </ul>
        </div>
        <input
          type="password"
          autoComplete="current-password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password corrente"
          data-testid="mfa-disable-password-input"
        />
      </div>
    </Modal>
  );
}

Object.assign(window, { Settings });
