// ============================================================
// Communications.jsx — FASE 15 (sessione 72): full
// ============================================================
// View modes: 'flat' (lista messaggi singoli) | 'threads' (raggruppato per thread)
// Filtri: kind + signal + priority + vendor + tag (multi-select)
// Detail panel:
//   - mode flat → singola comm con AI signal explanation
//   - mode threads → thread con messaggi cronologici + COMMS_SUMMARIZER button
// Sessione 89 — SSoT signal labels/chips condivise (coerente con ProjectDetail.CommTab)
const COMMS_SIGNAL_VALUES_GLOBAL = ['delay', 'risk', 'action', 'decision', 'offer', 'ok'];
const COMMS_SIGNAL_LABEL_GLOBAL = { delay: 'ritardo', risk: 'rischio', action: 'azione richiesta', decision: 'decisione', offer: 'offerta', ok: 'informativa' };
const COMMS_SIGNAL_CHIP_KIND_GLOBAL = { delay: 'err', risk: 'warn', action: 'info', decision: 'info', offer: 'info', ok: 'ok' };

function Communications() {
  const { seed, extras, addCommunication, pushToast, user, seedCustom } = useStore();
  // FASE 2 RBAC (sessione 102) — gating "+ Nuova comunicazione".
  const canCreateComm = window.can('communication.create', user, seedCustom);
  const [viewMode, setViewMode] = React.useState('flat'); // 'flat' | 'threads'
  const [kind, setKind] = React.useState('');
  const [signal, setSignal] = React.useState('');
  const [priority, setPriority] = React.useState('');
  const [vendorFilter, setVendorFilter] = React.useState('');
  const [tagFilter, setTagFilter] = React.useState('');
  const [searchQ, setSearchQ] = React.useState('');
  const [sel, setSel] = React.useState(null);
  const [selThread, setSelThread] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);

  const [threads, setThreads] = React.useState(null);
  const [threadsLoading, setThreadsLoading] = React.useState(false);

  // Sessione 89 — Live fetch + paginazione cursor + filtro signal_source + counts
  const [liveComms, setLiveComms] = React.useState(null); // null=loading, [...]=loaded
  const [liveLoading, setLiveLoading] = React.useState(false);
  const [liveHasMore, setLiveHasMore] = React.useState(false);
  const [liveCursor, setLiveCursor] = React.useState(null);
  const [counts, setCounts] = React.useState(null);
  // Sessione 90 — default 'not_confirmed' (mail da gestire = user OR ai_suggested).
  // L'utente apre /communications e vede subito il triage queue, non lo storico.
  const [signalSourceFilter, setSignalSourceFilter] = React.useState('not_confirmed'); // ''|'user'|'ai_suggested'|'user_confirmed'|'unlinked'|'not_confirmed'
  const [assigningId, setAssigningId] = React.useState(null); // commId in pending PATCH
  const [signalActionId, setSignalActionId] = React.useState(null); // commId in pending suggest/confirm
  const [signalPicker, setSignalPicker] = React.useState(null); // {comm} per modal cambio
  const [assignProjectModal, setAssignProjectModal] = React.useState(null); // {comm} per modal assegna

  // Sessione 89.C — Server-side batch load grosso (200/call) + paginazione
  // client-side 5/pagina via usePaginated. Migliore UX rispetto a infinite scroll
  // per liste medie (50-500 mail tenant).
  const PAGE_LIMIT = 200;

  const loadLive = React.useCallback(async (opts = {}) => {
    const { append = false } = opts;
    setLiveLoading(true);
    try {
      const params = new URLSearchParams();
      params.set('limit', String(PAGE_LIMIT));
      params.set('includeCounts', 'true');
      if (signalSourceFilter === 'unlinked') {
        params.set('unlinkedOnly', 'true');
      } else if (signalSourceFilter) {
        params.set('signalSource', signalSourceFilter);
      }
      if (append && liveCursor) params.set('cursor', liveCursor);
      const r = await fetch(`/api/communications?${params.toString()}`, {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) {
        if (!append) { setLiveComms([]); setLiveHasMore(false); setLiveCursor(null); }
        return;
      }
      const j = await r.json();
      const data = Array.isArray(j.data) ? j.data : [];
      setLiveComms((prev) => (append && Array.isArray(prev)) ? [...prev, ...data] : data);
      setLiveHasMore(!!j.hasMore);
      setLiveCursor(j.nextCursor || null);
      if (j.counts) setCounts(j.counts);
    } catch {
      if (!append) { setLiveComms([]); setLiveHasMore(false); setLiveCursor(null); }
    } finally {
      setLiveLoading(false);
    }
  }, [user?.id, signalSourceFilter, liveCursor]);

  // Reload quando cambia il filtro signal_source
  React.useEffect(() => {
    setLiveCursor(null);
    setLiveComms(null);
    // ESLint disable: vogliamo trigger sempre su cambio filtro
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [signalSourceFilter]);

  React.useEffect(() => {
    if (liveComms === null && !liveLoading) {
      loadLive({ append: false });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [liveComms]);

  // Sessione 89 — Handler AI signal (suggest + confirm + change)
  async function handleSuggestSignal(commId) {
    if (signalActionId) return;
    setSignalActionId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/suggest-signal`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: '{}',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'AI scoring fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: `AI: ${COMMS_SIGNAL_LABEL_GLOBAL[j.classification?.signal] || j.classification?.signal}`, desc: `${j.classification?.confidence}% · ${j.classification?.provider}/${j.classification?.model}`, tone: 'info' });
      await loadLive({ append: false });
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setSignalActionId(null);
    }
  }

  async function handleConfirmSignal(commId, newSignal /* undefined=accept */) {
    if (signalActionId) return;
    setSignalActionId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/confirm-signal`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify(newSignal ? { signal: newSignal } : {}),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'Conferma fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      // Sessione 90 — mostra workflow actions triggered (anomaly_flag, audit_log, ecc)
      const actions = j?.workflow?.actions || [];
      const summary = actions.length === 0
        ? 'Evasa (nessuna azione downstream)'
        : actions.map((a) => {
            if (a.type === 'anomaly_flag') return `🚩 anomaly_flag ${a.severity}`;
            if (a.type === 'audit_log') return `📝 ${a.reason}`;
            return a.type;
          }).join(' · ');
      pushToast({
        title: newSignal ? `Evasa → ${COMMS_SIGNAL_LABEL_GLOBAL[newSignal]}` : 'Evasa (AI accettato)',
        desc: summary,
        tone: 'ok',
      });
      setSignalPicker(null);
      await loadLive({ append: false });
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setSignalActionId(null);
    }
  }

  async function handleAssignProject(commId, projectId) {
    if (assigningId) return;
    setAssigningId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify({ projectId }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'Assegnazione fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: 'Project assegnato', desc: `${commId} → ${projectId || 'orphan'}`, tone: 'ok' });
      setAssignProjectModal(null);
      await loadLive({ append: false });
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setAssigningId(null);
    }
  }

  // Sessione 86 — "Invia reale" single send via provider email attivo (Resend/SMTP).
  const [realSendTarget, setRealSendTarget] = React.useState(() => {
    try { return localStorage.getItem('lgs.comms.realSendTarget') || 'antetempo@gmail.com'; }
    catch { return 'antetempo@gmail.com'; }
  });
  const [realSendBusy, setRealSendBusy] = React.useState({});
  const updateRealSendTarget = (v) => {
    setRealSendTarget(v);
    try { localStorage.setItem('lgs.comms.realSendTarget', v); } catch {}
  };
  const sendCommReal = async (commId) => {
    if (!realSendTarget || !commId) return;
    setRealSendBusy((b) => ({ ...b, [commId]: true }));
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/send-real`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ overrideTo: realSendTarget }),
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'Invio fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: '✓ Mail inviata davvero', desc: `→ ${realSendTarget} · ${j.data.latencyMs}ms · msgId ${j.data.messageId?.slice(0, 12)}…`, tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'send fallito', tone: 'err' });
    } finally {
      setRealSendBusy((b) => { const n = { ...b }; delete n[commId]; return n; });
    }
  };

  const reloadThreads = React.useCallback(async () => {
    setThreadsLoading(true);
    try {
      const params = new URLSearchParams();
      if (vendorFilter) params.set('vendorId', vendorFilter);
      if (signal) params.set('lastSignal', signal);
      if (searchQ) params.set('q', searchQ);
      params.set('limit', '200');
      const r = await fetch('/api/communication-threads?' + params, {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json();
      if (r.ok) setThreads(j.data || []);
    } catch (e) {
      console.warn('[comms] threads load failed', e);
    } finally {
      setThreadsLoading(false);
    }
  }, [vendorFilter, signal, searchQ, user?.id]);

  React.useEffect(() => {
    if (viewMode === 'threads') reloadThreads();
  }, [viewMode, reloadThreads]);

  async function generateActionFollowup(originalComm) {
    if (!originalComm) return;
    const followup = {
      kind: 'note',
      projectId: originalComm.project || originalComm.projectId || null,
      from: user?.name || 'AI Copilot',
      to: 'Project Manager',
      subject: `Follow-up: ${originalComm.subject}`,
      excerpt: `Action richiesta da AI in risposta a "${originalComm.subject}". Segnale rilevato: ${originalComm.signal}.`,
      signal: 'action',
      signalScore: Math.min(1, (originalComm.signalScore || 0) + 0.1),
    };
    try {
      const res = await fetch('/api/communications', {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify(followup),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        pushToast({ title: 'Errore', desc: json?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      if (json?.data) {
        addCommunication(json.data);
        pushToast({ title: 'Follow-up creato', desc: json.data.subject, tone: 'ok' });
      }
    } catch (e) {
      pushToast({ title: 'Errore', desc: String(e?.message || e), tone: 'err' });
    }
  }

  // Sessione 89 — fonte dati: liveComms se caricata, altrimenti fallback seed+extras
  // mentre il fetch è in corso (NO flash di empty state).
  const allComms = React.useMemo(() => {
    if (Array.isArray(liveComms)) return liveComms;
    const seen = new Set();
    const out = [];
    for (const c of [...(extras?.communicationsExt || []), ...(seed.COMMUNICATIONS || [])]) {
      if (!c?.id || seen.has(c.id)) continue;
      seen.add(c.id); out.push(c);
    }
    return out;
  }, [liveComms, seed.COMMUNICATIONS, extras?.communicationsExt]);
  const isLive = liveComms !== null;

  const allTags = React.useMemo(() => {
    const set = new Set();
    for (const c of allComms) (c.tags || []).forEach((t) => set.add(t));
    return Array.from(set).sort();
  }, [allComms]);

  const vendors = (seed.VENDORS || []).slice().sort((a, b) => a.name.localeCompare(b.name));

  const filtered = allComms.filter((c) =>
    (!kind || c.kind === kind) &&
    (!signal || c.signal === signal) &&
    (!priority || (c.priority || 'normal') === priority) &&
    (!vendorFilter || c.vendorId === vendorFilter) &&
    (!tagFilter || (c.tags || []).includes(tagFilter)) &&
    (!searchQ || c.subject.toLowerCase().includes(searchQ.toLowerCase()) || (c.excerpt || '').toLowerCase().includes(searchQ.toLowerCase())),
  );

  // Sessione 89.C — Paginazione client-side 5 per pagina (configurabile via select)
  const pg = usePaginated(filtered, 5);
  // Reset page 1 quando cambiano i filtri
  React.useEffect(() => { pg.setPage(1); }, [signalSourceFilter, kind, signal, priority, vendorFilter, tagFilter, searchQ]);

  // KPI
  const totalMsgs = allComms.length;
  const delaySignals = allComms.filter(c => c.signal === 'delay').length;
  const actionItems = allComms.filter(c => c.signal === 'action').length;
  const last30Days = allComms.filter(c => {
    if (!c.date) return false;
    const dt = new Date(c.date).getTime();
    return Number.isFinite(dt) && dt >= Date.now() - 30 * 24 * 3600 * 1000;
  }).length;

  return (
    <div className="page fade-in">
      <div className="page-header">
        <div>
          <div className="eyebrow">Knowledge · early warning</div>
          <h1 className="page-title">Monitoraggio comunicazioni</h1>
          <div className="page-sub">Mail, meeting e minute classificati automaticamente dall'agente per intercettare segnali deboli. {totalMsgs} message{totalMsgs === 1 ? 'o' : 'i'} indicizzat{totalMsgs === 1 ? 'o' : 'i'} totali.</div>
        </div>
        <div className="actions">
          <Btn
            variant={canCreateComm ? 'primary' : 'ghost'}
            size="sm"
            disabled={!canCreateComm}
            onClick={() => { if (canCreateComm) setShowNew(true); }}
            title={canCreateComm ? undefined : window.whyDisabled('communication.create')}
          ><Icon name="plus" size={12}/> Nuova comunicazione</Btn>
        </div>
      </div>

      <div className="grid grid-4" style={{ marginBottom: 14 }}>
        <div className="card"><Stat label="Messaggi indicizzati (30gg)" value={last30Days} delta={`${totalMsgs} totali`} tone="" /></div>
        <div className="card"><Stat label="Segnali ritardo" value={delaySignals} delta={delaySignals > 0 ? 'alta confidenza' : 'nessuno'} tone={delaySignals > 0 ? 'down' : 'up'} /></div>
        <div className="card"><Stat label="Action item aperti" value={actionItems} delta={actionItems === 1 ? '1 da gestire' : `${actionItems} da gestire`} tone={actionItems > 0 ? 'down' : 'up'} /></div>
        <div className="card"><Stat label="Indicizzazione AI" value="auto" delta="signal scoring abilitato" tone="up" /></div>
      </div>

      {showNew && <NewCommModal onClose={() => setShowNew(false)} />}

      {/* View mode toggle */}
      <div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--line)', marginBottom: 12 }}>
        {[
          { id: 'flat', label: 'Lista messaggi', count: allComms.length },
          { id: 'threads', label: 'Thread', count: threads?.length ?? 0 },
        ].map((t) => (
          <button
            key={t.id}
            data-view-mode={t.id}
            onClick={() => { setViewMode(t.id); setSel(null); setSelThread(null); }}
            style={{
              padding: '8px 14px', fontSize: 12, fontWeight: 500,
              color: viewMode === t.id ? 'var(--text-0)' : 'var(--text-2)',
              borderBottom: '2px solid ' + (viewMode === t.id ? 'var(--accent)' : 'transparent'),
              marginBottom: -1, background: 'transparent', cursor: 'pointer',
            }}
          >
            {t.label} <span className="mono" style={{ color: 'var(--text-3)', marginLeft: 4 }}>({t.count})</span>
          </button>
        ))}
      </div>

      {/* Sessione 89 — Filtro signal_source + PAGINAZIONE integrata + STICKY top */}
      {isLive && counts && (() => {
        const filteredTotal = signalSourceFilter === ''
          ? counts.total
          : signalSourceFilter === 'unlinked'
            ? (counts.byLink?.unlinked ?? 0)
            : (counts.bySource?.[signalSourceFilter] ?? 0);
        return (
          <div className="card" style={{ marginBottom: 12, position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-1)', boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }} data-pagination-banner="top">
            <div className="card-body tight" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {/* Riga 1: 5 chip filter signal_source */}
              <div className="row" style={{ gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
                <span className="eyebrow" style={{ marginRight: 4 }}>Stato</span>
                {[
                  { id: 'not_confirmed', label: 'Da gestire', count: (counts.bySource?.user ?? 0) + (counts.bySource?.ai_suggested ?? 0), tone: 'info' },
                  { id: 'user', label: 'Da classificare', count: counts.bySource?.user ?? 0, tone: 'info' },
                  { id: 'ai_suggested', label: 'AI in attesa', count: counts.bySource?.ai_suggested ?? 0, tone: 'warn' },
                  { id: 'user_confirmed', label: 'Evase', count: counts.bySource?.user_confirmed ?? 0, tone: 'ok' },
                  { id: 'unlinked', label: 'Senza progetto', count: counts.byLink?.unlinked ?? 0, tone: 'err' },
                  { id: '', label: 'Tutte', count: counts.total, tone: '' },
                ].map((f) => (
                  <button
                    key={f.id}
                    onClick={() => setSignalSourceFilter(f.id)}
                    className={`btn sm ${signalSourceFilter === f.id ? 'primary' : 'ghost'}`}
                    data-filter-source={f.id || 'all'}
                    style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}
                  >
                    <span>{f.label}</span>
                    <Chip kind={signalSourceFilter === f.id ? '' : f.tone}>{f.count}</Chip>
                  </button>
                ))}
                <div className="spacer" />
                {liveLoading && <span style={{ fontSize: 11, color: 'var(--text-3)' }}>Caricamento…</span>}
                <Btn variant="ghost" size="sm" onClick={() => loadLive({ append: false })} disabled={liveLoading} data-action="comms-refresh">
                  <Icon name="refresh" size={11}/> Aggiorna
                </Btn>
              </div>
              {/* Riga 2: Paginazione classica 5/pagina (« 1 · 2 · 3 ... N ») */}
              <div className="row" style={{ gap: 10, alignItems: 'center', flexWrap: 'wrap', borderTop: '1px solid var(--line)', paddingTop: 8 }}>
                <Icon name="docs" size={12}/>
                <span style={{ fontSize: 12, fontWeight: 500 }} data-testid="comm-pagination-counter">
                  {filtered.length === 0 ? (
                    'Nessuna comunicazione corrisponde ai filtri'
                  ) : (
                    <>
                      Pagina <strong>{pg.page}</strong> di <strong>{pg.pages}</strong>
                      {' · '}
                      Mostrate <strong>{Math.min(pg.page * pg.pageSize, filtered.length) - (pg.page - 1) * pg.pageSize}</strong> di <strong>{filtered.length}</strong>
                      {liveHasMore && <span style={{ color: 'var(--text-3)' }}> · {filteredTotal - filtered.length}+ altre sul server</span>}
                    </>
                  )}
                </span>
                <div className="spacer" />
                {/* Page navigator inline */}
                {pg.pages > 1 && (
                  <div className="row" style={{ gap: 2, alignItems: 'center' }} data-pagination-nav="true">
                    <Btn variant="ghost" size="xs" onClick={() => pg.setPage(1)} disabled={pg.page === 1} data-action="comms-page-first">«</Btn>
                    <Btn variant="ghost" size="xs" onClick={() => pg.setPage(Math.max(1, pg.page - 1))} disabled={pg.page === 1} data-action="comms-page-prev">‹</Btn>
                    {(() => {
                      const nums = [];
                      const w = 2;
                      const start = Math.max(1, pg.page - w);
                      const end = Math.min(pg.pages, pg.page + w);
                      if (start > 1) nums.push('...start');
                      for (let i = start; i <= end; i++) nums.push(i);
                      if (end < pg.pages) nums.push('...end');
                      return nums.map((n, idx) => {
                        if (typeof n === 'string') {
                          return <span key={n + idx} style={{ fontSize: 11, color: 'var(--text-3)', padding: '0 4px' }}>…</span>;
                        }
                        return (
                          <Btn
                            key={n}
                            variant={pg.page === n ? 'primary' : 'ghost'}
                            size="xs"
                            onClick={() => pg.setPage(n)}
                            data-action="comms-page-jump"
                            data-page={n}
                          >
                            {n}
                          </Btn>
                        );
                      });
                    })()}
                    <Btn variant="ghost" size="xs" onClick={() => pg.setPage(Math.min(pg.pages, pg.page + 1))} disabled={pg.page === pg.pages} data-action="comms-page-next">›</Btn>
                    <Btn variant="ghost" size="xs" onClick={() => pg.setPage(pg.pages)} disabled={pg.page === pg.pages} data-action="comms-page-last">»</Btn>
                  </div>
                )}
                <select
                  value={pg.pageSize}
                  onChange={(e) => pg.setPageSize(Number(e.target.value))}
                  data-testid="comms-page-size"
                  style={{ fontSize: 11, padding: '2px 6px', background: 'var(--bg-2)', border: '1px solid var(--line)', borderRadius: 4, color: 'var(--text-1)' }}
                >
                  {[5, 10, 25, 50].map((s) => (
                    <option key={s} value={s}>{s}/pag</option>
                  ))}
                </select>
                {liveHasMore && (
                  <Btn variant="ghost" size="xs" onClick={() => loadLive({ append: true })} disabled={liveLoading} data-action="comms-load-more-server" title={`Carica altri ${PAGE_LIMIT} dal server`}>
                    <Icon name="chev_d" size={10}/> +{PAGE_LIMIT} server
                  </Btn>
                )}
              </div>
            </div>
          </div>
        );
      })()}

      {/* Filtri */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-body tight" style={{ display: 'grid', gap: 10 }}>
          <div className="row" style={{ gap: 10, flexWrap: 'wrap' }}>
            <input
              type="search"
              placeholder="Cerca in oggetto/estratto..."
              value={searchQ}
              onChange={(e) => setSearchQ(e.target.value)}
              data-filter="search"
              style={{ flex: 1, minWidth: 240, fontSize: 12 }}
            />
            <Btn variant="ai" size="sm" onClick={() => { if (viewMode === 'threads') reloadThreads(); }} disabled={viewMode !== 'threads'}>
              <Icon name="refresh" size={12}/> Aggiorna thread
            </Btn>
          </div>
          <div className="row" style={{ gap: 8, flexWrap: 'wrap' }}>
            <div className="row" style={{ gap: 4 }}>
              <span className="eyebrow" style={{ alignSelf: 'center', marginRight: 4 }}>Tipo</span>
              {['', 'email', 'meeting', 'note', 'call'].map((k) => (
                <button key={k} onClick={() => setKind(k)} className={`btn sm ${kind === k ? 'primary' : 'ghost'}`} data-filter-kind={k || 'all'}>{k || 'Tutti'}</button>
              ))}
            </div>
            <div className="divider-v" />
            <div className="row" style={{ gap: 4 }}>
              <span className="eyebrow" style={{ alignSelf: 'center', marginRight: 4 }}>Segnale</span>
              {['', 'delay', 'risk', 'action', 'decision', 'offer', 'ok'].map((k) => (
                <button key={k} onClick={() => setSignal(k)} className={`btn sm ${signal === k ? 'primary' : 'ghost'}`} data-filter-signal={k || 'all'}>{k || 'Ogni'}</button>
              ))}
            </div>
          </div>
          <div className="row" style={{ gap: 8, flexWrap: 'wrap' }}>
            <div className="row" style={{ gap: 4 }}>
              <span className="eyebrow" style={{ alignSelf: 'center', marginRight: 4 }}>Priorità</span>
              {['', 'urgent', 'high', 'normal', 'low'].map((p) => (
                <button key={p} onClick={() => setPriority(p)} className={`btn sm ${priority === p ? 'primary' : 'ghost'}`} data-filter-priority={p || 'all'}>{p || 'Ogni'}</button>
              ))}
            </div>
            <div className="divider-v" />
            <div className="row" style={{ gap: 6, alignItems: 'center' }}>
              <span className="eyebrow">Vendor</span>
              <div style={{ minWidth: 180 }} data-filter="vendor">
                <window.Autocomplete value={vendorFilter} onChange={(v) => setVendorFilter(v)}
                  options={vendors.map((v) => ({ value: v.id, label: v.name }))}
                  placeholder="Ogni vendor · cerca…" testId="comms-vendor-ac" />
              </div>
            </div>
            {allTags.length > 0 && (
              <div className="row" style={{ gap: 6, alignItems: 'center' }}>
                <span className="eyebrow">Tag</span>
                <div style={{ minWidth: 160 }} data-filter="tag">
                  <window.Autocomplete value={tagFilter} onChange={(v) => setTagFilter(v)}
                    options={allTags.map((t) => ({ value: t, label: t }))}
                    placeholder="Ogni tag · cerca…" testId="comms-tag-ac" />
                </div>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Sessione 86 — Destinatario per "Invia reale" (persistito in localStorage) */}
      <div className="card" style={{ marginBottom: 12 }}>
        <div className="card-body" style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', fontSize: 12 }}>
          <Icon name="send" size={13}/>
          <span style={{ fontWeight: 500 }}>Replay mail su provider reale</span>
          <span style={{ color: 'var(--text-3)' }}>Destinatario test:</span>
          <input
            type="email"
            value={realSendTarget}
            onChange={(e) => updateRealSendTarget(e.target.value)}
            placeholder="antetempo@gmail.com"
            data-testid="comm-realsend-target-input"
            style={{ minWidth: 260, padding: '4px 8px', fontSize: 12 }}
          />
          <span style={{ color: 'var(--text-3)', fontSize: 11 }}>· ogni mail può essere inviata davvero col bottone <code>Invia reale</code> sulla riga</span>
        </div>
      </div>

      {viewMode === 'flat' ? (
        <>
          <FlatView
            filtered={pg.slice}
            sel={sel}
            setSel={setSel}
            generateActionFollowup={generateActionFollowup}
            realSendTarget={realSendTarget}
            realSendBusy={realSendBusy}
            sendCommReal={sendCommReal}
            isLive={isLive}
            signalActionId={signalActionId}
            assigningId={assigningId}
            handleSuggestSignal={handleSuggestSignal}
            handleConfirmSignal={handleConfirmSignal}
            setSignalPicker={setSignalPicker}
            setAssignProjectModal={setAssignProjectModal}
          />

          {/* Footer pagination compatto (replica numerica per chi è in fondo alla lista) */}
          {isLive && pg.pages > 1 && (
            <div className="card" style={{ marginTop: 10, background: 'var(--bg-2)' }} data-pagination-banner="bottom">
              <div className="card-body tight row" style={{ alignItems: 'center', gap: 6, justifyContent: 'center', flexWrap: 'wrap' }}>
                <Btn variant="ghost" size="xs" onClick={() => { pg.setPage(Math.max(1, pg.page - 1)); window.scrollTo({ top: 0, behavior: 'smooth' }); }} disabled={pg.page === 1}>‹ Precedente</Btn>
                <span style={{ fontSize: 11.5, color: 'var(--text-2)', padding: '0 8px' }}>
                  Pagina {pg.page} / {pg.pages} · {filtered.length} totali
                </span>
                <Btn variant="ghost" size="xs" onClick={() => { pg.setPage(Math.min(pg.pages, pg.page + 1)); window.scrollTo({ top: 0, behavior: 'smooth' }); }} disabled={pg.page === pg.pages}>Successiva ›</Btn>
              </div>
            </div>
          )}
        </>
      ) : (
        <ThreadsView
          threads={threads}
          loading={threadsLoading}
          selThread={selThread}
          setSelThread={setSelThread}
          user={user}
          pushToast={pushToast}
          searchQ={searchQ}
        />
      )}

      {/* Sessione 89 — Modal cambio signal */}
      {signalPicker && typeof window !== 'undefined' && window.Modal && (
        <window.Modal
          open
          onClose={() => setSignalPicker(null)}
          title={`Cambia signal — ${signalPicker.comm.id}`}
          size="sm"
          footer={<Btn variant="ghost" size="sm" onClick={() => setSignalPicker(null)}>Annulla</Btn>}
        >
          <div className="col" style={{ gap: 10 }}>
            <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
              AI ha suggerito <strong>{COMMS_SIGNAL_LABEL_GLOBAL[signalPicker.comm.signal]}</strong>. Seleziona il signal corretto:
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {COMMS_SIGNAL_VALUES_GLOBAL.map((s) => (
                <button
                  key={s}
                  className={`btn ${s === signalPicker.comm.signal ? 'primary' : 'ghost'} sm`}
                  onClick={() => handleConfirmSignal(signalPicker.comm.id, s)}
                  disabled={signalActionId === signalPicker.comm.id}
                  data-signal-option={s}
                  style={{ justifyContent: 'flex-start', textAlign: 'left' }}
                >
                  <Chip kind={COMMS_SIGNAL_CHIP_KIND_GLOBAL[s]} dot>{COMMS_SIGNAL_LABEL_GLOBAL[s]}</Chip>
                  <span style={{ fontSize: 10.5, color: 'var(--text-3)', marginLeft: 8 }}>{s}</span>
                </button>
              ))}
            </div>
          </div>
        </window.Modal>
      )}

      {/* Sessione 89 — Modal assegna progetto */}
      {assignProjectModal && typeof window !== 'undefined' && window.Modal && (
        <AssignProjectModal
          comm={assignProjectModal.comm}
          projects={seed.PROJECTS || []}
          busy={assigningId === assignProjectModal.comm.id}
          onAssign={(projectId) => handleAssignProject(assignProjectModal.comm.id, projectId)}
          onClose={() => setAssignProjectModal(null)}
        />
      )}
    </div>
  );
}

// Sessione 89 — Modal per assegnare manualmente una communication a un project
function AssignProjectModal({ comm, projects, busy, onAssign, onClose }) {
  const [search, setSearch] = React.useState('');
  const filtered = React.useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return projects.slice(0, 20);
    return projects.filter((p) =>
      (p.code || '').toLowerCase().includes(q) ||
      (p.name || '').toLowerCase().includes(q) ||
      (p.id || '').toLowerCase().includes(q),
    ).slice(0, 20);
  }, [search, projects]);

  return (
    <window.Modal
      open
      onClose={onClose}
      title={`Assegna progetto — ${comm.id}`}
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
          {comm.projectId && (
            <Btn variant="ghost" size="sm" onClick={() => onAssign(null)} disabled={busy} data-action="comm-unassign-project">
              <Icon name="x" size={11}/> Rimuovi assegnazione
            </Btn>
          )}
        </>
      }
    >
      <div className="col" style={{ gap: 10 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          Subject: <em>{comm.subject?.slice(0, 80)}{comm.subject?.length > 80 ? '…' : ''}</em>
        </div>
        <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>
          Project attuale: {comm.projectId ? <code className="mono">{comm.projectId}</code> : <em>nessuno (orphan)</em>}
        </div>
        <input
          type="search"
          autoFocus
          placeholder="Cerca progetto per code, nome o id…"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          data-testid="comm-assign-search"
          style={{ fontSize: 12, padding: '6px 10px' }}
        />
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 280, overflowY: 'auto' }}>
          {filtered.length === 0 ? (
            <div style={{ fontSize: 11.5, color: 'var(--text-3)', padding: '8px 10px' }}>
              Nessun progetto trovato per <code>{search}</code>
            </div>
          ) : filtered.map((p) => (
            <button
              key={p.id}
              onClick={() => onAssign(p.id)}
              disabled={busy}
              className={`btn ghost sm ${p.id === comm.projectId ? 'primary' : ''}`}
              data-project-option={p.id}
              style={{ display: 'flex', justifyContent: 'flex-start', textAlign: 'left', padding: '8px 10px', gap: 8 }}
            >
              <div style={{ flex: 1 }}>
                <div className="row" style={{ gap: 6, alignItems: 'center' }}>
                  <code className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{p.code || p.id}</code>
                  {p.id === comm.projectId && <Chip kind="ok" dot>attuale</Chip>}
                </div>
                <div style={{ fontSize: 12, marginTop: 2 }}>{p.name}</div>
              </div>
            </button>
          ))}
        </div>
      </div>
    </window.Modal>
  );
}

// FlatView — Sessione 89: aggiunti chip AI signal_source + bottoni inline
// (Accetta/Cambia/Suggerisci-AI/Assegna-progetto) + flag orphan
function FlatView({ filtered, sel, setSel, generateActionFollowup, realSendTarget, realSendBusy, sendCommReal, isLive, signalActionId, assigningId, handleSuggestSignal, handleConfirmSignal, setSignalPicker, setAssignProjectModal }) {
  const priorityKind = (p) => p === 'urgent' ? 'err' : p === 'high' ? 'warn' : p === 'low' ? '' : 'info';
  return (
    <div className="grid" style={{ gridTemplateColumns: sel ? '1fr 400px' : '1fr', gap: 14 }}>
      <div className="card">
        {filtered.length === 0 ? (
          <div className="card-body" style={{ color: 'var(--text-3)', fontSize: 12.5 }}>
            Nessuna comunicazione corrisponde ai filtri attivi.
          </div>
        ) : filtered.map((c) => {
          const sigSource = c.signalSource || 'user';
          const isAiSuggested = sigSource === 'ai_suggested';
          const isUserConfirmed = sigSource === 'user_confirmed';
          const isOrphan = !c.projectId && !c.project;
          return (
          <div key={c.id} onClick={() => setSel(c)} data-comm-id={c.id} data-signal-source={sigSource} data-orphan={isOrphan ? 'true' : 'false'} style={{ padding: '14px 16px', borderBottom: '1px solid var(--line)', cursor: 'pointer', background: sel?.id === c.id ? 'var(--bg-2)' : 'transparent' }}>
            <div className="row" style={{ gap: 8 }}>
              <div style={{ width: 32, height: 32, borderRadius: 6, background: 'var(--bg-2)', border: '1px solid var(--line)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
                <Icon name={c.kind === 'email' ? 'mail' : c.kind === 'meeting' ? 'users' : 'docs'} size={14}/>
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="row" style={{ gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontWeight: 500, fontSize: 13 }}>{c.subject}</span>
                  {c.priority && c.priority !== 'normal' && <Chip kind={priorityKind(c.priority)} dot>{c.priority}</Chip>}
                  {/* Signal chip con prefix ✨ (ai_suggested) o ✓ (user_confirmed) */}
                  <Chip kind={COMMS_SIGNAL_CHIP_KIND_GLOBAL[c.signal] || 'info'} dot>
                    {isAiSuggested ? '✨ ' : ''}{isUserConfirmed ? '✓ ' : ''}{COMMS_SIGNAL_LABEL_GLOBAL[c.signal] || c.signal}
                    {(isAiSuggested || isUserConfirmed) && c.signalConfidence > 0 ? ` · ${c.signalConfidence}%` : ''}
                  </Chip>
                  {/* Stato classificazione */}
                  {isAiSuggested && <Chip kind="warn">AI in attesa</Chip>}
                  {isOrphan && <Chip kind="err">senza progetto</Chip>}
                  {(c.tags || []).slice(0, 3).map(t => (
                    <Chip key={t}>#{t}</Chip>
                  ))}
                </div>
                <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 2 }}>{c.from} → {c.to} · {typeof c.date === 'string' && c.date.length > 10 ? c.date.slice(0, 10) : c.date} · {c.projectId || c.project || '—'}</div>
                <div style={{ fontSize: 12, color: 'var(--text-2)', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.excerpt}</div>
                {/* Rationale AI (visibile in chiaro nella riga, troncato) */}
                {(isAiSuggested || isUserConfirmed) && c.signalRationale && (
                  <div style={{ fontSize: 10.5, color: 'var(--text-3)', fontStyle: 'italic', marginTop: 4 }} title={c.signalRationale}>
                    AI: {c.signalRationale.length > 120 ? c.signalRationale.slice(0, 120) + '…' : c.signalRationale}
                  </div>
                )}
                {/* Sessione 89 — Action bar inline (live only) */}
                {isLive && (
                  <div className="row" style={{ gap: 6, marginTop: 6, flexWrap: 'wrap' }} onClick={(e) => e.stopPropagation()}>
                    {isAiSuggested && (
                      <>
                        <Btn variant="ghost" size="xs" onClick={() => handleConfirmSignal(c.id)} disabled={signalActionId === c.id} data-action="comm-accept-signal" data-comm-id={c.id}>
                          <Icon name="check" size={10}/> {signalActionId === c.id ? '…' : 'Accetta'}
                        </Btn>
                        <Btn variant="ghost" size="xs" onClick={() => setSignalPicker({ comm: c })} disabled={signalActionId === c.id} data-action="comm-change-signal" data-comm-id={c.id}>
                          <Icon name="edit" size={10}/> Cambia
                        </Btn>
                      </>
                    )}
                    {sigSource === 'user' && !isUserConfirmed && (
                      <Btn variant="ghost" size="xs" onClick={() => handleSuggestSignal(c.id)} disabled={signalActionId === c.id} data-action="comm-suggest-signal" data-comm-id={c.id}>
                        <Icon name="sparkle" size={10}/> {signalActionId === c.id ? 'AI…' : 'Suggerisci AI'}
                      </Btn>
                    )}
                    <Btn variant="ghost" size="xs" onClick={() => setAssignProjectModal({ comm: c })} disabled={assigningId === c.id} data-action="comm-assign-project" data-comm-id={c.id}>
                      <Icon name="link" size={10}/> {isOrphan ? 'Assegna progetto' : 'Cambia progetto'}
                    </Btn>
                  </div>
                )}
              </div>
              <button
                type="button"
                onClick={(e) => { e.stopPropagation(); sendCommReal(c.id); }}
                disabled={!!realSendBusy[c.id] || !realSendTarget}
                title={`Invia questa mail davvero a ${realSendTarget}`}
                data-testid={`comm-send-real-${c.id}`}
                style={{ flexShrink: 0, alignSelf: 'center', padding: '6px 10px', fontSize: 11, borderRadius: 6, border: '1px solid var(--line)', background: realSendBusy[c.id] ? 'var(--bg-3)' : 'var(--bg-2)', color: 'var(--text-2)', cursor: realSendBusy[c.id] ? 'wait' : 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}
              >
                <Icon name="send" size={11}/>{realSendBusy[c.id] ? 'invio…' : 'Invia reale'}
              </button>
            </div>
          </div>
          );
        })}
      </div>

      {sel && (
        <div className="card">
          <div className="card-header">
            <div className="title">{sel.subject}</div>
            <div className="actions"><button className="btn ghost icon" onClick={() => setSel(null)}><Icon name="x"/></button></div>
          </div>
          <div className="card-body">
            <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{sel.from}<br/>→ {sel.to}<br/>{sel.date}</div>
            <div className="sep"/>
            <div style={{ fontSize: 12.5, lineHeight: 1.6 }}>{sel.excerpt}</div>
            <div className="sep"/>
            <div className="eyebrow">Analisi AI</div>
            <div style={{ marginTop: 4 }}>
              {sel.signal === 'delay' && <Chip kind="err" dot>ritardo rilevato · confidenza {(sel.signalScore*100).toFixed(0)}%</Chip>}
              {sel.signal === 'risk' && <Chip kind="warn" dot>rischio · confidenza {(sel.signalScore*100).toFixed(0)}%</Chip>}
            </div>
            <p style={{ fontSize: 12, color: 'var(--text-2)', marginTop: 6, lineHeight: 1.5 }}>{sel.signal === 'delay' ? 'Pattern semantico di ritardo. Suggerimento: notifica al PM con piano di mitigazione.' : sel.signal === 'risk' ? 'Pattern di rischio identificato. Suggerimento: assessment + escalation.' : sel.signal === 'action' ? 'Action item rilevato. Suggerimento: follow-up entro 3 giorni.' : 'Nessun pattern critico individuato.'}</p>
            <div className="row" style={{ marginTop: 10, gap: 6 }}>
              <Btn variant="ai" size="sm" onClick={() => generateActionFollowup(sel)}><Icon name="sparkle" size={12}/> Genera azione</Btn>
              <Btn variant="ghost" size="sm">Archivia</Btn>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ThreadsView — FASE 15 sessione 72: vista raggruppata per thread + summarize agent
function ThreadsView({ threads, loading, selThread, setSelThread, user, pushToast, searchQ }) {
  if (loading) return <div className="card"><div className="card-body">Caricamento thread…</div></div>;
  if (!threads || threads.length === 0) {
    return (
      <div className="card">
        <div className="card-body" style={{ color: 'var(--text-3)', fontSize: 12.5 }}>
          Nessun thread di comunicazione presente. I thread si creano automaticamente al POST di una nuova comunicazione (raggruppamento per subject normalizzato).
        </div>
      </div>
    );
  }
  const filtered = searchQ
    ? threads.filter((t) => t.subjectNormalized.includes(searchQ.toLowerCase()))
    : threads;

  return (
    <div className="grid" style={{ gridTemplateColumns: selThread ? '1fr 480px' : '1fr', gap: 14 }}>
      <div className="card">
        <div className="card-header">
          <div className="title">Thread ({filtered.length})</div>
          <div className="desc">Comunicazioni aggregate per subject normalizzato</div>
        </div>
        <table className="tbl dense">
          <thead>
            <tr>
              <th>Subject</th>
              <th style={{ width: 80 }}>Messaggi</th>
              <th style={{ width: 90 }}>Segnale</th>
              <th style={{ width: 110 }}>Ultimo</th>
              <th>Tag</th>
            </tr>
          </thead>
          <tbody>
            {filtered.map((t) => (
              <tr key={t.id} className="clickable" onClick={() => setSelThread(t)} data-thread-id={t.id} style={selThread?.id === t.id ? { background: 'color-mix(in oklch, var(--accent) 8%, var(--bg-1))' } : undefined}>
                <td style={{ fontSize: 12, fontWeight: 500 }}>{t.subjectFirst}</td>
                <td className="num" style={{ textAlign: 'right' }}>{t.messagesCount}</td>
                <td><Chip kind={t.lastSignal === 'delay' ? 'err' : t.lastSignal === 'risk' ? 'warn' : t.lastSignal === 'action' ? 'info' : t.lastSignal === 'ok' ? 'ok' : 'info'} dot>{t.lastSignal}</Chip></td>
                <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{new Date(t.lastDate).toLocaleDateString('it-IT', { day: '2-digit', month: 'short' })}</td>
                <td style={{ fontSize: 11 }}>{(t.tags || []).slice(0, 3).map(tg => <Chip key={tg}>#{tg}</Chip>)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {selThread && (
        <ThreadDetailPanel thread={selThread} onClose={() => setSelThread(null)} user={user} pushToast={pushToast} />
      )}
    </div>
  );
}

// Thread detail con messaggi cronologici + COMMS_SUMMARIZER button
function ThreadDetailPanel({ thread, onClose, user, pushToast }) {
  const [detail, setDetail] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [summary, setSummary] = React.useState(null);
  const [summarizing, setSummarizing] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const r = await fetch(`/api/communication-threads/${encodeURIComponent(thread.id)}`, {
          credentials: 'same-origin',
          cache: 'no-store',
          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        });
        const j = await r.json();
        if (!cancelled && r.ok) setDetail(j.data);
      } catch (e) {
        console.warn('[thread.detail] failed', e);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [thread.id, user?.id]);

  const summarize = async () => {
    if (summarizing || !user?.id) return;
    setSummarizing(true);
    setSummary(null);
    const prefs = (() => {
      try {
        return JSON.parse(localStorage.getItem(`lgs.ai.user.${user.id}.preferences`) || '{}');
      } catch { return {}; }
    })();
    const headers = { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user.id };
    if (prefs.mode && prefs.mode !== 'default') headers['X-AI-Mode-Override'] = prefs.mode;
    if (prefs.provider && prefs.provider !== 'default') headers['X-AI-Provider-Override'] = prefs.provider;
    try {
      const r = await fetch('/api/ai/comms-summarizer/run', {
        method: 'POST',
        headers,
        credentials: 'same-origin',
        body: JSON.stringify({ threadId: thread.id }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || j.detail || 'HTTP ' + r.status);
      setSummary(j);
      pushToast({ title: 'Sintesi completata', desc: `${j.provider} · ${j.summary?.actions?.length || 0} action`, tone: 'ok' });
    } catch (e) {
      pushToast({ title: 'COMMS_SUMMARIZER fallito', desc: String(e?.message || e).slice(0, 200), tone: 'err' });
    } finally {
      setSummarizing(false);
    }
  };

  const toneKind = (t) => t === 'critical' ? 'err' : t === 'tense' ? 'warn' : t === 'positive' ? 'ok' : 'info';
  const sevKind = (s) => s === 'critical' ? 'err' : s === 'high' ? 'warn' : s === 'medium' ? 'info' : 'ok';
  const priKind = (p) => p === 'high' ? 'err' : p === 'medium' ? 'warn' : 'info';

  return (
    <div className="card">
      <div className="card-header">
        <div>
          <div className="title">{thread.subjectFirst}</div>
          <div className="desc">{thread.messagesCount} messaggi · {(thread.participants || []).length} partecipanti</div>
        </div>
        <div className="actions">
          <Btn variant="ai" size="sm" onClick={summarize} disabled={summarizing} data-action="summarize-thread">
            {summarizing ? 'Sintesi…' : <><Icon name="sparkle" size={11}/> Riassumi</>}
          </Btn>
          <button className="btn ghost icon" onClick={onClose}><Icon name="x"/></button>
        </div>
      </div>
      <div className="card-body" style={{ maxHeight: '70vh', overflow: 'auto' }}>
        {summary && (
          <div style={{ marginBottom: 14, padding: 10, background: 'var(--bg-2)', borderRadius: 6, border: '1px solid var(--line)' }} data-summary="loaded">
            <div className="row" style={{ marginBottom: 6 }}>
              <Chip kind={toneKind(summary.summary.tone)} dot>tone {summary.summary.tone}</Chip>
              <span className="spacer"/>
              <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{summary.provider} · {summary.durationMs}ms</span>
            </div>
            <p style={{ fontSize: 12, lineHeight: 1.5, margin: '6px 0' }}>{summary.summary.outline}</p>
            {summary.summary.actions.length > 0 && (
              <div style={{ marginTop: 8 }}>
                <div className="eyebrow" style={{ marginBottom: 4 }}>Action items ({summary.summary.actions.length})</div>
                {summary.summary.actions.map((a, i) => (
                  <div key={i} style={{ fontSize: 11.5, padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                    <Chip kind={priKind(a.priority)} dot>{a.priority}</Chip> <strong>{a.description}</strong>
                    {a.owner && <span style={{ color: 'var(--text-2)' }}> · {a.owner}</span>}
                  </div>
                ))}
              </div>
            )}
            {summary.summary.decisions.length > 0 && (
              <div style={{ marginTop: 8 }}>
                <div className="eyebrow" style={{ marginBottom: 4 }}>Decisioni ({summary.summary.decisions.length})</div>
                {summary.summary.decisions.map((d, i) => (
                  <div key={i} style={{ fontSize: 11.5, padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>{d.description}</div>
                ))}
              </div>
            )}
            {summary.summary.risks.length > 0 && (
              <div style={{ marginTop: 8 }}>
                <div className="eyebrow" style={{ marginBottom: 4 }}>Rischi ({summary.summary.risks.length})</div>
                {summary.summary.risks.map((r, i) => (
                  <div key={i} style={{ fontSize: 11.5, padding: '4px 0', borderBottom: '1px dashed var(--line)' }}>
                    <Chip kind={sevKind(r.severity)} dot>{r.severity}</Chip> {r.description}
                  </div>
                ))}
              </div>
            )}
            {summary.summary.nextStepHint && (
              <div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--accent)' }}>
                <Icon name="sparkle" size={10}/> Next step: {summary.summary.nextStepHint}
              </div>
            )}
          </div>
        )}

        <div className="eyebrow" style={{ marginBottom: 6 }}>Cronologia messaggi</div>
        {loading ? (
          <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Caricamento…</div>
        ) : detail?.messages?.length ? (
          detail.messages.map((m) => {
            const msgAttachments = (detail.attachments || []).filter((a) => a.communicationId === m.id);
            return (
              <div key={m.id} style={{ padding: '8px 10px', marginBottom: 6, background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 6 }}>
                <div className="row" style={{ gap: 6, marginBottom: 4 }}>
                  <Icon name={m.kind === 'email' ? 'mail' : m.kind === 'meeting' ? 'users' : 'docs'} size={11}/>
                  <span style={{ fontSize: 11, fontWeight: 500 }}>{m.from}</span>
                  <span className="spacer"/>
                  <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{m.date && new Date(m.date).toLocaleString('it-IT', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}</span>
                </div>
                <div style={{ fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.4 }}>{m.excerpt || m.bodyText?.slice(0, 200)}</div>
                <div className="row" style={{ gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
                  {m.priority && m.priority !== 'normal' && <Chip kind={m.priority === 'urgent' ? 'err' : m.priority === 'high' ? 'warn' : ''} dot>{m.priority}</Chip>}
                  {m.signal && m.signal !== 'ok' && <Chip kind={m.signal === 'delay' ? 'err' : m.signal === 'risk' ? 'warn' : 'info'} dot>{m.signal}</Chip>}
                  <CommAttachmentSection
                    communicationId={m.id}
                    attachments={msgAttachments}
                    user={user}
                    pushToast={pushToast}
                    onChanged={() => {
                      // reload thread detail after attachment change
                      (async () => {
                        const r = await fetch(`/api/communication-threads/${encodeURIComponent(thread.id)}`, {
                          credentials: 'same-origin', cache: 'no-store',
                          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
                        });
                        const j = await r.json();
                        if (r.ok) setDetail(j.data);
                      })();
                    }}
                  />
                </div>
              </div>
            );
          })
        ) : (
          <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Nessun messaggio nel thread.</div>
        )}
      </div>
    </div>
  );
}

// MVP gap #1: NewCommModal — POST /api/communications con audit log + auto-link.
function NewCommModal({ onClose }) {
  const { addCommunication, pushToast, user, seed } = useStore();
  const projects = (seed.PROJECTS || []).slice().sort((a, b) => a.code.localeCompare(b.code));
  const [form, setForm] = React.useState({
    kind: 'email',
    projectId: '',
    from: user?.name ? `${user.name} <${user.id}@veridanto>` : '',
    to: '',
    subject: '',
    excerpt: '',
    bodyText: '',
    signal: 'ok',
    signalScore: 0,
    priority: 'normal',
    tags: '',
  });
  const [saving, setSaving] = React.useState(false);
  const [serverError, setServerError] = React.useState(null);
  const [autoLinked, setAutoLinked] = React.useState(null);
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const valid = form.from.trim() && form.to.trim() && form.subject.trim();

  async function handleSubmit() {
    if (!valid || saving) return;
    setSaving(true); setServerError(null); setAutoLinked(null);
    try {
      const tagsArr = form.tags.split(',').map((t) => t.trim()).filter(Boolean);
      const res = await fetch('/api/communications', {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({
          kind: form.kind,
          projectId: form.projectId || null,
          from: form.from.trim(),
          to: form.to.trim(),
          subject: form.subject.trim(),
          excerpt: form.excerpt.trim() || null,
          bodyText: form.bodyText.trim() || null,
          signal: form.signal,
          signalScore: Number(form.signalScore),
          priority: form.priority,
          tags: tagsArr,
        }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        setServerError(json?.error === 'validation_error'
          ? `Validazione fallita: ${(json.issues||[]).map(i => i.message).join(' · ')}`
          : (json?.error || `HTTP ${res.status}`));
        return;
      }
      if (json?.data) {
        addCommunication(json.data);
        if (json.autoLinked && Object.keys(json.autoLinked).length > 0) {
          setAutoLinked(json.autoLinked);
          pushToast({ title: 'Comunicazione salvata + auto-link', desc: `Linked: ${Object.entries(json.autoLinked).map(([k, v]) => `${k}=${v}`).join(', ')}`, tone: 'ok' });
        } else {
          pushToast({ title: json.data.subject, desc: 'Comunicazione salvata. Audit registrato.', tone: 'ok' });
        }
      }
      onClose();
    } catch (e) { setServerError(String(e?.message || e)); }
    finally { setSaving(false); }
  }

  return (
    <Modal open onClose={onClose} title="Nuova comunicazione" size="lg" footer={<>
      <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
      <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleSubmit}>
        {saving ? 'Salvataggio…' : 'Crea comunicazione'}
      </Btn>
    </>}>
      <div className="col" style={{gap:14}}>
        {serverError && (
          <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> {serverError}
          </div>
        )}
        {autoLinked && (
          <div style={{ padding:'8px 10px', border:'1px solid var(--accent)', borderRadius:6, background:'color-mix(in oklch, var(--accent) 6%, transparent)', fontSize:11.5 }}>
            <Icon name="sparkle" size={11}/> Auto-link applicato: {Object.entries(autoLinked).map(([k, v]) => <code key={k}>{k}={v}</code>)}
          </div>
        )}
        <div className="grid grid-3">
          <div className="field"><label>Tipo</label>
            <select value={form.kind} onChange={e => set('kind', e.target.value)}>
              <option value="email">email</option>
              <option value="meeting">meeting</option>
              <option value="note">note</option>
              <option value="call">call</option>
            </select>
          </div>
          <div className="field"><label>Priorità</label>
            <select value={form.priority} onChange={e => set('priority', e.target.value)}>
              <option value="low">low</option>
              <option value="normal">normal</option>
              <option value="high">high</option>
              <option value="urgent">urgent</option>
            </select>
          </div>
          <div className="field"><label>Progetto associato (opzionale)</label>
            <window.Autocomplete value={form.projectId} onChange={v => set('projectId', v)}
              options={projects.map(p => ({ value: p.id, label: (p.name || p.id), sublabel: p.code }))}
              placeholder="Auto-link da subject/body · cerca…" testId="comm-project-ac" />
          </div>
          <div className="field"><label>Da <span style={{color:'var(--err)'}}>*</span></label>
            <input value={form.from} onChange={e => set('from', e.target.value)} placeholder="Nome <email>"/>
          </div>
          <div className="field" style={{gridColumn:'span 2'}}><label>A <span style={{color:'var(--err)'}}>*</span></label>
            <input value={form.to} onChange={e => set('to', e.target.value)} placeholder="destinatari"/>
          </div>
          <div className="field" style={{gridColumn:'span 3'}}><label>Oggetto <span style={{color:'var(--err)'}}>*</span></label>
            <input value={form.subject} onChange={e => set('subject', e.target.value)}/>
          </div>
          <div className="field" style={{gridColumn:'span 3'}}><label>Estratto</label>
            <textarea rows={2} value={form.excerpt} onChange={e => set('excerpt', e.target.value)} placeholder="Sintesi"/>
          </div>
          <div className="field" style={{gridColumn:'span 3'}}><label>Body completo (opzionale, max 50K char)</label>
            <textarea rows={4} value={form.bodyText} onChange={e => set('bodyText', e.target.value)} placeholder="Body full per auto-link + COMMS_SUMMARIZER più accurato"/>
          </div>
          <div className="field"><label>Segnale AI</label>
            <select value={form.signal} onChange={e => set('signal', e.target.value)}>
              <option value="ok">ok</option>
              <option value="action">action</option>
              <option value="risk">risk</option>
              <option value="delay">delay</option>
              <option value="decision">decision</option>
              <option value="offer">offer</option>
            </select>
          </div>
          <div className="field"><label>Confidenza (0-1)</label>
            <input type="number" min={0} max={1} step={0.05} value={form.signalScore} onChange={e => set('signalScore', Number(e.target.value))}/>
          </div>
          <div className="field"><label>Tag (csv)</label>
            <input value={form.tags} onChange={e => set('tags', e.target.value)} placeholder="es. urgent, fornitore, sla"/>
          </div>
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// FASE 73 — CommAttachmentSection: lista attachments + upload + delete + download
// s129 — Intelligent inbox: classificazione AI dell'allegato vs doc attesi del
//        project + deposito 1-click → sblocco gate workflow + notifica responsabili.
// ============================================================
function CommAttachmentSection({ communicationId, attachments, user, pushToast, onChanged }) {
  const store = useStore();
  const seedCustom = store?.seedCustom;
  const [uploading, setUploading] = React.useState(false);
  // attId → { loading, result, error, chosenCode, depositing, deposited, notice }
  const [aiByAtt, setAiByAtt] = React.useState({});
  const fileInputRef = React.useRef(null);

  const enc = encodeURIComponent;
  const docTypeCatalog = React.useMemo(
    () => (seedCustom?.DOC_TYPES || []).filter((d) => d.active !== false),
    [seedCustom],
  );
  const docTypeNameByCode = React.useMemo(() => {
    const m = {};
    for (const d of docTypeCatalog) m[d.code] = d.name;
    return m;
  }, [docTypeCatalog]);
  const acOptions = React.useMemo(
    () => docTypeCatalog.map((d) => ({ value: d.code, label: `${d.name} (${d.code})`, sublabel: d.category, keywords: d.category })),
    [docTypeCatalog],
  );
  const patchAtt = (attId, p) => setAiByAtt((prev) => ({ ...prev, [attId]: { ...(prev[attId] || {}), ...p } }));

  const upload = async (file) => {
    if (!file || !user?.id || uploading) return;
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      const r = await fetch(`/api/communications/${enc(communicationId)}/attachments`, {
        method: 'POST',
        headers: { 'X-Actor-Persona-Id': user.id },
        credentials: 'same-origin',
        body: fd,
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || j.detail || 'HTTP ' + r.status);
      pushToast({ title: 'Allegato caricato', desc: `${file.name} (${(file.size / 1024).toFixed(1)} KB)`, tone: 'ok' });
      onChanged && onChanged();
    } catch (e) {
      pushToast({ title: 'Upload fallito', desc: String(e?.message || e).slice(0, 150), tone: 'err' });
    } finally {
      setUploading(false);
      if (fileInputRef.current) fileInputRef.current.value = '';
    }
  };

  const removeAttachment = async (attId) => {
    if (!user?.id) return;
    if (!window.confirm('Eliminare questo allegato?')) return;
    try {
      const r = await fetch(`/api/communications/${enc(communicationId)}/attachments/${enc(attId)}`, {
        method: 'DELETE',
        headers: { 'X-Actor-Persona-Id': user.id },
        credentials: 'same-origin',
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error || 'HTTP ' + r.status);
      }
      pushToast({ title: 'Allegato eliminato', tone: 'ok' });
      onChanged && onChanged();
    } catch (e) {
      pushToast({ title: 'Delete fallito', desc: String(e?.message || e).slice(0, 150), tone: 'err' });
    }
  };

  // s129 — classifica un allegato vs doc attesi del project (read-only, suggerisce).
  const classify = async (attId) => {
    if (!user?.id || aiByAtt[attId]?.loading) return;
    patchAtt(attId, { loading: true, error: null });
    try {
      const r = await fetch(`/api/communications/${enc(communicationId)}/attachments/${enc(attId)}/classify`, {
        method: 'POST',
        headers: { 'X-Actor-Persona-Id': user.id, 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: '{}',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.detail || j.error || 'HTTP ' + r.status);
      patchAtt(attId, { loading: false, result: j.data, chosenCode: j.data?.docTypeCode || '' });
    } catch (e) {
      patchAtt(attId, { loading: false, error: String(e?.message || e).slice(0, 180) });
    }
  };

  // s129 — deposita l'allegato come project_document (conferma 1-click).
  const deposit = async (attId) => {
    const st = aiByAtt[attId];
    if (!st || !user?.id || st.depositing) return;
    const code = st.chosenCode || st.result?.docTypeCode;
    if (!code) { pushToast({ title: 'Scegli una categoria', desc: 'Seleziona il tipo documento prima di depositare.', tone: 'warn' }); return; }
    patchAtt(attId, { depositing: true });
    try {
      const r = await fetch(`/api/communications/${enc(communicationId)}/attachments/${enc(attId)}/deposit`, {
        method: 'POST',
        headers: { 'X-Actor-Persona-Id': user.id, 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ docTypeCode: code }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.detail || j.error || 'HTTP ' + r.status);
      const gn = j.data?.gateNotify;
      const notified = (gn?.notified) || [];
      const names = [...new Set(notified.flatMap((n) => n.recipientNames || []))];
      const label = docTypeNameByCode[code] || code;
      let desc = `${label} depositato nel progetto.`;
      if (notified.length) {
        desc += gn?.gateSatisfied === false
          ? ` Responsabili avvisati (${names.join(', ')}); va firmato per sbloccare lo step.`
          : ` Step sbloccato — notificati: ${names.join(', ')}.`;
      }
      pushToast({ title: 'Documento depositato', desc, tone: 'ok' });
      patchAtt(attId, { depositing: false, deposited: true, notice: desc });
      onChanged && onChanged();
    } catch (e) {
      patchAtt(attId, { depositing: false });
      pushToast({ title: 'Deposito fallito', desc: String(e?.message || e).slice(0, 180), tone: 'err' });
    }
  };

  const renderBadge = (res) => {
    if (res.unlocksGate) return <Chip kind="ok" dot>Sblocca uno step in attesa</Chip>;
    if (res.needsSignatureToUnlock) return <Chip kind="info" dot>Atteso da uno step · da firmare per sbloccare</Chip>;
    if (res.expected && res.alreadyPresent) return <Chip kind="warn" dot>Atteso · già presente (nuova versione)</Chip>;
    if (res.expected) return <Chip kind="info" dot>Atteso dal progetto</Chip>;
    if (res.validCode) return <Chip kind="" dot>Tipo non atteso qui</Chip>;
    return <Chip kind="warn" dot>AI incerta — scegli la categoria</Chip>;
  };

  return (
    <span className="row" style={{ gap: 4, alignItems: 'center', marginLeft: 8, flexWrap: 'wrap' }} data-attachments-section={communicationId}>
      {attachments.map((a) => (
        <span
          key={a.id}
          className="mono"
          style={{ fontSize: 10.5, padding: '2px 6px', background: 'var(--bg-2)', borderRadius: 4, border: '1px solid var(--line)' }}
          title={`${a.mimeType || 'binary'} · ${a.fileSize ? (a.fileSize / 1024).toFixed(1) + ' KB' : '—'}`}
        >
          <a href={`/api/communications/${enc(communicationId)}/attachments/${enc(a.id)}`} target="_blank" rel="noopener" style={{ color: 'var(--accent)', textDecoration: 'none' }}>
            <Icon name="attach" size={10}/> {a.filename}
          </a>
          <button
            onClick={() => classify(a.id)}
            disabled={aiByAtt[a.id]?.loading}
            style={{ background: 'transparent', border: 'none', cursor: aiByAtt[a.id]?.loading ? 'wait' : 'pointer', color: 'var(--accent)', marginLeft: 6 }}
            title="Classifica con AI vs documenti attesi dal progetto"
            data-classify-attach={a.id}
          >
            <Icon name="sparkle" size={10}/>
          </button>
          <button onClick={() => removeAttachment(a.id)} style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: 'var(--text-3)', marginLeft: 4 }} title="Elimina">×</button>
        </span>
      ))}
      <label
        style={{ fontSize: 10.5, color: 'var(--text-3)', cursor: uploading ? 'wait' : 'pointer', padding: '2px 6px', borderRadius: 4 }}
        title="Carica file (max 50MB)"
        data-upload-attach={communicationId}
      >
        <Icon name="upload" size={10}/> {uploading ? 'Upload…' : '+'}
        <input
          ref={fileInputRef}
          type="file"
          style={{ display: 'none' }}
          disabled={uploading}
          onChange={(e) => upload(e.target.files?.[0])}
        />
      </label>

      {/* s129 — pannelli AI per allegato (full-width nella riga flex) */}
      {attachments.map((a) => {
        const st = aiByAtt[a.id];
        if (!st) return null;
        const res = st.result;
        return (
          <div
            key={'ai-' + a.id}
            style={{ flexBasis: '100%', marginTop: 4, padding: '8px 10px', background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 6, fontSize: 11.5 }}
            data-ai-panel={a.id}
          >
            <div className="row" style={{ gap: 6, marginBottom: 4 }}>
              <Icon name="sparkle" size={11}/>
              <span style={{ fontWeight: 600 }}>Classificazione AI</span>
              <span className="mono" style={{ color: 'var(--text-3)', fontSize: 10 }}>{a.filename}</span>
            </div>

            {st.loading && <div style={{ color: 'var(--text-3)' }}>Lettura del documento e confronto con i documenti attesi dal progetto…</div>}
            {st.error && <div style={{ color: 'var(--err, #c0392b)' }}>Errore: {st.error}</div>}

            {st.deposited && (
              <div style={{ color: 'var(--ok, #1a7f37)' }}><Icon name="check" size={11}/> {st.notice}</div>
            )}

            {res && !st.deposited && (
              <>
                <div className="row" style={{ gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
                  <span>
                    Tipo riconosciuto:{' '}
                    <strong>{res.docTypeCode ? (docTypeNameByCode[res.docTypeCode] || res.docTypeCode) : '—'}</strong>
                    {res.docTypeCode && <span className="mono" style={{ color: 'var(--text-3)', marginLeft: 4 }}>({res.docTypeCode})</span>}
                  </span>
                  <span style={{ color: res.confidence >= 0.7 ? 'var(--ok, #1a7f37)' : 'var(--text-3)' }}>
                    {Math.round((res.confidence || 0) * 100)}% confidenza
                  </span>
                  {renderBadge(res)}
                </div>
                {res.reasoning && <div style={{ color: 'var(--text-3)', marginBottom: 6 }}>{res.reasoning}</div>}
                <div className="row" style={{ gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
                  <span style={{ color: 'var(--text-3)' }}>Categoria:</span>
                  <div style={{ minWidth: 240 }}>
                    <window.Autocomplete
                      value={st.chosenCode || ''}
                      onChange={(v) => patchAtt(a.id, { chosenCode: v })}
                      options={acOptions}
                      placeholder="Cerca tipo documento…"
                    />
                  </div>
                  <button
                    className="btn sm primary"
                    onClick={() => deposit(a.id)}
                    disabled={st.depositing || !(st.chosenCode || res.docTypeCode)}
                    data-deposit-attach={a.id}
                  >
                    {st.depositing ? 'Deposito…' : 'Deposita nel progetto'}
                  </button>
                </div>
              </>
            )}
          </div>
        );
      })}
    </span>
  );
}

Object.assign(window, { Communications });
