// Main App — top-level router, shell, drawer, command palette, modals
const TWEAK_DEFAULTS = (() => {
  try {
    const saved = localStorage.getItem('sintracon_admin_tweaks');
    if (saved) return JSON.parse(saved);
  } catch (e) {}
  return { theme: 'dark', sidebar: 'full', accent: 'orange', dashLayout: 'cards' };
})();

// RbacToastBridge — escuta o evento global `rbac:forbidden` emitido pelo api()
// em data.jsx e mostra toast amigável quando o backend retorna 403. Ignora as
// pings do whoami (path /api/admin/whoami pode retornar 403 em fluxo legado).
function RbacToastBridge() {
  const toast = useToast();
  useEffect(() => {
    const onForbidden = (ev) => {
      const d = (ev && ev.detail) || {};
      if (d.path && d.path.indexOf('/whoami') !== -1) return;
      toast({
        title: 'Sem permissão',
        msg: d.message || 'Esta ação requer uma role superior.',
        kind: 'error',
      });
    };
    window.addEventListener('rbac:forbidden', onForbidden);
    return () => window.removeEventListener('rbac:forbidden', onForbidden);
  }, [toast]);
  return null;
}

function App() {
  const [authed, setAuthed] = useState(!!Auth.token);
  const [route, setRoute] = useState("dashboard");
  const [cmdkOpen, setCmdkOpen] = useState(false);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [modal, setModal] = useState(null);
  const [saveState, setSaveStateVal] = useState("saved");
  const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);
  const [version, setVersion] = useState('');
  const [user, setUser] = useState(null);
  // Branding configurável (nome + logo + inicial), lido de /api/branding.
  // Default preserva visual atual enquanto carrega ou se endpoint falhar.
  const [branding, setBranding] = useState({ brand_name: 'SINTRACON-SP', brand_initial: 'S', brand_logo_path: '' });

  // Persist + apply tweaks
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", tweaks.theme);
    document.documentElement.setAttribute("data-accent", tweaks.accent);
    try {
      localStorage.setItem('sintracon_admin_tweaks', JSON.stringify(tweaks));
      localStorage.setItem(THEME_KEY, tweaks.theme);
    } catch (e) {}
  }, [tweaks]);

  const setTweak = (k, v) => setTweaks((t) => ({ ...t, [k]: v }));

  // Fetch app version once. /api/version retorna JSON ({"version":"3.x.y"});
  // o parsing-como-texto antigo deixava {"version":"3.x.y"} literal no sidebar.
  useEffect(() => {
    fetch('/api/version').then((r) => r.json()).then((j) => {
      const v = (j && j.version) ? String(j.version).trim() : '';
      if (v) setVersion('v' + v);
    }).catch(() => {});
    // Branding também é público — carrega antes do login pra mostrar nome
    // certo na tela de login + sidebar. /api/branding sempre retorna 200
    // com defaults.
    fetch('/api/branding').then((r) => r.json()).then((b) => {
      if (b && typeof b === 'object') {
        setBranding((cur) => ({ ...cur, ...b }));
        // Cacheia em localStorage pro splash inline do admin.html ler no próximo
        // carregamento — assim a logo + nome aparecem na tela "Carregando" sem
        // esperar /api/branding terminar.
        try { localStorage.setItem('sintracon_admin_branding', JSON.stringify(b)); } catch (_) {}
      }
    }).catch(() => {});
  }, []);

  // Helper: recarrega branding (chamado quando admin salva mudanças em
  // Sistema → Geral → Identidade). Exposto globalmente pra outras telas.
  const reloadBranding = () => {
    fetch('/api/branding').then((r) => r.json()).then((b) => {
      if (b && typeof b === 'object') {
        setBranding((cur) => ({ ...cur, ...b }));
        try { localStorage.setItem('sintracon_admin_branding', JSON.stringify(b)); } catch (_) {}
      }
    }).catch(() => {});
  };
  useEffect(() => { window.appReloadBranding = reloadBranding; }, []);

  // Preload counts + whoami no boot autenticado
  const [, forceRerender] = useState(0);
  useEffect(() => {
    if (!authed) return;
    DataLoader.whoami().then((u) => { if (u) setUser(u); });
    Promise.all([
      DataLoader.directContexts(),
      DataLoader.files(),
      DataLoader.sitePages(),
      DataLoader.metrics(),
    ]).then(() => forceRerender((n) => n + 1));
  }, [authed]);

  // Expõe setRoute globalmente — útil pra componentes de tela que precisam
  // mandar o usuário pra outra tela sem ter que passar props pelo App.
  useEffect(() => { window.appNavigate = setRoute; }, []);

  // Sessão expirada (401 vindo do api() em data.jsx): volta pra tela de login
  // em vez de manter a casca renderizada sem conteúdo. Cobre token expirado
  // no meio do uso e restart do servidor com segredo efêmero.
  useEffect(() => {
    const onExpired = () => setAuthed(false);
    window.addEventListener('auth:expired', onExpired);
    return () => window.removeEventListener('auth:expired', onExpired);
  }, []);

  // Keyboard ⌘K / Ctrl+K
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
        e.preventDefault();
        setCmdkOpen((v) => !v);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const handleAction = (action) => {
    if (!action) return;
    if (action.startsWith("nav:")) { setRoute(action.slice(4)); return; }
    if (action === "toggle-theme") { setTweak("theme", tweaks.theme === "dark" ? "light" : "dark"); return; }
    if (action === "open-preview") {
      // Preview drawer foi removido — o widget real já aparece no canto inferior
      // direito do painel. Dispara o clique no launcher pra abrir o chat.
      const launcher = document.getElementById('widget-launcher');
      if (launcher) launcher.click();
      return;
    }
    if (action === "logout") { Auth.token = ''; setAuthed(false); return; }
    if (action === "add-context" || action === "upload" || action === "add-page" || action === "add-redirect") {
      setModal(action); return;
    }
    if (action === "reindex") {
      setSaveStateVal("saving");
      api('/api/admin/reindex', { method: 'POST' })
        .then(() => setSaveStateVal("saved"))
        .catch(() => setSaveStateVal("saved"));
      return;
    }
    if (action === "clear-cache") {
      api('/api/admin/cache', { method: 'DELETE' }).catch(() => {});
      return;
    }
  };

  const markDirty = () => setSaveStateVal("dirty");
  const markSaved = () => {
    setSaveStateVal("saving");
    setTimeout(() => setSaveStateVal("saved"), 600);
  };

  if (!authed) {
    // Antes de renderizar login, checa query string por reset/invite tokens.
    // Quando presente, sobe a tela apropriada (pública, sem auth) em vez do login.
    const qs = new URLSearchParams(window.location.search);
    const resetToken  = qs.get('reset_token');
    const inviteToken = qs.get('invite_token');
    return (
      <ToastProvider>
        <ConfirmProvider>
          {resetToken ? (
            <ResetPasswordScreen token={resetToken} branding={branding} onDone={() => { window.history.replaceState({}, '', '/admin.html'); window.location.reload(); }} />
          ) : inviteToken ? (
            <AcceptInviteScreen token={inviteToken} branding={branding} onDone={() => { window.history.replaceState({}, '', '/admin.html'); window.location.reload(); }} />
          ) : (
            <LoginScreen onAuthed={() => setAuthed(true)} branding={branding} />
          )}
        </ConfirmProvider>
      </ToastProvider>
    );
  }

  const base = route.split(":")[0];
  return (
    <ToastProvider>
      <ConfirmProvider>
      <RbacToastBridge />
      <div className="app" data-sidebar={tweaks.sidebar}>
        <Sidebar
          route={route}
          onRoute={setRoute}
          collapsed={tweaks.sidebar === "collapsed"}
          onLogout={() => handleAction("logout")}
          version={version}
          theme={tweaks.theme}
          branding={branding}
        />

        <div className="main">
          <Topbar
            route={route}
            onCmdk={() => setCmdkOpen(true)}
            theme={tweaks.theme}
            onTheme={() => setTweak("theme", tweaks.theme === "dark" ? "light" : "dark")}
            onCollapseSidebar={() => setTweak("sidebar", tweaks.sidebar === "full" ? "collapsed" : "full")}
            saveState={saveState}
            onLogout={() => handleAction("logout")}
            user={user}
          />

          {base === "dashboard" && <ScreenDashboard onRoute={setRoute} onAction={handleAction} layout={tweaks.dashLayout} user={user} />}
          {base === "chatbot"   && <ScreenChatbot route={route} onMarkDirty={markDirty} onSaved={markSaved} onAction={handleAction} />}
          {base === "knowledge" && <ScreenKnowledge route={route} onAction={handleAction} />}
          {base === "history"   && <ScreenHistory />}
          {base === "learning"  && window.ScreenLearning && <window.ScreenLearning />}
          {base === "search"    && window.ScreenKbSearch && <window.ScreenKbSearch />}
          {base === "system"    && <ScreenSystem route={route} onAction={handleAction} version={version} />}
        </div>
      </div>

      <CommandPalette
        open={cmdkOpen}
        onClose={() => setCmdkOpen(false)}
        onNavigate={(to) => setRoute(to)}
        onAction={handleAction}
      />

      <TweaksPanel
        open={tweaksOpen}
        onClose={() => setTweaksOpen(false)}
        tweaks={tweaks}
        setTweak={setTweak}
      />

      {modal && <SimpleModal kind={modal} onClose={() => setModal(null)} />}
      </ConfirmProvider>
    </ToastProvider>
  );
}

