// History screen — conversation logs with filters
// Per user request: clicking a row opens a MODAL with the full conversation
// (instead of inline expand like the original design).
function ScreenHistory() {
  const [logs, setLogs] = useState(Data.recentLogs);
  const [filter, setFilter] = useState("all"); // all | fallback | low | high
  const [query, setQuery] = useState("");
  const [period, setPeriod] = useState("all");
  const [openLog, setOpenLog] = useState(null); // log object => opens modal
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);
  const [total, setTotal] = useState(0);
  const [counts, setCounts] = useState({ all: 0, fallback: 0, low: 0, high: 0, rated_low: 0 });
  const [avgSim, setAvgSim] = useState('0.00');
  const [loading, setLoading] = useState(false);

  // Busca server-side: o termo é debounced (300ms) pra não disparar request a
  // cada tecla. Filtro/período/busca disparam reload e voltam pra página 1.
  const [debouncedQuery, setDebouncedQuery] = useState("");
  useEffect(() => {
    const t = setTimeout(() => setDebouncedQuery(query), 300);
    return () => clearTimeout(t);
  }, [query]);
  useEffect(() => { setPage(1); }, [period, filter, debouncedQuery]);

  // 'since' ISO derivado do período no fuso LOCAL do navegador (TZ-correto:
  // "Hoje" = meia-noite local de hoje). O backend compara created_at >= since.
  const sinceIso = useMemo(() => {
    const now = new Date();
    if (period === 'today') return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
    if (period === 'week')  return new Date(now.getTime() - 7  * 86400000).toISOString();
    if (period === 'month') return new Date(now.getTime() - 30 * 86400000).toISOString();
    return null; // 'all' → sem filtro de data (todo o histórico)
  }, [period]);

  // Rótulo do período pra deixar os cards coerentes com o seletor.
  const periodLabel = period === 'today' ? 'hoje'
                    : period === 'week'  ? 'últimos 7 dias'
                    : period === 'month' ? 'últimos 30 dias'
                    : 'no total';

  // Carrega do servidor sempre que página/período/busca/aba mudam.
  useEffect(() => {
    let alive = true;
    setLoading(true);
    DataLoader.logs({ page, since: sinceIso, search: debouncedQuery, filter }).then((meta) => {
      if (!alive) return;
      setLogs(Data.recentLogs.slice());
      if (meta) {
        setTotalPages(meta.totalPages || 1);
        setTotal(meta.total || 0);
        if (meta.counts) setCounts(meta.counts);
        if (meta.avgSim != null) setAvgSim(meta.avgSim);
      }
      setLoading(false);
    });
    return () => { alive = false; };
  }, [page, sinceIso, debouncedQuery, filter]);

  // O servidor já aplicou período + busca + aba; a tabela renderiza direto.
  const filtered = logs;
  const fallbacksToday = counts.fallback;
  const fbPct = counts.all ? ((counts.fallback / counts.all) * 100).toFixed(1) : '0.0';

  return (
    <div className="page" data-screen-label="Histórico">
      <div className="page-header">
        <div>
          <h1>
            <span className="h1-icon"><i className="fas fa-history"></i></span>
            Histórico de conversas
          </h1>
          <p className="sub">Cada interação do chatbot. Clique em uma linha para abrir a conversa completa.</p>
        </div>
        <div className="page-header-actions">
          <button className="btn btn-ghost"><i className="fas fa-file-export"></i> Exportar CSV</button>
          <button className="btn btn-danger"><i className="fas fa-eraser"></i> Limpar antigos</button>
        </div>
      </div>

      <div className="dash-grid-3">
        <div className="card tight" style={{ display: "flex", alignItems: "center", gap: 14 }}>
          <div className="stat-icon or" style={{ width: 36, height: 36 }}><i className="fas fa-comments"></i></div>
          <div>
            <div style={{ fontSize: 22, fontWeight: 600 }}>{Data.stats.conversations}</div>
            <div style={{ fontSize: 11.5, color: "var(--t3)" }}>conversas hoje</div>
          </div>
        </div>
        <div className="card tight" style={{ display: "flex", alignItems: "center", gap: 14 }}>
          <div className="stat-icon rd" style={{ width: 36, height: 36 }}><i className="fas fa-exclamation"></i></div>
          <div>
            <div style={{ fontSize: 22, fontWeight: 600 }}>{fallbacksToday} <span style={{ fontSize: 12, color: "var(--rd)" }}>({fbPct}%)</span></div>
            <div style={{ fontSize: 11.5, color: "var(--t3)" }}>fallbacks {periodLabel}</div>
          </div>
        </div>
        <div className="card tight" style={{ display: "flex", alignItems: "center", gap: 14 }}>
          <div className="stat-icon gr" style={{ width: 36, height: 36 }}><i className="fas fa-bullseye"></i></div>
          <div>
            <div style={{ fontSize: 22, fontWeight: 600 }}>{avgSim}</div>
            <div style={{ fontSize: 11.5, color: "var(--t3)" }}>similaridade média</div>
          </div>
        </div>
      </div>

      <div className="row gap-sm" style={{ justifyContent: "space-between" }}>
        <div className="subtabs" style={{ background: "var(--bg2)" }}>
          <button className={"tab" + (filter === "all" ? " active" : "")} onClick={() => setFilter("all")}>Todas<span className="count">{counts.all}</span></button>
          <button className={"tab" + (filter === "fallback" ? " active" : "")} onClick={() => setFilter("fallback")}>Fallbacks<span className="count">{counts.fallback}</span></button>
          <button className={"tab" + (filter === "low" ? " active" : "")} onClick={() => setFilter("low")}>Baixa similaridade<span className="count">{counts.low}</span></button>
          <button className={"tab" + (filter === "rated_low" ? " active" : "")} onClick={() => setFilter("rated_low")}>Mal avaliadas<span className="count">{counts.rated_low || 0}</span></button>
          <button className={"tab" + (filter === "high" ? " active" : "")} onClick={() => setFilter("high")}>Alta similaridade<span className="count">{counts.high}</span></button>
        </div>
        <div className="row gap-sm">
          <select className="input" style={{ width: 170 }} value={period} onChange={(e) => setPeriod(e.target.value)}>
            <option value="all">Todo o período</option>
            <option value="today">Hoje</option>
            <option value="week">Últimos 7 dias</option>
            <option value="month">Últimos 30 dias</option>
          </select>
          <input className="input" placeholder="Buscar pergunta ou resposta…" style={{ width: 280 }} value={query} onChange={(e) => setQuery(e.target.value)} />
        </div>
      </div>

      <div className="card flush">
        <div className="table-wrap">
          <table className="dt">
            <thead>
              <tr>
                <th style={{ width: 96 }}>Data</th>
                <th style={{ width: 72 }}>Hora</th>
                <th>Pergunta</th>
                <th>Resposta (resumida)</th>
                <th style={{ width: 110 }}>Similaridade</th>
                <th style={{ width: 90 }}>Fontes</th>
                <th style={{ width: 70, textAlign: "right" }}></th>
              </tr>
            </thead>
            <tbody>
              {loading && (
                <tr><td colSpan="7" style={{ textAlign: "center", color: "var(--t3)", padding: 24 }}>
                  <i className="fas fa-circle-notch fa-spin"></i> Carregando…
                </td></tr>
              )}
              {!loading && filtered.map((l, i) => {
                return (
                  <tr key={l.id || i} className="log-row" onClick={() => setOpenLog(l)}>
                    <td style={{ fontFamily: "var(--ff-mono)", color: "var(--t3)", fontSize: 12, whiteSpace: "nowrap" }}>{l.dateShort}</td>
                    <td style={{ fontFamily: "var(--ff-mono)", color: "var(--t3)", fontSize: 12 }}>{l.time}</td>
                    <td style={{ fontWeight: 500 }}>{l.q}</td>
                    <td style={{ color: "var(--t2)", maxWidth: 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{l.a}</td>
                    <td>
                      <div className="row gap-sm">
                        <div style={{ width: 52, height: 5, background: "var(--bg3)", borderRadius: 3, overflow: "hidden" }}>
                          <div style={{
                            width: (l.sim * 100) + "%",
                            height: "100%",
                            background: l.fb ? "var(--rd)" : l.sim >= 0.8 ? "var(--gr)" : l.sim >= 0.6 ? "var(--am)" : "var(--rd)",
                          }}></div>
                        </div>
                        <code style={{ fontSize: 11, color: l.fb ? "var(--rd)" : l.sim >= 0.8 ? "var(--gr)" : "var(--am)", fontWeight: 600 }}>{l.sim.toFixed(2)}</code>
                      </div>
                    </td>
                    <td>
                      {l.fb ? (
                        <span className="badge b-rd"><i className="fas fa-exclamation"></i> fallback</span>
                      ) : (
                        <span style={{ fontSize: 12, color: "var(--t2)" }}>{l.sources} chunk{l.sources !== 1 ? "s" : ""}</span>
                      )}
                    </td>
                    <td style={{ textAlign: "right" }}>
                      <i className="fas fa-external-link-alt" style={{ color: "var(--t3)", fontSize: 11 }}></i>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        {!loading && filtered.length === 0 && (
          <div className="empty">
            <i className="fas fa-search"></i>
            <p>Nenhuma conversa encontrada com os filtros atuais</p>
          </div>
        )}
      </div>

      <div className="row gap-sm" style={{ justifyContent: "space-between", padding: "0 4px" }}>
        <span className="muted" style={{ fontSize: 12 }}>Mostrando {filtered.length} de {total} conversas (página {page}/{totalPages})</span>
        <div className="row gap-sm">
          <button className="btn btn-ghost btn-sm" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}><i className="fas fa-chevron-left"></i> Anterior</button>
          <span className="badge b-mut" style={{ fontFamily: "var(--ff-mono)" }}>{page} / {totalPages}</span>
          <button className="btn btn-ghost btn-sm" disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}>Próxima <i className="fas fa-chevron-right"></i></button>
        </div>
      </div>

      {openLog && <ConversationModal log={openLog} onClose={() => setOpenLog(null)} />}
    </div>
  );
}

// ---- Conversation modal ----------------------------------------------------
// Layout estilo chat: bot do lado esquerdo, usuário do lado direito.
// Carrega a thread completa da sessão se houver session_id; caso contrário,
// renderiza só o turno isolado.
function ConversationModal({ log, onClose }) {
  const toast = useToast();
  const confirm = useConfirm();
  const [thread, setThread] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!log || !log.session) {
      setThread(null);
      return;
    }
    setLoading(true);
    let alive = true;
    DataLoader.conversationThread(log.session).then((msgs) => {
      if (!alive) return;
      setThread(msgs);
      setLoading(false);
    });
    return () => { alive = false; };
  }, [log && log.session]);

  const turns = thread && thread.length ? thread : [log];
  const allFallback = turns.every((t) => t.fb);
  const sessionShort = log.session ? String(log.session).slice(0, 8) : null;

  const askDelete = async () => {
    if (!log.session) {
      toast({ title: "Sem session_id", msg: "Esta conversa não tem identificador para apagar.", kind: "info" });
      return;
    }
    const ok = await confirm({
      title: 'Apagar conversa (LGPD)',
      message: 'Você vai apagar TODA a conversa desta sessão.',
      details: 'Inclui todas as mensagens, fontes consultadas e métricas associadas a esta sessão. ' +
               'Esta ação é irreversível e atende ao direito ao esquecimento (LGPD).',
      confirmLabel: 'Apagar conversa',
      cancelLabel: 'Cancelar',
      danger: true,
      icon: 'fa-trash',
    });
    if (!ok) return;
    api('/api/admin/chat-logs?session_id=' + encodeURIComponent(log.session), { method: 'DELETE' })
      .then(() => { toast({ title: "Conversa apagada", kind: "success" }); onClose(); })
      .catch((e) => toast({ title: "Falha ao apagar", msg: e.message, kind: "error" }));
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal modal-wide modal-tall" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h3>
            <i className="fas fa-comments"></i> Detalhes da conversa
            {thread && thread.length > 1 && (
              <span className="badge b-bl" style={{ marginLeft: 8 }}>
                <i className="fas fa-comment-dots"></i> {thread.length} mensagens
              </span>
            )}
            {allFallback && (
              <span className="badge b-rd" style={{ marginLeft: 8 }}>
                <i className="fas fa-exclamation"></i> fallback
              </span>
            )}
          </h3>
          <button type="button" className="icon-btn" onClick={onClose}><i className="fas fa-times"></i></button>
        </div>
        <div className="conv-modal-body">
          <div className="conv-meta">
            <span className="pill"><i className="fas fa-calendar" style={{ marginRight: 4 }}></i>{log.date}</span>
            {sessionShort && <span className="pill"><i className="fas fa-fingerprint" style={{ marginRight: 4 }}></i>session: {sessionShort}…</span>}
          </div>

          {loading && (
            <div style={{ textAlign: 'center', padding: 20, color: 'var(--t3)' }}>
              <i className="fas fa-circle-notch fa-spin"></i> Carregando thread completa…
            </div>
          )}

          {!loading && turns.map((t, i) => (
            <ConversationTurn key={t.id || i} turn={t} />
          ))}
        </div>
        <div className="modal-foot">
          <button type="button" className="btn btn-danger btn-sm" onClick={askDelete}>
            <i className="fas fa-trash"></i> Apagar conversa (LGPD)
          </button>
          <button type="button" className="btn btn-ghost" onClick={onClose}>Fechar</button>
        </div>
      </div>
    </div>
  );
}