// ---- Login screen ---------------------------------------------------------
// LoginScreen com fluxo de 2 etapas:
//   1. user + senha + opcional "lembrar 30 dias"
//   2. Se 2FA exigido (totp_enabled E sem trusted_device cookie): tela com
//      input de código TOTP / backup code
// Fluxo decidido pelo backend — frontend só reage ao { requires_2fa } e
// envia o partial_token de volta junto com o código.
function LoginScreen({ onAuthed, branding }) {
  const [step, setStep]     = useState(1); // 1 = credentials, 2 = 2fa code, 3 = forgot password
  const [user, setUser]     = useState('');
  const [pass, setPass]     = useState('');
  const [remember, setRemember] = useState(false);
  const [code, setCode]     = useState('');
  const [partial, setPartial] = useState(null);
  const [busy, setBusy]     = useState(false);
  const [err, setErr]       = useState('');
  // Forgot-password sub-state (step=3)
  const [forgotEmail, setForgotEmail] = useState('');
  const [forgotMsg, setForgotMsg]     = useState('');

  // Branding pode estar carregando ainda — fallback pros defaults.
  const brand = branding || { brand_name: 'SINTRACON-SP', brand_initial: 'S', brand_logo_path: '' };
  const brandLogoSrc = brand.brand_logo_path
    ? (brand.brand_logo_path.startsWith('http') || brand.brand_logo_path.startsWith('/')
        ? brand.brand_logo_path
        : '/' + brand.brand_logo_path)
    : null;

  const submitCredentials = (e) => {
    e.preventDefault();
    if (!user || !pass) return;
    setBusy(true); setErr('');
    fetch('/api/admin/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // envia/recebe cookie trusted_device
      body: JSON.stringify({ user, password: pass, remember_me: remember }),
    })
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(j.error || 'Credenciais inválidas');
        if (j.requires_2fa && j.partial_token) {
          // Sobe pra step 2 — usuário tem 5min pra digitar o código.
          setPartial(j.partial_token);
          setStep(2);
          setBusy(false);
          return;
        }
        if (j.token) {
          Auth.token = j.token;
          onAuthed();
          return;
        }
        throw new Error('Resposta inesperada do servidor');
      })
      .catch((e) => { setErr(e.message); setBusy(false); });
  };

  const submit2fa = (e) => {
    e.preventDefault();
    if (!code) return;
    setBusy(true); setErr('');
    fetch('/api/admin/login/2fa', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ partial_token: partial, code: code.trim() }),
    })
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        if (!r.ok || !j.token) throw new Error(j.error || 'Código inválido');
        Auth.token = j.token;
        onAuthed();
      })
      .catch((e) => { setErr(e.message); setBusy(false); });
  };

  const backToCreds = () => {
    setStep(1); setCode(''); setPartial(null); setErr(''); setBusy(false);
    setForgotEmail(''); setForgotMsg('');
  };

  const submitForgot = (e) => {
    e.preventDefault();
    if (!forgotEmail) return;
    setBusy(true); setErr(''); setForgotMsg('');
    fetch('/api/auth/forgot-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: forgotEmail }),
    })
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        setForgotMsg(j.message || 'Se o e-mail corresponder a uma conta, um link foi enviado.');
        setBusy(false);
      })
      .catch(() => {
        // Mesmo se 500, mostra mensagem genérica (não vaza enumeração).
        setForgotMsg('Se o e-mail corresponder a uma conta, um link foi enviado.');
        setBusy(false);
      });
  };

  return (
    <div className="login-overlay">
      <div className="login-box">
        <div className="login-brand">
          <div className={"login-mark" + (brandLogoSrc ? " has-logo" : "")}>
            {brandLogoSrc
              ? <img src={brandLogoSrc} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 'inherit' }} />
              : (brand.brand_initial || 'S').charAt(0).toUpperCase()}
          </div>
          <div>
            <h1>{brand.brand_name || 'SINTRACON-SP'} · Painel Admin</h1>
            <div className="sub">
              {step === 1
                ? 'Acesso restrito — informe suas credenciais'
                : 'Verificação em 2 etapas'}
            </div>
          </div>
        </div>

        {step === 1 && (
          <form onSubmit={submitCredentials}>
            <div className="field">
              <label><i className="fas fa-user"></i> Usuário</label>
              <input className="input" value={user} onChange={(e) => setUser(e.target.value)} autoFocus required autoComplete="username" />
            </div>
            <div className="field">
              <label><i className="fas fa-key"></i> Senha</label>
              <input className="input" type="password" value={pass} onChange={(e) => setPass(e.target.value)} required autoComplete="current-password" />
            </div>
            <div className="field" style={{ marginTop: -4 }}>
              <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontWeight: 'normal', fontSize: 12, color: 'var(--t2)', cursor: 'pointer' }}>
                <input type="checkbox" checked={remember} onChange={(e) => setRemember(e.target.checked)} style={{ width: 16, height: 16 }} />
                <span>Lembrar deste dispositivo por 30 dias</span>
              </label>
              <span className="hint" style={{ fontSize: 10.5, marginTop: 2 }}>
                Pula o 2º fator nesse navegador. Não marque em computadores compartilhados.
              </span>
            </div>
            {err && <div className="login-error">{err}</div>}
            <button type="submit" className="btn btn-primary" disabled={busy} style={{ justifyContent: 'center' }}>
              {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Entrando…</> : <><i className="fas fa-sign-in-alt"></i> Entrar</>}
            </button>
            <div style={{ textAlign: 'center', marginTop: 10 }}>
              <button
                type="button"
                onClick={() => { setStep(3); setErr(''); setForgotMsg(''); }}
                style={{ background: 'transparent', border: 'none', color: 'var(--or)', fontSize: 12, cursor: 'pointer', padding: 4 }}
              >
                Esqueci minha senha
              </button>
            </div>
          </form>
        )}

        {step === 2 && (
          <form onSubmit={submit2fa}>
            <div style={{ padding: '10px 12px', background: 'var(--bg1)', border: '1px solid var(--b1)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t2)', marginBottom: 12 }}>
              <i className="fas fa-shield-halved" style={{ color: 'var(--or)', marginRight: 6 }}></i>
              <strong>Confirmação 2FA exigida.</strong> Abra o app authenticator (Google Authenticator, Authy, etc.) e digite o código atual.
            </div>
            <div className="field">
              <label><i className="fas fa-mobile-screen"></i> Código de 6 dígitos (ou backup XXXX-XXXX)</label>
              <input
                className="input"
                value={code}
                onChange={(e) => setCode(e.target.value)}
                autoFocus
                required
                autoComplete="one-time-code"
                inputMode="text"
                placeholder="123456 ou ABCD-EFGH"
                style={{ fontFamily: 'var(--ff-mono)', fontSize: 16, letterSpacing: 2 }}
                maxLength={20}
              />
              <span className="hint" style={{ fontSize: 10.5 }}>
                Sem acesso ao authenticator? Use um código de backup gerado quando você ativou 2FA.
              </span>
            </div>
            {err && <div className="login-error">{err}</div>}
            <button type="submit" className="btn btn-primary" disabled={busy} style={{ justifyContent: 'center' }}>
              {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Verificando…</> : <><i className="fas fa-check"></i> Confirmar</>}
            </button>
            <button type="button" className="btn btn-ghost" onClick={backToCreds} style={{ justifyContent: 'center', marginTop: 8 }}>
              <i className="fas fa-arrow-left"></i> Voltar
            </button>
          </form>
        )}

        {step === 3 && (
          <form onSubmit={submitForgot}>
            <div style={{ padding: '10px 12px', background: 'var(--bg1)', border: '1px solid var(--b1)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t2)', marginBottom: 12 }}>
              <i className="fas fa-envelope" style={{ color: 'var(--or)', marginRight: 6 }}></i>
              <strong>Recuperar senha.</strong> Informe seu e-mail cadastrado. Se houver conta, enviaremos um link pra redefinir a senha.
            </div>
            <div className="field">
              <label><i className="fas fa-at"></i> E-mail</label>
              <input
                type="email"
                className="input"
                value={forgotEmail}
                onChange={(e) => setForgotEmail(e.target.value)}
                autoFocus
                required
                placeholder="voce@empresa.com"
                autoComplete="email"
              />
            </div>
            {forgotMsg && (
              <div style={{ padding: '8px 12px', background: 'rgba(34,197,94,0.08)', border: '1px solid var(--gr)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t1)', marginBottom: 8 }}>
                <i className="fas fa-check-circle" style={{ color: 'var(--gr)', marginRight: 6 }}></i>
                {forgotMsg}
              </div>
            )}
            <button type="submit" className="btn btn-primary" disabled={busy || !!forgotMsg} style={{ justifyContent: 'center' }}>
              {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Enviando…</> : forgotMsg ? <><i className="fas fa-check"></i> Enviado</> : <><i className="fas fa-paper-plane"></i> Enviar link</>}
            </button>
            <button type="button" className="btn btn-ghost" onClick={backToCreds} style={{ justifyContent: 'center', marginTop: 8 }}>
              <i className="fas fa-arrow-left"></i> Voltar pra login
            </button>
          </form>
        )}
      </div>
    </div>
  );
}

// ---- Generic modal for add-context / upload / add-page / add-redirect ----
function SimpleModal({ kind, onClose }) {
  const toast = useToast();
  const config = {
    "add-context": {
      title: "Novo contexto direto", icon: "fa-brain",
      fields: [
        { id: "kind", label: "Tipo", type: "select", default: "informacao",
          options: [
            { value: "informacao", label: "Informação (um fato pra IA responder)" },
            { value: "diretriz", label: "Diretriz de atendimento (como conduzir a conversa)" },
          ],
          hint: "Informação = um dado autoritativo (ex.: 'a mensalidade é R$40'). Diretriz = uma regra de COMO responder certos pedidos (ex.: pedido de cancelamento → primeiro perguntar o motivo, depois mostrar benefícios). A diretriz é seguida como fluxo, nunca aparece na resposta." },
        { id: "title", label: "Título", type: "text", placeholder: "Ex.: Fluxo de cancelamento da mensalidade" },
        { id: "body", label: "Conteúdo", type: "textarea", placeholder: "Informação: o texto/fato. Diretriz: o passo a passo de como a IA deve conduzir (o que perguntar primeiro, o que apresentar, quando entregar a solução).", rows: 8 },
        { id: "tag", label: "Tags (vírgula)", type: "text", placeholder: "cancelar, oposição, sair de sócio, parar de pagar", hint: "Quando a pergunta contém uma tag, este contexto/diretriz entra no prompt." },
        { id: "expires_at", label: "Data de expiração (opcional)", type: "datetime-local", hint: "Quando essa data passar, é desativado automaticamente. Útil pra avisos sazonais. Vazio = nunca expira." },
      ],
      switches: [{ id: "always_inject", label: "Sempre injetar (ignora tags)", on: false }, { id: "enabled", label: "Ativo", on: true }],
      submitLabel: "Salvar contexto",
      submit: (vals) => {
        // Converte datetime-local local-time pra ISO UTC. Vazio = null (não expira).
        const body = { ...vals };
        body.expires_at = vals.expires_at ? new Date(vals.expires_at).toISOString() : null;
        return api('/api/admin/direct-contexts', { method: 'POST', body });
      },
    },
    "upload": {
      title: "Adicionar conteúdo", icon: "fa-cloud-upload-alt",
      tabbed: true,
      submitLabel: "Enviar e indexar",
    },
    "add-page": {
      title: "Cadastrar nova página", icon: "fa-globe",
      fields: [
        { id: "url", label: "URL completa", type: "url", placeholder: "https://sintracon.org.br/filiacao" },
        { id: "title", label: "Título (opcional)", type: "text", placeholder: "Ex.: Filie-se ao SINTRACON" },
        { id: "description", label: "Descrição / intuito da página", type: "textarea-long", rows: 10, placeholder: "Descreva detalhadamente o que tem na página, quais perguntas ela responde, em que contexto a IA deve recomendá-la, palavras-chave importantes, exemplos…", hint: "Texto rico ajuda a IA a recomendar a página certa. Mínimo 10 caracteres, sem limite prático (até 20.000)." },
      ],
      submitLabel: "Cadastrar página",
      submit: (vals) => api('/api/admin/site-pages', { method: 'POST', body: vals }),
    },
    "add-redirect": {
      title: "Novo canal de atendimento", icon: "fa-headset",
      fields: [
        { id: "channel", label: "Tipo", type: "select", options: ["whatsapp", "phone", "email", "rdstation", "link"] },
        { id: "label", label: "Rótulo (botão)", type: "text", placeholder: "Ex.: Falar pelo WhatsApp" },
        { id: "url", label: "URL completa", type: "url", placeholder: "https://wa.me/55...", hint: "WhatsApp: wa.me/55... · Telefone: tel:+55... · E-mail: mailto:..." },
      ],
      submitLabel: "Salvar canal",
      submit: (vals) => api('/api/admin/support-redirects', { method: 'POST', body: vals }),
    },
  }[kind];

  if (!config) return null;

  // Modal "upload" tem comportamento próprio (abas arquivo vs texto digitado)
  if (config.tabbed) return <UploadModal title={config.title} icon={config.icon} onClose={onClose} />;

  const [vals, setVals] = useState(() => {
    const v = {};
    (config.fields || []).forEach((f) => {
      if (f.type === 'select') {
        const first = f.options[0];
        v[f.id] = first == null ? '' : (typeof first === 'object' ? first.value : first);
      } else {
        v[f.id] = f.default != null ? f.default : '';
      }
    });
    (config.switches || []).forEach((s) => { v[s.id] = !!s.on; });
    return v;
  });
  const [files, setFiles] = useState({});
  const [busy, setBusy] = useState(false);
  const [aiSuggesting, setAiSuggesting] = useState(false);

  const setVal = (k, v) => setVals((s) => ({ ...s, [k]: v }));

  // Sugestão de descrição via IA — só pra kind="add-page". Faz fetch da URL
  // no backend, manda pro LLM e popula title + description com a sugestão.
  // Admin revisa antes de submeter — o submit final continua via fluxo normal.
  const suggestWithAi = async () => {
    const url = (vals.url || '').trim();
    if (!url || !/^https?:\/\//i.test(url)) {
      toast({ title: 'Preencha a URL primeiro', msg: 'Informe uma URL http(s) válida pra IA analisar.', kind: 'info' });
      return;
    }
    setAiSuggesting(true);
    try {
      const r = await api('/api/admin/site-pages/ai-suggest', { method: 'POST', body: { url } });
      if (r && r.description) {
        setVals((s) => ({
          ...s,
          // Só preenche título se o admin ainda não digitou nada.
          title: s.title && s.title.trim() ? s.title : (r.title || s.title),
          description: r.description,
        }));
        toast({ title: 'Descrição gerada pela IA', msg: 'Revise e ajuste antes de salvar.', kind: 'success' });
      } else {
        toast({ title: 'IA não retornou descrição', kind: 'error' });
      }
    } catch (err) {
      toast({ title: 'Falha ao analisar URL', msg: err.message, kind: 'error' });
    } finally {
      setAiSuggesting(false);
    }
  };

  const onSubmit = (e) => {
    e.preventDefault();
    setBusy(true);
    config.submit(vals, files)
      .then(() => {
        toast({ title: "Salvo!", msg: config.title + " adicionado com sucesso.", kind: "success" });
        onClose();
      })
      .catch((err) => {
        toast({ title: "Erro ao salvar", msg: err.message, kind: "error" });
        setBusy(false);
      });
  };

  const hasLongTextarea = (config.fields || []).some((f) => f.type === 'textarea-long');

  return (
    <div className="modal-overlay" onClick={onClose}>
      <form className={"modal" + (hasLongTextarea ? ' modal-wide' : '')} onClick={(e) => e.stopPropagation()} onSubmit={onSubmit}>
        <div className="modal-head">
          <h3><i className={"fas " + config.icon}></i> {config.title}</h3>
          <button type="button" className="icon-btn" onClick={onClose}><i className="fas fa-times"></i></button>
        </div>
        <div className="modal-body">
          {config.fields.map((f) => (
            <div className="field" key={f.id}>
              <label>
                {f.label}
                {f.type === 'textarea-long' && (
                  <span style={{ marginLeft: 'auto', fontSize: 10.5, color: 'var(--t3)', fontFamily: 'var(--ff-mono)' }}>
                    {(vals[f.id] || '').length} caracteres
                  </span>
                )}
              </label>
              {f.type === "textarea" || f.type === "textarea-long" ? (
                <textarea
                  className="input"
                  rows={f.rows || 4}
                  placeholder={f.placeholder}
                  value={vals[f.id] || ''}
                  onChange={(e) => setVal(f.id, e.target.value)}
                  style={f.type === 'textarea-long' ? { minHeight: 220 } : undefined}
                />
              ) : f.type === "select" ? (
                <select className="input" value={vals[f.id]} onChange={(e) => setVal(f.id, e.target.value)}>
                  {f.options.map((o) => {
                    const val = typeof o === 'object' ? o.value : o;
                    const lbl = typeof o === 'object' ? o.label : o;
                    return <option key={val} value={val}>{lbl}</option>;
                  })}
                </select>
              ) : (
                <input
                  className="input"
                  type={f.type}
                  placeholder={f.placeholder}
                  value={vals[f.id] || ''}
                  onChange={(e) => setVal(f.id, e.target.value)}
                />
              )}
              {f.hint && <span className="hint">{f.hint}</span>}
              {/* Botão IA pro modal de páginas — aparece logo após o campo URL.
                  Faz fetch da página + pede pro LLM gerar título + descrição. */}
              {kind === 'add-page' && f.id === 'url' && (
                <div style={{ marginTop: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
                  <button
                    type="button"
                    className="btn btn-soft btn-sm"
                    onClick={suggestWithAi}
                    disabled={aiSuggesting || !vals.url}
                    title="Faz fetch da URL, extrai o conteúdo e pede pra IA gerar uma descrição"
                  >
                    <i className={'fas ' + (aiSuggesting ? 'fa-circle-notch fa-spin' : 'fa-wand-magic-sparkles')} style={{ color: aiSuggesting ? undefined : 'var(--or)' }}></i>{' '}
                    {aiSuggesting ? 'Analisando página…' : 'Gerar descrição com IA'}
                  </button>
                  <span className="hint" style={{ margin: 0 }}>
                    Opcional — popula título + descrição abaixo com base no conteúdo real da URL.
                  </span>
                </div>
              )}
            </div>
          ))}
          {config.switches && (
            <div className="row gap-md" style={{ paddingTop: 4 }}>
              {config.switches.map((s) => (
                <Switch
                  key={s.id}
                  on={vals[s.id]}
                  onChange={(v) => setVal(s.id, v)}
                  label={s.label}
                />
              ))}
            </div>
          )}
        </div>
        <div className="modal-foot">
          <button type="button" className="btn btn-ghost" onClick={onClose}>Cancelar</button>
          <button type="submit" className="btn btn-primary" disabled={busy}>
            {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Enviando…</> : <><i className="fas fa-save"></i> {config.submitLabel}</>}
          </button>
        </div>
      </form>
    </div>
  );
}

// ---- Upload modal com abas Arquivo vs Texto digitado ---------------------
function UploadModal({ title, icon, onClose }) {
  const toast = useToast();
  const [mode, setMode] = useState('file');  // 'file' | 'text'
  const [vals, setVals] = useState({ title: '', context: '', text: '' });
  const [file, setFile] = useState(null);
  const [busy, setBusy] = useState(false);
  const [showAIImprove, setShowAIImprove] = useState(false);
  const setV = (k, v) => setVals((s) => ({ ...s, [k]: v }));

  const submit = (e) => {
    e.preventDefault();
    setBusy(true);
    if (mode === 'file') {
      if (!file) { toast({ title: "Escolha um arquivo", kind: "info" }); setBusy(false); return; }
      const fd = new FormData();
      fd.append('file', file);
      if (vals.title) fd.append('title', vals.title);
      if (vals.context) fd.append('context', vals.context);
      api('/api/admin/upload', { method: 'POST', body: fd })
        .then(() => { toast({ title: "Arquivo enviado", msg: "Indexação em andamento…", kind: "success" }); onClose(); })
        .catch((e) => { toast({ title: "Falha no upload", msg: e.message, kind: "error" }); setBusy(false); });
    } else {
      if (!vals.title.trim()) { toast({ title: "Título obrigatório", kind: "info" }); setBusy(false); return; }
      if (vals.text.trim().length < 20) { toast({ title: "Texto curto demais", msg: "Mínimo 20 caracteres.", kind: "info" }); setBusy(false); return; }
      api('/api/admin/files/text', { method: 'POST', body: { title: vals.title, content: vals.text } })
        .then(() => { toast({ title: "Texto enfileirado", msg: "Indexação em andamento…", kind: "success" }); onClose(); })
        .catch((e) => { toast({ title: "Falha ao enviar", msg: e.message, kind: "error" }); setBusy(false); });
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <form className="modal modal-wide" onClick={(e) => e.stopPropagation()} onSubmit={submit}>
        <div className="modal-head">
          <h3><i className={"fas " + icon}></i> {title}</h3>
          <button type="button" className="icon-btn" onClick={onClose}><i className="fas fa-times"></i></button>
        </div>
        <div className="modal-body">
          <div className="mode-tabs">
            <button type="button" className={mode === 'file' ? 'active' : ''} onClick={() => setMode('file')}>
              <i className="fas fa-file-upload"></i> Arquivo
            </button>
            <button type="button" className={mode === 'text' ? 'active' : ''} onClick={() => setMode('text')}>
              <i className="fas fa-align-left"></i> Texto digitado
            </button>
          </div>

          <div className="field">
            <label>Título {mode === 'text' ? '' : '(opcional)'}</label>
            <input
              className="input"
              placeholder={mode === 'text' ? 'Ex.: Comunicado interno 2026-05' : 'Ex.: Estatuto Social 2024'}
              value={vals.title}
              onChange={(e) => setV('title', e.target.value)}
              required={mode === 'text'}
            />
          </div>

          {mode === 'file' ? (
            <>
              <div className="field">
                <label>Arquivo (PDF, DOCX, TXT, MD, JPG, PNG)</label>
                <label style={{
                  padding: 28, border: "2px dashed var(--b2)", borderRadius: "var(--r-input)",
                  textAlign: "center", color: "var(--t3)", background: "var(--bg1)",
                  cursor: "pointer", display: "block",
                }}>
                  <i className="fas fa-cloud-upload-alt" style={{ fontSize: 24, marginBottom: 8 }}></i>
                  <div style={{ fontSize: 13, color: "var(--t1)" }}>
                    {file ? file.name : 'Solte o arquivo aqui ou clique para selecionar'}
                  </div>
                  <div style={{ fontSize: 11, marginTop: 4 }}>PDF, DOCX, TXT, MD, JPG, PNG · até 25MB</div>
                  <input
                    type="file"
                    accept=".pdf,.docx,.txt,.md,.markdown,.jpg,.jpeg,.png,.gif,.webp,application/pdf,text/plain,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/*"
                    style={{ display: 'none' }}
                    onChange={(e) => setFile(e.target.files[0])}
                  />
                </label>
              </div>
              <div className="field">
                <label>Contexto adicional (opcional)</label>
                <textarea
                  className="input"
                  rows={3}
                  placeholder="Ex.: Este PDF contém o estatuto social do SINTRACON…"
                  value={vals.context}
                  onChange={(e) => setV('context', e.target.value)}
                />
                <span className="hint">Anexado ao texto extraído antes do chunking, ajuda a IA a entender o intuito do arquivo.</span>
              </div>
            </>
          ) : (
            <div className="field">
              <label>
                Conteúdo (texto)
                <span style={{ marginLeft: 'auto', fontSize: 10.5, color: 'var(--t3)', fontFamily: 'var(--ff-mono)' }}>
                  {vals.text.length} caracteres
                </span>
              </label>
              <textarea
                className="input"
                rows={12}
                placeholder="Cole ou digite o conteúdo que a IA deve indexar…"
                value={vals.text}
                onChange={(e) => setV('text', e.target.value)}
                style={{ minHeight: 260 }}
                required
              />
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
                <span className="hint" style={{ flex: 1, margin: 0 }}>Mínimo 20 caracteres. Cada texto vira embeddings que entram na busca semântica do RAG.</span>
                <button
                  type="button"
                  className="btn btn-soft btn-sm"
                  onClick={() => setShowAIImprove(true)}
                  disabled={vals.text.trim().length < 50}
                  title={vals.text.trim().length < 50 ? 'Cole pelo menos 50 caracteres' : 'Pré-processar com IA antes de indexar'}
                >
                  <i className="fas fa-wand-magic-sparkles" style={{ color: 'var(--or)' }}></i> Melhorar com IA
                </button>
              </div>
            </div>
          )}
        </div>
        <div className="modal-foot">
          <button type="button" className="btn btn-ghost" onClick={onClose}>Cancelar</button>
          <button type="submit" className="btn btn-primary" disabled={busy}>
            {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Enviando…</> : <><i className="fas fa-save"></i> {mode === 'file' ? 'Enviar e indexar' : 'Indexar texto'}</>}
          </button>
        </div>
      </form>
      {showAIImprove && (
        <AIImproveModal
          originalText={vals.text}
          sourceHint="text"
          onAccept={(improved) => { setV('text', improved); toast({ title: 'Texto atualizado', msg: 'Pronto pra indexar — revise antes de enviar.', kind: 'success' }); }}
          onClose={() => setShowAIImprove(false)}
        />
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// Helper de diff word-level (LCS) — usado pelo AIImproveModal pra
// destacar visualmente o que mudou entre original e reescrita.
// ═══════════════════════════════════════════════════════════════════════════
// Tokeniza preservando whitespace (split em \s+ mantém os separadores como
// tokens separados — sem isso, "hello world" e "hello  world" virariam
// idênticos visualmente mas o LCS marcaria diferença).
function tokenizeForDiff(text) {
  if (!text) return [];
  return text.split(/(\s+)/).filter((t) => t.length > 0);
}

// LCS-based word diff. Retorna lista de ops { op: 'keep'|'add'|'remove', text }.
// Limite hard: tokens > 4M de combinações → retorna null pra evitar travar o browser.
// Pra docs grandes, UI mostra side-by-side sem highlighting (fallback).
function computeWordDiff(originalText, improvedText) {
  const a = tokenizeForDiff(originalText);
  const b = tokenizeForDiff(improvedText);
  const m = a.length, n = b.length;
  if (m * n > 4_000_000 || m + n > 50_000) return null; // skip pra perf

  // DP table (Int32Array é mais leve que Array of arrays)
  const dp = new Array(m + 1);
  for (let i = 0; i <= m; i++) dp[i] = new Int32Array(n + 1);
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
      else dp[i][j] = dp[i - 1][j] >= dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1];
    }
  }
  // Backtrack pra montar a sequência de ops.
  const ops = [];
  let i = m, j = n;
  while (i > 0 || j > 0) {
    if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
      ops.push({ op: 'keep', text: a[i - 1] });
      i--; j--;
    } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
      ops.push({ op: 'add', text: b[j - 1] });
      j--;
    } else {
      ops.push({ op: 'remove', text: a[i - 1] });
      i--;
    }
  }
  ops.reverse();

  // Contagens pra mostrar stats no header.
  let kept = 0, added = 0, removed = 0;
  for (const o of ops) {
    if (o.op === 'keep') kept++;
    else if (o.op === 'add') added++;
    else removed++;
  }
  return { ops, stats: { kept, added, removed } };
}

function escapeDiffHtml(s) {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

// Renderiza HTML com tokens coloridos. side='left' mostra original com
// remoções vermelhas; side='right' mostra reescrita com adições verdes.
function diffOpsToHtml(ops, side) {
  if (!ops || !ops.length) return '';
  const parts = [];
  for (const o of ops) {
    const escaped = escapeDiffHtml(o.text);
    if (o.op === 'keep') {
      parts.push(escaped);
    } else if (o.op === 'remove' && side === 'left') {
      parts.push('<span style="background:rgba(239,68,68,0.22);color:var(--rd);text-decoration:line-through;">' + escaped + '</span>');
    } else if (o.op === 'add' && side === 'right') {
      parts.push('<span style="background:rgba(34,197,94,0.22);color:var(--gr);font-weight:600;">' + escaped + '</span>');
    }
    // ops "remove" no lado direito e "add" no esquerdo são ocultos por design
    // (cada lado mostra só o que pertence a ele + suas modificações).
  }
  return parts.join('');
}

// ═══════════════════════════════════════════════════════════════════════════
// AIImproveModal — pré-processamento por IA do texto antes de indexar
// ═══════════════════════════════════════════════════════════════════════════
// Modal reutilizável que mostra preview side-by-side (original ⇄ melhorado),
// alerta de alucinações detectadas no passo 3 do pipeline, e permite editar
// o resultado antes de aceitar. Chamado por UploadModal (texto), por
// ContextEditModal (contexto direto), por PageEditModal (descrição de URL),
// e pela linha do FilesPane (reprocessar arquivo já indexado).
//
// Props:
//   - originalText: string  - texto que vai pra IA
//   - sourceHint:   string  - 'text' | 'direct_context' | 'url' | 'pdf' | etc.
//   - fileId:       number? - se passado, backend lê extracted_text e o aceite
//                             chama /apply-file-improvement em vez de só onAccept
//   - filename:     string? - exibido no header pra contexto
//   - onAccept:     (improvedText, fullResult) => void
//                             chamado quando admin clica "Aceitar e usar".
//                             Caller substitui textarea ou re-indexa.
//   - onClose:      () => void
// ═══════════════════════════════════════════════════════════════════════════
function AIImproveModal({ originalText, sourceHint, fileId, filename, onAccept, onClose }) {
  const toast = useToast();
  const [phase, setPhase]       = useState('loading'); // loading | preview | error | applying
  const [result, setResult]     = useState(null);
  const [errorMsg, setErrorMsg] = useState(null);
  const [editedText, setEditedText] = useState('');
  // 'edit' = textareas side-by-side editáveis (default — admin pode corrigir).
  // 'diff' = pre coloridos com palavras removidas em vermelho / adicionadas
  //         em verde. Não editável; útil pra revisar mudanças. Toggle no header.
  const [viewMode, setViewMode] = useState('edit');

  useEffect(() => {
    let alive = true;
    const payload = fileId ? { file_id: fileId, source_hint: sourceHint } : { text: originalText, source_hint: sourceHint, filename };

    // Carrega config + dispara pipeline em paralelo. Quando ambos terminam,
    // se ai_auto_accept_when_clean=true E validação retornou 0 alucinações
    // E 0 fatos removidos, aceita auto sem mostrar preview.
    Promise.all([
      api('/api/admin/ai/improve-content', { method: 'POST', body: payload }),
      api('/api/admin/system-config').catch(() => ({})),
    ])
      .then(([r, cfg]) => {
        if (!alive) return;
        if (!r || !r.improved) throw new Error(r && r.error ? r.error : 'Resposta inesperada da IA');
        const autoAccept = cfg && cfg.ai_auto_accept_when_clean === 'true';
        const v = r.validation || {};
        const isClean = (!v.alucinacoes || v.alucinacoes.length === 0)
                     && (!v.fatos_removidos || v.fatos_removidos.length === 0)
                     && !v._parse_error;

        setResult(r);
        setEditedText(r.improved);

        if (autoAccept && isClean) {
          // Auto-aceite: dispatch onAccept (caller substitui textarea ou
          // chama /apply-file-improvement) e fecha modal. Toast curto avisa.
          // Pra fileId, ainda assim mostra "applying" porque o re-index leva tempo.
          toast({ title: 'IA aplicada automaticamente', msg: 'Validação limpa (0 alucinações). Pra revisar manualmente, desabilite o toggle em Sistema.', kind: 'success' });
          if (fileId) {
            // Aplica direto no backend
            api('/api/admin/ai/apply-file-improvement/' + fileId, {
              method: 'POST',
              body: { improved_text: r.improved, improvement_log: { analysis: r.analysis, validation: r.validation, usage: r.usage, elapsed_ms: r.elapsed_ms, auto_accepted: true } },
            })
              .then(() => { if (typeof onAccept === 'function') onAccept(r.improved, r); onClose(); })
              .catch((e) => { toast({ title: 'Auto-aceite falhou', msg: e.message, kind: 'error' }); setPhase('preview'); });
            setPhase('applying');
          } else {
            if (typeof onAccept === 'function') onAccept(r.improved, r);
            onClose();
          }
        } else {
          setPhase('preview');
        }
      })
      .catch((e) => {
        if (!alive) return;
        setErrorMsg(e.message || 'Falha ao processar com IA');
        setPhase('error');
      });
    return () => { alive = false; };
  }, [originalText, fileId, sourceHint]);

  // Memoizado no corpo do componente (não dentro de renderPreview) pra cumprir
  // Rules of Hooks: useMemo precisa rodar em TODO render, não só quando o
  // preview é exibido. Retorna null enquanto result ainda é null (phase=loading).
  const diffResult = useMemo(() => {
    if (!result || !result.original) return null;
    return computeWordDiff(result.original, editedText);
  }, [result && result.original, editedText]);

  const accept = async () => {
    if (!editedText || editedText.trim().length < 50) {
      toast({ title: 'Texto muito curto', msg: 'Mínimo 50 caracteres.', kind: 'info' });
      return;
    }
    // Caso 1: reprocessamento de arquivo — backend aplica + re-indexa.
    if (fileId) {
      setPhase('applying');
      try {
        await api('/api/admin/ai/apply-file-improvement/' + fileId, {
          method: 'POST',
          body: { improved_text: editedText, improvement_log: { analysis: result.analysis, validation: result.validation, usage: result.usage, elapsed_ms: result.elapsed_ms } },
        });
        toast({ title: 'Melhoria aplicada', msg: 'Re-indexação em andamento — atualize a lista em alguns segundos.', kind: 'success' });
        if (typeof onAccept === 'function') onAccept(editedText, result);
        onClose();
      } catch (e) {
        toast({ title: 'Falha ao aplicar', msg: e.message, kind: 'error' });
        setPhase('preview');
      }
      return;
    }
    // Caso 2: contexto/URL/texto em modal — caller substitui o textarea.
    if (typeof onAccept === 'function') onAccept(editedText, result);
    onClose();
  };

  const renderLoading = () => (
    <div style={{ padding: 60, textAlign: 'center', color: 'var(--t2)' }}>
      <i className="fas fa-circle-notch fa-spin" style={{ fontSize: 36, color: 'var(--or)', marginBottom: 16 }}></i>
      <h4 style={{ margin: 0, marginBottom: 4 }}>Analisando conteúdo com IA…</h4>
      <p style={{ fontSize: 13, color: 'var(--t3)' }}>
        Pipeline 3-passos: análise → reescrita → validação de fatos.<br />
        Leva entre 5 e 30 segundos dependendo do tamanho do texto.
      </p>
    </div>
  );

  const renderError = () => (
    <div style={{ padding: 40, textAlign: 'center' }}>
      <i className="fas fa-exclamation-triangle" style={{ fontSize: 36, color: 'var(--rd)', marginBottom: 12 }}></i>
      <h4 style={{ margin: 0, marginBottom: 8 }}>Falha ao melhorar</h4>
      <p style={{ fontSize: 13, color: 'var(--t2)' }}>{errorMsg}</p>
      <button className="btn btn-soft" style={{ marginTop: 16 }} onClick={() => { setPhase('loading'); setErrorMsg(null); }}>
        <i className="fas fa-redo"></i> Tentar de novo
      </button>
    </div>
  );

  const renderPreview = () => {
    const v = (result && result.validation) || { alucinacoes: [], fatos_removidos: [], summary: '' };
    const a = (result && result.analysis) || {};
    const hasAlucinacoes = Array.isArray(v.alucinacoes) && v.alucinacoes.length > 0;
    const hasRemovidos   = Array.isArray(v.fatos_removidos) && v.fatos_removidos.length > 0;
    const tokensIn  = (result && result.usage && result.usage.total_input_tokens) || 0;
    const tokensOut = (result && result.usage && result.usage.total_output_tokens) || 0;
    const elapsed   = (result && result.elapsed_ms) || 0;
    const costBrl  = (result && result.cost_brl) || 0;
    const costUsd  = (result && result.cost_usd) || 0;

    // diffResult vem do useMemo no corpo do componente (Rules of Hooks).
    // Pra textos enormes (>50k tokens), computeWordDiff retorna null e a UI
    // fica sem highlighting (fallback).
    const diffStats = diffResult ? diffResult.stats : null;
    const canShowDiff = diffResult !== null;

    return (
      <>
        {/* Resumo da análise + custo + toggle de visualização */}
        <div style={{ display: 'flex', gap: 12, padding: 10, background: 'var(--bg1)', border: '1px solid var(--b1)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t2)', flexWrap: 'wrap', marginBottom: 12, alignItems: 'center' }}>
          <span><i className="fas fa-tag" style={{ color: 'var(--or)', marginRight: 4 }}></i><strong>Tipo:</strong> {a.tipo || '—'}</span>
          <span><i className="fas fa-list-check" style={{ color: 'var(--bl)', marginRight: 4 }}></i><strong>Fatos preservados:</strong> {(a.fatos_preservar || []).length}</span>
          <span><i className="fas fa-broom" style={{ color: 'var(--gr)', marginRight: 4 }}></i><strong>Boilerplate removido:</strong> {(a.boilerplate_a_remover || []).length}</span>
          {diffStats && (
            <span title={`${diffStats.added} palavras adicionadas, ${diffStats.removed} removidas, ${diffStats.kept} preservadas`}>
              <i className="fas fa-code-compare" style={{ color: 'var(--bl)', marginRight: 4 }}></i>
              <strong>+{diffStats.added}</strong>
              <span style={{ color: 'var(--rd)', marginLeft: 4 }}>−{diffStats.removed}</span>
            </span>
          )}
          <span style={{ marginLeft: 'auto', color: 'var(--t3)', display: 'flex', alignItems: 'center', gap: 8 }}>
            {elapsed > 0 && <span><i className="fas fa-clock" style={{ marginRight: 4 }}></i>{(elapsed / 1000).toFixed(1)}s</span>}
            {tokensIn > 0 && <span><i className="fas fa-coins" style={{ marginRight: 4 }}></i>{(tokensIn + tokensOut).toLocaleString('pt-BR')} tokens</span>}
            {costBrl > 0 && (
              <span title={'≈ US$ ' + costUsd.toFixed(6)} style={{ color: costBrl < 0.05 ? 'var(--gr)' : 'var(--t2)', fontWeight: 600 }}>
                <i className="fas fa-coins" style={{ marginRight: 4, color: 'var(--am)' }}></i>
                R$ {costBrl.toFixed(4)}
              </span>
            )}
            {canShowDiff && (
              <button
                type="button"
                className="btn btn-soft btn-sm"
                onClick={() => setViewMode(viewMode === 'diff' ? 'edit' : 'diff')}
                style={{ marginLeft: 8 }}
                title={viewMode === 'diff' ? 'Voltar pra modo edição' : 'Ver diferenças coloridas'}
              >
                <i className={'fas ' + (viewMode === 'diff' ? 'fa-pencil' : 'fa-code-compare')}></i>
                {' '}{viewMode === 'diff' ? 'Editar' : 'Ver diff'}
              </button>
            )}
          </span>
        </div>

        {/* Alerta de alucinações */}
        {hasAlucinacoes && (
          <div style={{ padding: '10px 14px', background: 'rgba(239,68,68,0.08)', border: '1px solid var(--rd)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t1)', marginBottom: 12 }}>
            <strong style={{ color: 'var(--rd)' }}>
              <i className="fas fa-exclamation-triangle"></i> {v.alucinacoes.length} possível{v.alucinacoes.length === 1 ? '' : 'eis'} alucinação{v.alucinacoes.length === 1 ? '' : 'ões'} detectada{v.alucinacoes.length === 1 ? '' : 's'}:
            </strong>
            <ul style={{ margin: '6px 0 0 20px', padding: 0, fontSize: 11, color: 'var(--t2)' }}>
              {v.alucinacoes.slice(0, 5).map((al, i) => (
                <li key={i}><code style={{ background: 'var(--bg2)', padding: '0 4px', borderRadius: 2 }}>{al.categoria || '?'}</code> {al.fato_inventado}</li>
              ))}
            </ul>
            <div style={{ marginTop: 6, color: 'var(--t3)', fontSize: 11 }}>
              Edite o texto da direita antes de aceitar pra remover/corrigir esses trechos.
            </div>
          </div>
        )}
        {hasRemovidos && (
          <div style={{ padding: '10px 14px', background: 'rgba(245,158,11,0.08)', border: '1px solid var(--am)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t1)', marginBottom: 12 }}>
            <strong style={{ color: 'var(--am)' }}>
              <i className="fas fa-exclamation-circle"></i> {v.fatos_removidos.length} fato{v.fatos_removidos.length === 1 ? '' : 's'} importante{v.fatos_removidos.length === 1 ? '' : 's'} pode{v.fatos_removidos.length === 1 ? '' : 'm'} ter sumido:
            </strong>
            <ul style={{ margin: '6px 0 0 20px', padding: 0, fontSize: 11, color: 'var(--t2)' }}>
              {v.fatos_removidos.slice(0, 5).map((fr, i) => (
                <li key={i}><code style={{ background: 'var(--bg2)', padding: '0 4px', borderRadius: 2 }}>{fr.categoria || '?'}</code> {fr.fato}</li>
              ))}
            </ul>
          </div>
        )}
        {!hasAlucinacoes && !hasRemovidos && (
          <div style={{ padding: '8px 14px', background: 'rgba(34,197,94,0.08)', border: '1px solid var(--gr)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t2)', marginBottom: 12 }}>
            <i className="fas fa-check-circle" style={{ color: 'var(--gr)', marginRight: 6 }}></i>
            <strong style={{ color: 'var(--gr)' }}>Validação OK:</strong> sem alucinações detectadas. {v.summary || ''}
          </div>
        )}

        {/* Side-by-side: original (readonly) | melhorado (editável OU diff colorido) */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
          <div>
            <label style={{ fontSize: 11, color: 'var(--t3)', marginBottom: 4, display: 'block' }}>
              <i className="fas fa-file-alt"></i> Original ({(result.original || '').length} chars)
              {viewMode === 'diff' && diffResult && (
                <span style={{ marginLeft: 8, color: 'var(--rd)', fontSize: 10 }}>
                  · vermelho riscado = removido pela IA
                </span>
              )}
            </label>
            {viewMode === 'diff' && diffResult ? (
              <pre
                className="input"
                style={{
                  minHeight: 320, maxHeight: 480, overflowY: 'auto',
                  fontFamily: 'var(--ff-mono)', fontSize: 11.5, lineHeight: 1.55,
                  background: 'var(--bg1)', color: 'var(--t2)',
                  whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0,
                }}
                dangerouslySetInnerHTML={{ __html: diffOpsToHtml(diffResult.ops, 'left') }}
              />
            ) : (
              <textarea
                className="input"
                value={result.original}
                readOnly
                style={{ minHeight: 320, fontFamily: 'var(--ff-mono)', fontSize: 11.5, lineHeight: 1.5, background: 'var(--bg1)', color: 'var(--t2)' }}
              />
            )}
          </div>
          <div>
            <label style={{ fontSize: 11, color: 'var(--or)', marginBottom: 4, display: 'block' }}>
              <i className="fas fa-wand-magic-sparkles"></i> Melhorado por IA ({(editedText || '').length} chars)
              {viewMode === 'edit' && ' — editável'}
              {viewMode === 'diff' && diffResult && (
                <span style={{ marginLeft: 8, color: 'var(--gr)', fontSize: 10 }}>
                  · verde = adicionado pela IA
                </span>
              )}
            </label>
            {viewMode === 'diff' && diffResult ? (
              <pre
                className="input"
                style={{
                  minHeight: 320, maxHeight: 480, overflowY: 'auto',
                  fontFamily: 'var(--ff-mono)', fontSize: 11.5, lineHeight: 1.55,
                  borderColor: 'var(--or)', color: 'var(--t1)',
                  whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0,
                }}
                dangerouslySetInnerHTML={{ __html: diffOpsToHtml(diffResult.ops, 'right') }}
              />
            ) : (
              <textarea
                className="input"
                value={editedText}
                onChange={(e) => setEditedText(e.target.value)}
                style={{ minHeight: 320, fontFamily: 'var(--ff-mono)', fontSize: 11.5, lineHeight: 1.5, borderColor: 'var(--or)' }}
              />
            )}
          </div>
        </div>
        {viewMode === 'diff' && !diffResult && (
          <div style={{ padding: '6px 10px', background: 'rgba(245,158,11,0.08)', border: '1px solid var(--am)', borderRadius: 4, fontSize: 11, color: 'var(--t2)', marginBottom: 8 }}>
            <i className="fas fa-info-circle" style={{ color: 'var(--am)', marginRight: 4 }}></i>
            Texto muito grande pra calcular diff visual. Usando comparação simples.
          </div>
        )}

        {result.truncated && (
          <div style={{ padding: '6px 10px', background: 'rgba(245,158,11,0.08)', border: '1px solid var(--am)', borderRadius: 4, fontSize: 11, color: 'var(--t2)', marginBottom: 8 }}>
            <i className="fas fa-cut" style={{ color: 'var(--am)', marginRight: 4 }}></i>
            Texto truncado em 80k chars pra processar. Considere dividir docs muito grandes.
          </div>
        )}
      </>
    );
  };

  const renderApplying = () => (
    <div style={{ padding: 60, textAlign: 'center', color: 'var(--t2)' }}>
      <i className="fas fa-circle-notch fa-spin" style={{ fontSize: 36, color: 'var(--gr)', marginBottom: 16 }}></i>
      <h4 style={{ margin: 0 }}>Aplicando melhoria e re-indexando…</h4>
    </div>
  );

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal modal-wide modal-tall" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 1100 }}>
        <div className="modal-head">
          <h3>
            <i className="fas fa-wand-magic-sparkles" style={{ color: 'var(--or)' }}></i> Melhorar conteúdo com IA
            {filename && <span style={{ fontSize: 12, color: 'var(--t3)', marginLeft: 12, fontWeight: 'normal' }}>· {filename}</span>}
          </h3>
          <button type="button" className="icon-btn" onClick={onClose} disabled={phase === 'applying'}>
            <i className="fas fa-times"></i>
          </button>
        </div>
        <div className="modal-body">
          {phase === 'loading'  && renderLoading()}
          {phase === 'error'    && renderError()}
          {phase === 'preview'  && renderPreview()}
          {phase === 'applying' && renderApplying()}
        </div>
        <div className="modal-foot">
          <button type="button" className="btn btn-ghost" onClick={onClose} disabled={phase === 'applying'}>
            {phase === 'preview' ? 'Descartar' : 'Cancelar'}
          </button>
          {phase === 'preview' && (
            <button type="button" className="btn btn-primary" onClick={accept}>
              <i className="fas fa-check"></i> {fileId ? 'Aceitar e re-indexar' : 'Aceitar e usar'}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

// Expõe global pra outras telas (knowledge.jsx) usarem.
window.AIImproveModal = AIImproveModal;

// ═══════════════════════════════════════════════════════════════════════════
// AIBatchModal — processa múltiplos arquivos em lote via /batch-improve
// ═══════════════════════════════════════════════════════════════════════════
// Workflow:
//   1. Modal abre mostrando "X arquivos pendentes". Admin confirma.
//   2. POST /batch-improve { limit }  → backend dispara processamento bg
//   3. Polling GET /batch-status a cada 2s → atualiza progress bar
//   4. Quando state=done, mostra sumário (sucessos/falhas) + botão fechar
//
// Props:
//   - onClose: () => void
//   - onDone:  () => void  (chamado quando batch concluído pra recarregar lista)
// ═══════════════════════════════════════════════════════════════════════════
function AIBatchModal({ onClose, onDone }) {
  const toast = useToast();
  const [phase, setPhase]       = useState('confirm'); // confirm | running | done | error
  const [status, setStatus]     = useState(null);      // payload de /batch-status
  const [limit, setLimit]       = useState(10);
  const [errorMsg, setErrorMsg] = useState(null);

  // Polling do status enquanto rodando.
  useEffect(() => {
    let alive = true;
    const tick = async () => {
      try {
        const r = await api('/api/admin/ai/batch-status');
        if (!alive) return;
        setStatus(r);
        // Se já tem batch rodando do lado servidor (mesmo abrindo o modal
        // pela 1ª vez), assume modo running.
        if (r && r.batch && r.batch.state === 'running' && phase === 'confirm') {
          setPhase('running');
        }
        if (r && r.batch && (r.batch.state === 'done' || r.batch.state === 'failed') && phase === 'running') {
          setPhase('done');
        }
      } catch (e) { /* swallow */ }
    };
    tick(); // primeira leitura imediata
    if (phase === 'running' || phase === 'confirm') {
      const id = setInterval(tick, 2000);
      return () => { alive = false; clearInterval(id); };
    }
    return () => { alive = false; };
  }, [phase]);

  const start = async () => {
    setErrorMsg(null);
    try {
      const r = await api('/api/admin/ai/batch-improve', { method: 'POST', body: { limit } });
      if (!r || !r.batch_id) throw new Error('Falha ao iniciar lote');
      setPhase('running');
      toast({ title: 'Lote iniciado', msg: `Processando ${r.total} arquivo${r.total === 1 ? '' : 's'}…`, kind: 'info' });
    } catch (e) {
      setErrorMsg(e.message);
      setPhase('error');
    }
  };

  const handleClose = () => {
    // Se ainda rodando, avisa que o backend continua em background.
    if (phase === 'running') {
      toast({ title: 'Continua processando', msg: 'O lote continua no servidor. Volte aqui pra ver o resultado.', kind: 'info' });
    }
    if (phase === 'done' && typeof onDone === 'function') onDone();
    onClose();
  };

  const batch = status && status.batch ? status.batch : null;
  const pending = status ? status.pending_count : null;
  const pct = batch && batch.total > 0 ? Math.round((batch.processed / batch.total) * 100) : 0;

  return (
    <div className="modal-overlay" onClick={handleClose}>
      <div className="modal modal-wide" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h3>
            <i className="fas fa-wand-magic-sparkles" style={{ color: 'var(--or)' }}></i> Otimizar acervo com IA (lote)
          </h3>
          <button type="button" className="icon-btn" onClick={handleClose}><i className="fas fa-times"></i></button>
        </div>

        <div className="modal-body">
          {phase === 'confirm' && (
            <>
              {pending === 0 ? (
                <div className="empty" style={{ padding: 30 }}>
                  <i className="fas fa-check-circle" style={{ color: 'var(--gr)', fontSize: 32, marginBottom: 12 }}></i>
                  <p><strong>Acervo todo otimizado!</strong> Não há arquivos pendentes de melhoria por IA.</p>
                  <p style={{ fontSize: 11, color: 'var(--t3)' }}>Quando novos arquivos forem indexados, eles aparecem aqui pra processar.</p>
                </div>
              ) : (
                <>
                  <div style={{ padding: 12, background: 'var(--bg1)', border: '1px solid var(--b1)', borderRadius: 'var(--r-input)', fontSize: 13, marginBottom: 12 }}>
                    <i className="fas fa-info-circle" style={{ color: 'var(--bl)', marginRight: 6 }}></i>
                    <strong>{pending != null ? pending : '…'}</strong> arquivo{pending === 1 ? '' : 's'} ainda não otimizado{pending === 1 ? '' : 's'} pela IA (sem <code>ai_improved_at</code>).
                  </div>
                  <div className="field">
                    <label>Quantidade neste lote (1 a 50)</label>
                    <input
                      type="number"
                      className="input"
                      value={limit}
                      onChange={(e) => setLimit(Math.min(50, Math.max(1, parseInt(e.target.value || '10', 10))))}
                      min={1}
                      max={50}
                    />
                    <span className="hint">
                      Processa sequencialmente — cada arquivo leva 5-30s (pipeline 3-passos).
                      Estime ~{Math.round(limit * 15 / 60)} min total. O backend roda em background; você pode fechar e voltar.
                    </span>
                  </div>
                  <div style={{ padding: '10px 12px', background: 'rgba(245,158,11,0.08)', border: '1px solid var(--am)', borderRadius: 'var(--r-input)', fontSize: 11.5, color: 'var(--t2)' }}>
                    <i className="fas fa-exclamation-triangle" style={{ color: 'var(--am)', marginRight: 6 }}></i>
                    <strong>Atenção:</strong> cada arquivo será re-indexado (texto melhorado vira o novo <code>extracted_text</code>, embeddings refeitos). O texto original fica preservado em <code>original_text</code>. Custo via Gemini ~R$0,003 por arquivo.
                  </div>
                </>
              )}
            </>
          )}

          {phase === 'running' && batch && (
            <>
              <div style={{ textAlign: 'center', padding: '12px 0' }}>
                <i className="fas fa-cogs fa-spin" style={{ fontSize: 32, color: 'var(--or)', marginBottom: 8 }}></i>
                <h4 style={{ margin: 0 }}>{batch.processed} de {batch.total} processados</h4>
                {batch.current && <p style={{ fontSize: 12, color: 'var(--t2)', marginTop: 4 }}>Em andamento: <strong>{batch.current.name}</strong></p>}
              </div>
              <div style={{ height: 10, background: 'var(--bg3)', borderRadius: 5, overflow: 'hidden', marginBottom: 8 }}>
                <div style={{ width: pct + '%', height: '100%', background: 'var(--or)', transition: 'width 0.3s' }}></div>
              </div>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: 'var(--t3)' }}>
                <span><i className="fas fa-check-circle" style={{ color: 'var(--gr)', marginRight: 4 }}></i>{batch.succeeded} OK</span>
                <span>{pct}%</span>
                <span>{batch.failed > 0 && <><i className="fas fa-times-circle" style={{ color: 'var(--rd)', marginRight: 4 }}></i>{batch.failed} falhas</>}</span>
              </div>
            </>
          )}

          {phase === 'done' && batch && (
            <>
              <div style={{ textAlign: 'center', padding: '12px 0' }}>
                <i className={'fas ' + (batch.failed === 0 ? 'fa-check-circle' : 'fa-exclamation-triangle')} style={{ fontSize: 32, color: batch.failed === 0 ? 'var(--gr)' : 'var(--am)', marginBottom: 8 }}></i>
                <h4 style={{ margin: 0 }}>Lote concluído</h4>
                <p style={{ fontSize: 12, color: 'var(--t2)', marginTop: 4 }}>
                  <strong style={{ color: 'var(--gr)' }}>{batch.succeeded} OK</strong>
                  {batch.failed > 0 && <> · <strong style={{ color: 'var(--rd)' }}>{batch.failed} falharam</strong></>}
                  {' de '}{batch.total} arquivos
                </p>
              </div>
              {batch.errors && batch.errors.length > 0 && (
                <div style={{ padding: 10, background: 'rgba(239,68,68,0.06)', border: '1px solid var(--rd)', borderRadius: 'var(--r-input)', fontSize: 11.5, marginTop: 8 }}>
                  <strong style={{ color: 'var(--rd)' }}>Erros:</strong>
                  <ul style={{ margin: '4px 0 0 20px', padding: 0 }}>
                    {batch.errors.slice(0, 8).map((e, i) => (
                      <li key={i}><code style={{ background: 'var(--bg2)', padding: '0 4px' }}>{e.name}</code> — {e.error}</li>
                    ))}
                  </ul>
                </div>
              )}
            </>
          )}

          {phase === 'error' && (
            <div style={{ padding: 30, textAlign: 'center' }}>
              <i className="fas fa-exclamation-triangle" style={{ fontSize: 32, color: 'var(--rd)', marginBottom: 8 }}></i>
              <h4 style={{ margin: 0 }}>Falha ao iniciar lote</h4>
              <p style={{ fontSize: 13, color: 'var(--t2)', marginTop: 8 }}>{errorMsg}</p>
            </div>
          )}
        </div>

        <div className="modal-foot">
          {phase === 'confirm' && pending > 0 && (
            <>
              <button type="button" className="btn btn-ghost" onClick={handleClose}>Cancelar</button>
              <button type="button" className="btn btn-primary" onClick={start}>
                <i className="fas fa-rocket"></i> Iniciar lote ({Math.min(limit, pending)} arquivos)
              </button>
            </>
          )}
          {phase === 'confirm' && pending === 0 && (
            <button type="button" className="btn btn-ghost" onClick={handleClose}>Fechar</button>
          )}
          {phase === 'running' && (
            <button type="button" className="btn btn-ghost" onClick={handleClose}>Fechar (continua em background)</button>
          )}
          {(phase === 'done' || phase === 'error') && (
            <button type="button" className="btn btn-primary" onClick={handleClose}>Fechar</button>
          )}
        </div>
      </div>
    </div>
  );
}

window.AIBatchModal = AIBatchModal;

// ═══════════════════════════════════════════════════════════════════════════
// ResetPasswordScreen — tela pública renderizada quando ?reset_token=X
// ═══════════════════════════════════════════════════════════════════════════
function ResetPasswordScreen({ token, branding, onDone }) {
  const brand = branding || { brand_name: 'SINTRACON-SP', brand_initial: 'S', brand_logo_path: '' };
  const [pw, setPw]       = useState('');
  const [pw2, setPw2]     = useState('');
  const [busy, setBusy]   = useState(false);
  const [err, setErr]     = useState('');
  const [done, setDone]   = useState(false);

  const submit = (e) => {
    e.preventDefault();
    if (pw !== pw2) { setErr('Senhas não conferem'); return; }
    if (pw.length < 8) { setErr('Senha mínima 8 caracteres'); return; }
    setBusy(true); setErr('');
    fetch('/api/auth/reset-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, new_password: pw }),
    })
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(j.error || 'Falha');
        setDone(true); setBusy(false);
      })
      .catch((e) => { setErr(e.message); setBusy(false); });
  };

  return (
    <div className="login-overlay">
      <div className="login-box">
        <div className="login-brand">
          <div className={"login-mark" + (brand.brand_logo_path ? " has-logo" : "")}>
            {brand.brand_logo_path
              ? <img src={brand.brand_logo_path.startsWith('http') ? brand.brand_logo_path : '/' + brand.brand_logo_path} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 'inherit' }} />
              : (brand.brand_initial || 'S').charAt(0).toUpperCase()}
          </div>
          <div>
            <h1>{brand.brand_name || 'SINTRACON-SP'} · Redefinir senha</h1>
            <div className="sub">Escolha uma nova senha pra acessar o painel</div>
          </div>
        </div>

        {done ? (
          <>
            <div style={{ padding: 16, background: 'rgba(34,197,94,0.08)', border: '1px solid var(--gr)', borderRadius: 'var(--r-input)', fontSize: 13, color: 'var(--t1)', marginBottom: 12 }}>
              <i className="fas fa-check-circle" style={{ color: 'var(--gr)', marginRight: 6 }}></i>
              <strong>Senha redefinida.</strong> Faça login com a nova senha.
            </div>
            <button className="btn btn-primary" onClick={onDone} style={{ justifyContent: 'center', width: '100%' }}>
              <i className="fas fa-sign-in-alt"></i> Ir pro login
            </button>
          </>
        ) : (
          <form onSubmit={submit}>
            <div className="field">
              <label><i className="fas fa-key"></i> Nova senha</label>
              <input type="password" className="input" value={pw} onChange={(e) => setPw(e.target.value)} autoFocus required minLength={8} autoComplete="new-password" />
              <span className="hint">Mínimo 8 caracteres.</span>
            </div>
            <div className="field">
              <label><i className="fas fa-key"></i> Confirmar nova senha</label>
              <input type="password" className="input" value={pw2} onChange={(e) => setPw2(e.target.value)} required minLength={8} autoComplete="new-password" />
            </div>
            {err && <div className="login-error">{err}</div>}
            <button type="submit" className="btn btn-primary" disabled={busy} style={{ justifyContent: 'center' }}>
              {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Redefinindo…</> : <><i className="fas fa-key"></i> Redefinir senha</>}
            </button>
          </form>
        )}
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// AcceptInviteScreen — tela pública renderizada quando ?invite_token=X
// ═══════════════════════════════════════════════════════════════════════════
function AcceptInviteScreen({ token, branding, onDone }) {
  const brand = branding || { brand_name: 'SINTRACON-SP', brand_initial: 'S', brand_logo_path: '' };
  const [info, setInfo]   = useState(null); // { username, full_name, email, role }
  const [loadErr, setLoadErr] = useState('');
  const [pw, setPw]       = useState('');
  const [pw2, setPw2]     = useState('');
  const [busy, setBusy]   = useState(false);
  const [err, setErr]     = useState('');
  const [done, setDone]   = useState(false);

  useEffect(() => {
    fetch('/api/auth/invite-info?token=' + encodeURIComponent(token))
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(j.error || 'Convite inválido');
        setInfo(j);
      })
      .catch((e) => setLoadErr(e.message));
  }, [token]);

  const submit = (e) => {
    e.preventDefault();
    if (pw !== pw2) { setErr('Senhas não conferem'); return; }
    if (pw.length < 8) { setErr('Senha mínima 8 caracteres'); return; }
    setBusy(true); setErr('');
    fetch('/api/auth/accept-invite', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, password: pw }),
    })
      .then(async (r) => {
        const j = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(j.error || 'Falha');
        setDone(true); setBusy(false);
      })
      .catch((e) => { setErr(e.message); setBusy(false); });
  };

  return (
    <div className="login-overlay">
      <div className="login-box">
        <div className="login-brand">
          <div className={"login-mark" + (brand.brand_logo_path ? " has-logo" : "")}>
            {brand.brand_logo_path
              ? <img src={brand.brand_logo_path.startsWith('http') ? brand.brand_logo_path : '/' + brand.brand_logo_path} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 'inherit' }} />
              : (brand.brand_initial || 'S').charAt(0).toUpperCase()}
          </div>
          <div>
            <h1>{brand.brand_name || 'SINTRACON-SP'} · Aceitar convite</h1>
            <div className="sub">Bem-vindo! Defina sua senha pra acessar.</div>
          </div>
        </div>

        {loadErr && (
          <>
            <div style={{ padding: 14, background: 'rgba(239,68,68,0.08)', border: '1px solid var(--rd)', borderRadius: 'var(--r-input)', fontSize: 13, color: 'var(--t1)', marginBottom: 12 }}>
              <i className="fas fa-exclamation-triangle" style={{ color: 'var(--rd)', marginRight: 6 }}></i>
              {loadErr}
            </div>
            <button className="btn btn-ghost" onClick={onDone} style={{ justifyContent: 'center', width: '100%' }}>
              <i className="fas fa-arrow-left"></i> Ir pro login
            </button>
          </>
        )}

        {!loadErr && !info && (
          <div style={{ padding: 30, textAlign: 'center', color: 'var(--t3)' }}>
            <i className="fas fa-circle-notch fa-spin"></i> Carregando convite…
          </div>
        )}

        {info && done && (
          <>
            <div style={{ padding: 16, background: 'rgba(34,197,94,0.08)', border: '1px solid var(--gr)', borderRadius: 'var(--r-input)', fontSize: 13, color: 'var(--t1)', marginBottom: 12 }}>
              <i className="fas fa-check-circle" style={{ color: 'var(--gr)', marginRight: 6 }}></i>
              <strong>Convite aceito!</strong> Sua conta <code>@{info.username}</code> está ativa. Faça login com a nova senha.
            </div>
            <button className="btn btn-primary" onClick={onDone} style={{ justifyContent: 'center', width: '100%' }}>
              <i className="fas fa-sign-in-alt"></i> Ir pro login
            </button>
          </>
        )}

        {info && !done && (
          <form onSubmit={submit}>
            <div style={{ padding: '10px 12px', background: 'var(--bg1)', border: '1px solid var(--b1)', borderRadius: 'var(--r-input)', fontSize: 12, color: 'var(--t2)', marginBottom: 12, lineHeight: 1.6 }}>
              <div><i className="fas fa-user" style={{ color: 'var(--or)', marginRight: 4 }}></i> Username: <code>@{info.username}</code></div>
              {info.full_name && <div><i className="fas fa-id-card" style={{ color: 'var(--or)', marginRight: 4 }}></i> Nome: <strong>{info.full_name}</strong></div>}
              {info.email && <div><i className="fas fa-at" style={{ color: 'var(--or)', marginRight: 4 }}></i> E-mail: {info.email}</div>}
              <div><i className="fas fa-user-shield" style={{ color: 'var(--or)', marginRight: 4 }}></i> Papel: <strong>{info.role}</strong></div>
            </div>
            <div className="field">
              <label><i className="fas fa-key"></i> Definir senha</label>
              <input type="password" className="input" value={pw} onChange={(e) => setPw(e.target.value)} autoFocus required minLength={8} autoComplete="new-password" />
              <span className="hint">Mínimo 8 caracteres.</span>
            </div>
            <div className="field">
              <label><i className="fas fa-key"></i> Confirmar senha</label>
              <input type="password" className="input" value={pw2} onChange={(e) => setPw2(e.target.value)} required minLength={8} autoComplete="new-password" />
            </div>
            {err && <div className="login-error">{err}</div>}
            <button type="submit" className="btn btn-primary" disabled={busy} style={{ justifyContent: 'center' }}>
              {busy ? <><i className="fas fa-circle-notch fa-spin"></i> Salvando…</> : <><i className="fas fa-check"></i> Aceitar e definir senha</>}
            </button>
          </form>
        )}
      </div>
    </div>
  );
}

// Boot
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