// Componente de avaliação de resposta (estrelas 1-5). Persiste via POST
// /api/admin/chat-logs/:id/rate. Quando rating ≤2★, backend auto-invalida
// a entrada de cache pra que a mesma pergunta dispare uma resposta nova
// no próximo turno. Mostra estado atual (ratedBy + when + note) se já avaliado.
function StarRating({ turnId, initialRating, initialNote, ratedBy, ratedAt, onChange }) {
  const [rating, setRating]   = useState(initialRating || 0);
  const [hover, setHover]     = useState(0);
  const [note, setNote]       = useState(initialNote || '');
  const [showNote, setShowNote] = useState(false);
  const [saving, setSaving]   = useState(false);
  const [meta, setMeta]       = useState({ ratedBy: ratedBy || null, ratedAt: ratedAt || null });
  const toast = useToast();

  // Sincroniza quando o turno carregar com avaliação já feita (mount inicial
  // vinha 0 enquanto o thread carregava do servidor).
  useEffect(() => {
    if (initialRating != null && initialRating !== rating) setRating(initialRating);
    if (initialNote && initialNote !== note) setNote(initialNote);
    setMeta({ ratedBy: ratedBy || null, ratedAt: ratedAt || null });
  }, [initialRating, initialNote, ratedBy, ratedAt]);

  const submit = async (stars, noteText) => {
    if (!turnId) return;
    setSaving(true);
    try {
      const r = await fetch('/api/admin/chat-logs/' + turnId + '/rate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + (Auth && Auth.token ? Auth.token : '') },
        body: JSON.stringify({ rating: stars, note: noteText || null }),
      });
      const data = await r.json();
      if (!r.ok) throw new Error(data.error || 'Falha ao salvar');
      setMeta({ ratedBy: data.rated_by, ratedAt: data.rated_at });
      toast({
        title: 'Avaliação salva',
        msg: data.cache_invalidated
            ? 'Cache da pergunta foi invalidado — próxima resposta virá nova.'
            : 'Obrigado pelo feedback!',
        kind: 'success',
      });
      if (typeof onChange === 'function') onChange(stars, noteText);
    } catch (e) {
      toast({ title: 'Falha ao avaliar', msg: e.message, kind: 'error' });
      // Reverte rating otimista
      setRating(initialRating || 0);
    } finally {
      setSaving(false);
    }
  };

  const clear = async () => {
    if (!turnId) return;
    setSaving(true);
    try {
      const r = await fetch('/api/admin/chat-logs/' + turnId + '/rate', {
        method: 'DELETE',
        headers: { Authorization: 'Bearer ' + (Auth && Auth.token ? Auth.token : '') },
      });
      if (!r.ok) throw new Error('Falha ao remover');
      setRating(0); setNote(''); setMeta({ ratedBy: null, ratedAt: null }); setShowNote(false);
      toast({ title: 'Avaliação removida', kind: 'success' });
      if (typeof onChange === 'function') onChange(0, '');
    } catch (e) {
      toast({ title: 'Falha ao remover', msg: e.message, kind: 'error' });
    } finally {
      setSaving(false);
    }
  };

  const onStarClick = (n) => {
    if (saving) return;
    setRating(n); // otimista
    submit(n, note);
  };

  const fmtDate = (iso) => {
    if (!iso) return '';
    try {
      return new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
    } catch (_) { return ''; }
  };

  return (
    <div className="star-rating" style={{ marginTop: 6, fontSize: 12, color: 'var(--t2)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
        <span style={{ marginRight: 2 }}>Qualidade:</span>
        <div onMouseLeave={() => setHover(0)} style={{ display: 'inline-flex', gap: 2 }}>
          {[1, 2, 3, 4, 5].map((n) => {
            const filled = (hover || rating) >= n;
            return (
              <button
                key={n}
                type="button"
                onClick={() => onStarClick(n)}
                onMouseEnter={() => setHover(n)}
                disabled={saving}
                title={`${n} estrela${n > 1 ? 's' : ''}`}
                style={{
                  background: 'transparent', border: 'none', padding: 0, cursor: saving ? 'wait' : 'pointer',
                  color: filled ? 'var(--or, #f97316)' : 'var(--t3)',
                  fontSize: 16, lineHeight: 1, transition: 'color 0.1s',
                }}
              >
                <i className={filled ? 'fas fa-star' : 'far fa-star'}></i>
              </button>
            );
          })}
        </div>
        {rating > 0 && (
          <>
            <button
              type="button"
              className="provenance-toggle"
              onClick={() => setShowNote((v) => !v)}
              style={{ fontSize: 11 }}
              title={note ? 'Editar nota' : 'Adicionar nota'}
            >
              <i className={'fas ' + (note ? 'fa-comment' : 'fa-comment-medical')}></i>
              {note ? ' nota' : ' + nota'}
            </button>
            <button
              type="button"
              className="provenance-toggle"
              onClick={clear}
              style={{ fontSize: 11, color: 'var(--rd)' }}
              title="Remover avaliação"
              disabled={saving}
            >
              <i className="fas fa-times"></i>
            </button>
          </>
        )}
        {meta.ratedBy && (
          <span style={{ fontSize: 10, color: 'var(--t3)', marginLeft: 'auto' }}>
            por {meta.ratedBy} · {fmtDate(meta.ratedAt)}
          </span>
        )}
      </div>
      {showNote && (
        <div style={{ marginTop: 6 }}>
          <textarea
            value={note}
            onChange={(e) => setNote(e.target.value)}
            placeholder="O que foi bom/ruim nesta resposta? (opcional)"
            rows={2}
            style={{
              width: '100%', fontSize: 12, padding: 6,
              background: 'var(--bg1)', border: '1px solid var(--b1)',
              borderRadius: 'var(--r-input)', color: 'var(--t1)',
              resize: 'vertical',
            }}
            disabled={saving}
          />
          <div className="row gap-sm" style={{ marginTop: 4, justifyContent: 'flex-end' }}>
            <button
              type="button"
              className="btn btn-soft btn-sm"
              onClick={() => submit(rating, note)}
              disabled={saving || !rating}
            >
              <i className={'fas ' + (saving ? 'fa-spinner fa-spin' : 'fa-save')}></i> Salvar nota
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// Um turno = pergunta do usuário (direita) + resposta do bot (esquerda)
function ConversationTurn({ turn }) {
  const [showSources, setShowSources] = useState(false);
  const hasSources = Array.isArray(turn.sourcesList) && turn.sourcesList.length > 0;
  const isFallback = !!turn.fb;
  // "Resposta sem fonte" = não foi fallback explícito mas também não citou nada.
  // É o caso mais perigoso porque o bot pode ter "inventado" baseado no system prompt.
  const couldBeInvention = !hasSources && !isFallback;

  return (
    <div className="conv-turn">
      {/* Pergunta do usuário — direita */}
      <div className="conv-row user">
        <div className="conv-bubble-chat user">
          <div className="conv-bubble-text">{turn.q}</div>
          <div className="conv-bubble-time">{turn.time || turn.date}</div>
        </div>
      </div>

      {/* Resposta do bot — esquerda */}
      <div className="conv-row bot">
        <div
          className={"conv-bubble-chat bot" + (isFallback ? ' fallback' : '') + (couldBeInvention ? ' invention' : '')}
          style={{ maxWidth: '85%' }}
        >
          <ProvenanceBadge turn={turn} hasSources={hasSources} isFallback={isFallback} couldBeInvention={couldBeInvention} />

          <div className="conv-bubble-text" style={{ whiteSpace: 'pre-wrap' }}>
            {turn.a || <em style={{ color: 'var(--t3)' }}>(resposta vazia)</em>}
          </div>

          <div className="conv-bubble-meta">
            <span>{turn.time || turn.date}</span>
            {turn.responseTimeMs > 0 && <span>· {turn.responseTimeMs}ms</span>}
            {(() => {
              const logSim = typeof turn.sim === 'number' ? turn.sim : 0;
              const maxSrc = hasSources
                ? Math.max(0, ...(turn.sourcesList || []).map((s) => Number(s.similarity) || 0))
                : 0;
              const eff = logSim > 0 ? logSim : maxSrc;
              if (eff <= 0) return null;
              return (
                <span title={logSim === 0 && maxSrc > 0 ? 'sim derivada das fontes (cache hit — log não registrou)' : 'top_similarity registrada no log'}>
                  · sim <strong style={{ color: isFallback ? 'var(--rd)' : eff >= 0.8 ? 'var(--gr)' : 'var(--am)' }}>
                    {eff.toFixed(2)}
                  </strong>
                </span>
              );
            })()}
            <button
              type="button"
              onClick={() => setShowSources((v) => !v)}
              className="provenance-toggle"
              title="Ver origem da resposta"
            >
              <i className={"fas " + (showSources ? "fa-minus" : "fa-plus")}></i>
              {hasSources
                ? ` ${turn.sources} fonte${turn.sources === 1 ? '' : 's'}`
                : isFallback ? ' (fallback)' : ' (sem fonte)'}
            </button>
            <LatencyBreakdown turn={turn} />
          </div>

          {showSources && (
            <ProvenancePanel
              sources={turn.sourcesList || []}
              isFallback={isFallback}
              couldBeInvention={couldBeInvention}
              topSim={turn.sim}
              minSim={turn.minSim}
            />
          )}

          {/* Avaliação 1-5★ pelo admin. Aparece em todo turno do bot exceto
              quando o id do log não veio (turno mock/legado). Persiste via
              /api/admin/chat-logs/:id/rate; ≤2★ invalida cache automático. */}
          {turn.id && (
            <StarRating
              turnId={turn.id}
              initialRating={turn.rating}
              initialNote={turn.ratingNote}
              ratedBy={turn.ratingBy}
              ratedAt={turn.ratingAt}
              onChange={(stars, noteText) => {
                turn.rating = stars || null;
                turn.ratingNote = noteText || '';
              }}
            />
          )}
        </div>
      </div>
    </div>
  );
}

// Pequena badge no topo do balão indicando a confiabilidade da resposta
function ProvenanceBadge({ turn, hasSources, isFallback, couldBeInvention }) {
  const logSim = typeof turn.sim === 'number' ? turn.sim : 0;
  const maxSourceSim = hasSources
    ? Math.max(0, ...(turn.sourcesList || []).map((s) => Number(s.similarity) || 0))
    : 0;
  const sim = logSim > 0 ? logSim : maxSourceSim;
  // Cache hit detection: o caminho de cache em assistant.js grava
  // time_llm_ms = 0 (e os outros tempos também = 0). Esse é o indicador
  // definitivo de cache, mais confiável que olhar similaridade.
  const llmMs = (turn.timings && turn.timings.llm) || 0;
  const embMs = (turn.timings && turn.timings.embedding) || 0;
  const retMs = (turn.timings && turn.timings.retrieval) || 0;
  const isCacheHit = hasSources && llmMs === 0 && embMs === 0 && retMs === 0;

  let cls, icon, label, hint;
  if (isFallback) {
    cls = 'pb-rd'; icon = 'fa-shield-halved';
    label = 'FALLBACK — bot avisou que não sabia';
    hint = 'A política de fallback foi acionada. O bot NÃO inventou — disse explicitamente que não tinha a informação.';
  } else if (couldBeInvention) {
    cls = 'pb-am'; icon = 'fa-triangle-exclamation';
    label = 'SEM FONTES — possível invenção';
    hint = 'O bot respondeu sem consumir nenhum trecho da base. Pode ser invenção do LLM.';
  } else if (isCacheHit) {
    cls = sim >= 0.8 ? 'pb-gr' : sim >= 0.6 ? 'pb-am' : 'pb-rd';
    icon = 'fa-bolt';
    label = `CACHE · resposta reusada (sim das fontes: ${sim.toFixed(2)})`;
    hint = 'Pergunta idêntica já respondida antes — bot serviu a resposta cacheada com as fontes originais. ' +
           'A similaridade no log fica 0.00 porque o pipeline não recomputa busca no cache hit.';
  } else if (sim >= 0.8) {
    cls = 'pb-gr'; icon = 'fa-circle-check';
    label = `RAG · alta confiança (sim ${sim.toFixed(2)})`;
    hint = 'O RAG encontrou trechos muito próximos da pergunta. Resposta provavelmente fiel à base.';
  } else if (sim >= 0.6) {
    cls = 'pb-am'; icon = 'fa-circle-info';
    label = `RAG · confiança média (sim ${sim.toFixed(2)})`;
    hint = 'Casamento médio entre pergunta e base. Vale conferir o texto da resposta vs os trechos abaixo.';
  } else {
    cls = 'pb-rd'; icon = 'fa-triangle-exclamation';
    label = `RAG · baixa similaridade (${sim.toFixed(2)}) — checar`;
    hint = 'Os trechos recuperados têm pouca afinidade com a pergunta. Risco de o bot ter generalizado demais.';
  }
  return (
    <div className={"provenance-badge " + cls} title={hint}>
      <i className={"fas " + icon}></i> {label}
    </div>
  );
}

// Painel expandível com cada chunk usado pelo RAG + snippet do texto-fonte
function ProvenancePanel({ sources, isFallback, couldBeInvention, topSim, minSim }) {
  if (couldBeInvention) {
    return (
      <div className="provenance-panel invention">
        <div className="provenance-warning">
          <i className="fas fa-robot"></i>
          <div>
            <strong>Esta resposta não consumiu nenhuma fonte da base.</strong>
            <div style={{ fontSize: 11.5, color: 'var(--t2)', marginTop: 4, lineHeight: 1.5 }}>
              O bot pode ter "completado" usando só o conhecimento geral do LLM (= risco de invenção),
              OU a base não tinha nada similar à pergunta e o pipeline não capturou como fallback formal.
              {typeof topSim === 'number' && topSim > 0 && (
                <> Maior similaridade encontrada nos chunks recuperados foi <code style={{ color: 'var(--am)' }}>{topSim.toFixed(2)}</code>,
                  abaixo do threshold (0.55).</>
              )}
            </div>
            <div style={{ fontSize: 11, color: 'var(--t3)', marginTop: 6 }}>
              <strong>Recomendação:</strong> se a resposta estiver correta, crie um contexto direto sobre o tema.
              Se estiver errada, reforce a política de fallback no system prompt.
            </div>
          </div>
        </div>
      </div>
    );
  }
  if (isFallback && sources.length === 0) {
    return (
      <div className="provenance-panel fallback">
        <div className="provenance-warning">
          <i className="fas fa-shield-halved"></i>
          <div>
            <strong>Fallback formal disparado.</strong>
            <div style={{ fontSize: 11.5, color: 'var(--t2)', marginTop: 4, lineHeight: 1.5 }}>
              O bot reconheceu que não tinha base para responder e seguiu a política de fallback
              configurada na persona (sem inventar).
              {typeof topSim === 'number' && topSim > 0 && (
                <> Maior similaridade dos chunks: <code style={{ color: 'var(--rd)' }}>{topSim.toFixed(2)}</code> &lt; 0.55.</>
              )}
            </div>
          </div>
        </div>
      </div>
    );
  }
  // Caso normal — listar fontes
  // Computa stats reais a partir das próprias fontes (mais confiável que
  // top_similarity do log, que pode estar zerado em cache hit)
  const sims = sources.map((s) => Number(s.similarity) || 0).filter((n) => n > 0);
  const maxSim = sims.length ? Math.max(...sims) : (topSim || 0);
  const minSimReal = sims.length ? Math.min(...sims) : (typeof minSim === 'number' ? minSim : null);
  return (
    <div className="provenance-panel">
      <div className="provenance-head">
        <i className="fas fa-link"></i>
        <span>Fontes consultadas pelo RAG ({sources.length})</span>
        <span className="provenance-stats">
          maior sim <strong style={{ color: maxSim >= 0.8 ? 'var(--gr)' : maxSim >= 0.6 ? 'var(--am)' : 'var(--rd)' }}>{maxSim.toFixed(2)}</strong>
          {minSimReal != null && minSimReal > 0 && (
            <> · menor <strong style={{ color: minSimReal >= 0.6 ? 'var(--am)' : 'var(--rd)' }}>{minSimReal.toFixed(2)}</strong></>
          )}
        </span>
      </div>
      <div className="provenance-list">
        {sources.map((s, i) => (
          <ProvenanceSource key={i} index={i} source={s} />
        ))}
      </div>
      <div className="provenance-foot">
        <i className="fas fa-info-circle"></i>
        Compare o texto da resposta com os trechos abaixo — se a IA disse algo que não aparece aqui,
        a chance de invenção é alta.
      </div>
    </div>
  );
}

function ProvenanceSource({ index, source }) {
  const [expanded, setExpanded] = useState(false);
  const sim = typeof source.similarity === 'number' ? source.similarity : 0;
  const simColor = sim >= 0.8 ? 'var(--gr)' : sim >= 0.6 ? 'var(--am)' : 'var(--rd)';
  const typeIcon = {
    pdf: 'fa-file-pdf', docx: 'fa-file-word', txt: 'fa-file-alt', text: 'fa-file-alt',
    md: 'fa-file-code', url: 'fa-globe', upload: 'fa-file-arrow-up',
  };
  const typeColor = {
    pdf: 'var(--rd)', docx: 'var(--bl)', url: 'var(--or)',
  };
  const t = (source.type || 'upload').toLowerCase();
  const icon = typeIcon[t] || 'fa-file';
  const iconColor = typeColor[t] || 'var(--t2)';
  const snippet = source.snippet || '';
  const fullLen = source.snippet_length || snippet.length;
  const isTruncated = snippet.endsWith('…') || snippet.endsWith('...');
  const displayed = expanded || snippet.length <= 280 ? snippet : (snippet.slice(0, 280) + '…');

  return (
    <div className="provenance-item">
      <div className="provenance-item-head">
        <span className="provenance-rank">{String(index + 1).padStart(2, '0')}</span>
        <i className={"fas " + icon} style={{ color: iconColor, fontSize: 12 }}></i>
        <span className="provenance-file" title={source.file || ''}>{source.file || '(sem nome)'}</span>
        {source.url && (
          <a href={source.url} target="_blank" rel="noopener" className="provenance-url" title={source.url}>
            <i className="fas fa-external-link-alt" style={{ fontSize: 9 }}></i> abrir
          </a>
        )}
        <span className="provenance-sim" style={{ color: simColor }}>
          sim {sim.toFixed(3)}
        </span>
      </div>
      <div className="provenance-snippet">
        <pre>{displayed}</pre>
        {(snippet.length > 280) && (
          <button type="button" className="provenance-expand" onClick={() => setExpanded((v) => !v)}>
            {expanded ? '↑ recolher' : `↓ ver mais (${snippet.length} chars${isTruncated ? `, original ~${fullLen}` : ''})`}
          </button>
        )}
      </div>
    </div>
  );
}

// ─── Latency breakdown — onde foram os ms da resposta ───────────────────
// Mostra como bolinhas mono na meta line: emb / ret / llm / post → soma total.
// Útil pra diagnosticar lentidão (se llm é 90%+ → LLM lento; se ret é alto →
// índice precisa de tuning; etc.). Quando todos zerados = cache hit.
function LatencyBreakdown({ turn }) {
  const [open, setOpen] = useState(false);
  const t = turn.timings || {};
  const emb = t.embedding || 0;
  const ret = t.retrieval || 0;
  const llm = t.llm || 0;
  const post = t.postprocess || 0;
  const sum = emb + ret + llm + post;
  if (sum <= 0) {
    // Cache hit ou log antigo sem breakdown
    return null;
  }
  const dominantLabel = llm > sum * 0.6 ? 'LLM domina'
                    : ret > sum * 0.4 ? 'retrieval domina'
                    : emb > sum * 0.3 ? 'embedding domina'
                    : 'distribuído';

  return (
    <button
      type="button"
      className="latency-breakdown-toggle"
      onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); }}
      title="Ver onde foi gasto cada milissegundo"
    >
      <i className="fas fa-stopwatch"></i>
      {!open
        ? <> · breakdown</>
        : (
          <span className="latency-bits">
            <span title="embedding da pergunta">emb {emb}ms</span>
            <span>·</span>
            <span title="busca vetorial + BM25 + rerank">ret {ret}ms</span>
            <span>·</span>
            <span style={{ color: llm > sum * 0.6 ? 'var(--rd)' : 'var(--or)', fontWeight: 600 }} title="chamada ao LLM (provider ativo)">llm {llm}ms</span>
            <span>·</span>
            <span title="limpeza/sanitização da resposta">post {post}ms</span>
            <span style={{ color: 'var(--t3)' }}>· {dominantLabel}</span>
          </span>
        )
      }
    </button>
  );
}

window.LatencyBreakdown = LatencyBreakdown;
window.ScreenHistory = ScreenHistory;
window.ConversationModal = ConversationModal;
