// Chat flutuante pra lançamento rápido por texto ou voz.
// Usuário fala/digita "R$ 15 no pastel, 20 no Uber" → IA parseia → confirma → grava.
// Web Speech API (sem servidor) pra captura de voz em pt-BR.

// Quando `embedded=true`, renderiza fullscreen sem FAB (modo "chat-only" via URL ?chat)
function QuickEntryChat({ ws, setWs, accent, embedded = false, isPro = false, showUpgrade, userId }) {
  const [open, setOpen] = React.useState(embedded);
  const [messages, setMessages] = React.useState([]);
  const [input, setInput] = React.useState("");
  const [loading, setLoading] = React.useState(false);
  const [recording, setRecording] = React.useState(false);
  const recognitionRef = React.useRef(null);
  const scrollRef = React.useRef(null);
  const inputRef = React.useRef(null);
  const fileInputRef = React.useRef(null);

  // Auto-scroll a cada mensagem nova
  React.useEffect(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [messages, loading]);

  // Foca o input quando abre
  React.useEffect(() => {
    if (open && inputRef.current && !recording) {
      setTimeout(() => inputRef.current?.focus(), 80);
    }
  }, [open, recording]);

  // Atalho: Esc fecha o chat
  React.useEffect(() => {
    const fn = e => { if (e.key === "Escape" && open && !recording) setOpen(false); };
    window.addEventListener("keydown", fn);
    return () => window.removeEventListener("keydown", fn);
  }, [open, recording]);

  // Voz — Web Speech API (Chrome/Edge têm suporte nativo)
  const startRecording = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) {
      alert("Seu navegador não suporta reconhecimento de voz. Use Chrome ou Edge — ou digite a mensagem.");
      return;
    }
    const rec = new SR();
    rec.lang = "pt-BR";
    rec.interimResults = true;
    rec.continuous = false;
    rec.onresult = (e) => {
      let transcript = "";
      for (let i = e.resultIndex; i < e.results.length; i++) transcript += e.results[i][0].transcript;
      setInput(transcript);
    };
    rec.onend = () => setRecording(false);
    rec.onerror = (e) => { console.error("[Voz] erro:", e.error); setRecording(false); };
    rec.start();
    recognitionRef.current = rec;
    setRecording(true);
  };
  const stopRecording = () => {
    if (recognitionRef.current) recognitionRef.current.stop();
  };

  // Envia mensagem → IA parseia → mostra preview
  const handleSubmit = async () => {
    const text = input.trim();
    if (!text || loading) return;
    // Free: limite de 10 msgs/dia
    if (!isPro && userId) {
      const used = window.PlanilhaPlan.getChatMessagesToday(userId);
      const limit = window.PlanilhaPlan.FREE_LIMITS.chatMessagesPerDay;
      if (used >= limit) {
        if (showUpgrade) showUpgrade(`Você usou suas ${limit} mensagens grátis de hoje. No Pro são ilimitadas.`);
        return;
      }
      window.PlanilhaPlan.incrementChatMessages(userId);
    }
    setMessages(prev => [...prev, { role: "user", text, ts: new Date() }]);
    setInput("");
    setLoading(true);
    try {
      const result = await aiParseQuickEntry(text, ws);
      const txs = result.transactions || [];

      if (isHighConfidence(txs)) {
        // Auto-confirm: lança direto e mostra toast com undo
        const { undo, count } = autoSaveTxs(txs);
        const totalValue = txs.reduce((s, t) => s + (Number(t.value) || 0), 0);
        const summary = count === 1
          ? `✓ Lançado: ${txs[0].desc} · R$ ${totalValue.toFixed(2).replace(".", ",")}`
          : `✓ Lançados ${count} itens · total R$ ${totalValue.toFixed(2).replace(".", ",")}`;
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: summary, status: "saved", txs,
        }]);
        if (window.toast) {
          window.toast({
            message: summary,
            action: { label: "Desfazer", onClick: undo },
            duration: 8000,
          });
        }
      } else if (txs.length > 0) {
        // Baixa confiança (valor zero, descrição vazia): cai no preview pro user revisar
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: "Identifiquei mas faltou detalhe — confere antes de confirmar:",
          txs, status: "preview",
        }]);
      } else {
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: "Não consegui identificar nenhum lançamento aqui. Tenta algo como: 'gastei 15 no pastel', '20 no Uber', 'recebi 100 da Maria'.",
        }]);
      }
    } catch (e) {
      console.error("[Chat IA] erro:", e);
      setMessages(prev => [...prev, { role: "ai", ts: new Date(), text: `Erro: ${e?.message || e}`, isError: true }]);
    } finally {
      setLoading(false);
    }
  };

  // Foto: usuário tira/escolhe → IA vision extrai → preview (mesmo fluxo de texto)
  const handleImagePicked = async (e) => {
    const file = e.target.files?.[0];
    if (e.target) e.target.value = ""; // permite escolher mesma foto de novo
    if (!file || loading) return;

    // Pro-only — foto via IA vision custa 3-5x mais que texto
    if (!isPro) {
      if (showUpgrade) showUpgrade("Anexar foto de notas/recibos é um recurso Pro. Faça upgrade pra usar.");
      return;
    }

    // Limite de chat ainda vale (mesmo bucket)
    if (userId) {
      const used = window.PlanilhaPlan.getChatMessagesToday(userId);
      const limit = window.PlanilhaPlan.FREE_LIMITS.chatMessagesPerDay;
      // Pro: ilimitado, mas pra defesa em depth, só conta se algum bucket existir
      if (!isPro && used >= limit) {
        if (showUpgrade) showUpgrade(`Você usou suas ${limit} mensagens grátis de hoje. No Pro são ilimitadas.`);
        return;
      }
      window.PlanilhaPlan.incrementChatMessages(userId);
    }

    // Thumbnail imediato (URL.createObjectURL evita re-encode em base64 pra exibir)
    const thumbUrl = URL.createObjectURL(file);
    setMessages(prev => [...prev, {
      role: "user", ts: new Date(),
      text: "📷 Foto anexada", imageThumbUrl: thumbUrl,
    }]);
    setLoading(true);

    try {
      const result = await aiParseImage(file, ws);
      const txs = result.transactions || [];

      if (isHighConfidence(txs)) {
        // Auto-confirm: lança direto e mostra toast com undo
        const { undo, count } = autoSaveTxs(txs);
        const totalValue = txs.reduce((s, t) => s + (Number(t.value) || 0), 0);
        const summary = count === 1
          ? `✓ Lançado da foto: ${txs[0].desc} · R$ ${totalValue.toFixed(2).replace(".", ",")}`
          : `✓ Lançados ${count} itens da foto · total R$ ${totalValue.toFixed(2).replace(".", ",")}`;
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: summary, status: "saved", txs,
        }]);
        if (window.toast) {
          window.toast({
            message: summary,
            action: { label: "Desfazer", onClick: undo },
            duration: 8000,
          });
        }
      } else if (txs.length > 0) {
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: "Identifiquei mas faltou detalhe — confere antes de confirmar:",
          txs, status: "preview",
        }]);
      } else {
        setMessages(prev => [...prev, {
          role: "ai", ts: new Date(),
          text: "Não consegui extrair dados claros dessa foto. Tenta uma foto mais nítida ou digita manualmente o que gastou.",
        }]);
      }
    } catch (err) {
      console.error("[Chat IA foto] erro:", err);
      setMessages(prev => [...prev, {
        role: "ai", ts: new Date(),
        text: `Erro ao analisar foto: ${err?.message || err}`,
        isError: true,
      }]);
    } finally {
      setLoading(false);
    }
  };

  // Helper: cria UI objects e separa entradas/saídas. Reutilizado por
  // autoSaveTxs (fluxo novo com auto-confirm) e confirmTxs (fallback preview).
  const buildTxnsToSave = (txsToSave) => {
    const newOut = [];
    const newIn = [];
    const today = new Date().toISOString().slice(0, 10);
    txsToSave.forEach(t => {
      const ui = {
        id: crypto.randomUUID(),
        date: t.date || today,
        desc: (t.desc || "").trim() || "Lançamento rápido",
        value: Number(t.value) || 0,
        status: t.type === "income" ? "RECEBIDO" : "PAGO",
        payment: t.payment || "Pix",
        category: (t.category || (t.type === "income" ? "OUTRAS RECEITAS" : "OUTROS")).toUpperCase(),
        subcategory: t.subcategory || "",
        who: "",
      };
      if (t.type === "income") newIn.push(ui);
      else newOut.push(ui);
    });
    return { newOut, newIn };
  };

  // Auto-save sem perguntar (fluxo novo). Lança direto, mostra toast com undo.
  // Retorna { ids } pra undo conseguir reverter.
  const autoSaveTxs = (txsToSave) => {
    const { newOut, newIn } = buildTxnsToSave(txsToSave);
    const outIds = newOut.map(t => t.id);
    const inIds = newIn.map(t => t.id);
    setWs(prev => ({
      ...prev,
      transactions: [...newOut, ...(prev.transactions || [])],
      receitas:    [...newIn,  ...(prev.receitas    || [])],
    }));
    // Undo: remove pelos IDs salvos
    const undo = () => {
      setWs(prev => ({
        ...prev,
        transactions: (prev.transactions || []).filter(t => !outIds.includes(t.id)),
        receitas: (prev.receitas || []).filter(r => !inIds.includes(r.id)),
      }));
    };
    return { undo, count: txsToSave.length };
  };

  // Confirma e grava no workspace (fluxo antigo — usado quando IA não tem
  // confiança alta e cai no preview, ou foto que precisa revisão).
  const confirmTxs = (msgIndex, txsToSave) => {
    const { newOut, newIn } = buildTxnsToSave(txsToSave);
    setWs(prev => ({
      ...prev,
      transactions: [...newOut, ...(prev.transactions || [])],
      receitas:    [...newIn,  ...(prev.receitas    || [])],
    }));
    setMessages(prev => prev.map((m, i) => i === msgIndex ? { ...m, status: "saved" } : m));
  };

  // Decide se a IA acertou o suficiente pra auto-confirmar:
  // todos os txs precisam ter valor > 0 e descrição não vazia
  const isHighConfidence = (txs) => {
    if (!Array.isArray(txs) || txs.length === 0) return false;
    return txs.every(t => Number(t.value) > 0 && (t.desc || "").trim().length > 0);
  };

  const cancelTxs = (msgIndex) => {
    setMessages(prev => prev.map((m, i) => i === msgIndex ? { ...m, status: "cancelled" } : m));
  };

  // Ajusta valor / desc / categoria de um item específico do preview
  const editTx = (msgIndex, txIndex, field, value) => {
    setMessages(prev => prev.map((m, i) => {
      if (i !== msgIndex) return m;
      const newTxs = m.txs.map((t, ti) => ti === txIndex ? { ...t, [field]: value } : t);
      return { ...m, txs: newTxs };
    }));
  };

  const removeTx = (msgIndex, txIndex) => {
    setMessages(prev => prev.map((m, i) => {
      if (i !== msgIndex) return m;
      const newTxs = m.txs.filter((_, ti) => ti !== txIndex);
      if (newTxs.length === 0) return { ...m, status: "cancelled" };
      return { ...m, txs: newTxs };
    }));
  };

  const onKey = (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      handleSubmit();
    }
  };

  // ============ UI ============

  // FAB fechado (só no modo flutuante)
  if (!open && !embedded) {
    return (
      <button
        className="qc-fab"
        onClick={() => setOpen(true)}
        title="Lançamento rápido por chat"
        style={{
          position: "fixed", bottom: 22, right: 22, zIndex: 9000,
          width: 60, height: 60, borderRadius: 999,
          background: "var(--green)", color: "var(--green-soft)", border: "none", cursor: "pointer",
          boxShadow: "var(--glow-green)",
          display: "flex", alignItems: "center", justifyContent: "center",
          fontFamily: "'Manrope', system-ui, sans-serif",
          transition: "transform .25s ease, box-shadow .25s ease",
        }}
        onMouseEnter={e => { e.currentTarget.style.transform = "translateY(-2px) scale(1.04)"; e.currentTarget.style.boxShadow = "0 12px 32px color-mix(in oklab, var(--green) 45%, transparent)"; }}
        onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "var(--glow-green)"; }}
      >
        <Icon name="spark" size={24} />
      </button>
    );
  }

  // Painel — flutuante (canto inferior direito) ou embedded (fullscreen)
  const containerStyle = embedded ? {
    position: "absolute", inset: 0,
    display: "flex", flexDirection: "column",
    background: "var(--surface-1)", overflow: "hidden",
  } : {
    position: "fixed", bottom: 22, right: 22, zIndex: 9000,
    width: 400, maxWidth: "calc(100vw - 24px)",
    height: 580, maxHeight: "calc(100vh - 40px)",
    display: "flex", flexDirection: "column",
    background: "var(--surface-1)", border: "1px solid var(--border)", borderRadius: 24,
    boxShadow: "var(--shadow-lg)", overflow: "hidden",
  };

  return (
    <div className={embedded ? "" : "qc-floating-panel"} style={containerStyle}>
      {/* Cabeçalho */}
      <div style={{
        padding: "16px 18px", borderBottom: "1px solid var(--border)",
        display: "flex", alignItems: "center", justifyContent: "space-between",
        background: "linear-gradient(165deg, color-mix(in oklab, var(--green) 8%, transparent), transparent)",
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
          <span style={{ width: 36, height: 36, borderRadius: 12, background: "var(--green)", color: "var(--green-soft)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 4px 14px color-mix(in oklab, var(--green) 35%, transparent)" }}>
            <Icon name="spark" size={16} />
          </span>
          <div>
            <div style={{ fontFamily: "'Fraunces', Georgia, serif", fontSize: 18, fontWeight: 400, color: "var(--fg-1)", letterSpacing: "-0.02em", lineHeight: 1.1 }}>
              Lançamento <em style={{ fontStyle: "italic", fontWeight: 300, color: "var(--green)" }}>rápido</em>
            </div>
            <div className="eyebrow" style={{ marginTop: 3, fontSize: 10 }}>
              Fala ou escreve · IA registra
            </div>
          </div>
        </div>
        {!embedded && (
          <button onClick={() => setOpen(false)} className="icon-btn">
            <Icon name="x" size={14} />
          </button>
        )}
      </div>

      {/* Histórico */}
      <div ref={scrollRef} style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 10, background: "var(--bg)" }}>
        {messages.length === 0 && (
          <div style={{ padding: "28px 12px", textAlign: "center", color: "var(--fg-3)", lineHeight: 1.5 }}>
            <div style={{ width: 56, height: 56, borderRadius: 18, background: "color-mix(in oklab, var(--green) 8%, transparent)", border: "1px solid color-mix(in oklab, var(--green) 22%, transparent)", color: "var(--green)", display: "inline-flex", alignItems: "center", justifyContent: "center", marginBottom: 16 }}>
              <Icon name="spark" size={26} />
            </div>
            <div style={{ fontFamily: "'Fraunces', Georgia, serif", fontSize: 22, fontWeight: 400, color: "var(--fg-1)", letterSpacing: "-0.02em", marginBottom: 18 }}>
              Manda do seu <em style={{ fontStyle: "italic", fontWeight: 300, color: "var(--green)" }}>jeito</em>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 18 }}>
              {[
                "gastei 15 no pastel",
                "20 no Uber e 80 no mercado",
                "recebi 1500 do salário",
              ].map((ex, i) => (
                <div key={i} style={{
                  fontFamily: "'Fraunces', Georgia, serif", fontStyle: "italic", fontWeight: 400,
                  fontSize: 14, color: "var(--fg-2)", lineHeight: 1.4,
                }}>
                  “{ex}”
                </div>
              ))}
            </div>
            <div className="eyebrow" style={{ fontSize: 10, color: "var(--fg-3)", display: "inline-flex", alignItems: "center", gap: 6 }}>
              <Icon name="mic" size={11} /> microfone também funciona — Chrome/Edge
            </div>
          </div>
        )}
        {messages.map((m, i) => (
          <ChatBubble
            key={i}
            msg={m}
            accent={accent}
            ws={ws}
            onConfirm={() => confirmTxs(i, m.txs)}
            onCancel={() => cancelTxs(i)}
            onEdit={(ti, f, v) => editTx(i, ti, f, v)}
            onRemove={(ti) => removeTx(i, ti)}
          />
        ))}
        {loading && (
          <div style={{ alignSelf: "flex-start", display: "flex", gap: 7, padding: "12px 16px", background: "var(--surface-1)", border: "1px solid var(--border)", borderRadius: "18px 18px 18px 4px" }}>
            <Dot accent="var(--green)" delay={0} />
            <Dot accent="var(--green)" delay={150} />
            <Dot accent="var(--green)" delay={300} />
          </div>
        )}
      </div>

      {/* Input */}
      <div style={{ padding: "12px 14px", borderTop: "1px solid var(--border)", background: "var(--surface-1)" }}>
        <div style={{ display: "flex", alignItems: "flex-end", gap: 6, padding: "6px 6px 6px 16px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 999 }}>
          <textarea
            ref={inputRef}
            value={input}
            onChange={e => setInput(e.target.value)}
            onKeyDown={onKey}
            onFocus={(e) => {
              // iOS Safari: força o input a aparecer acima do teclado.
              // O timeout dá tempo do teclado terminar de subir antes do scroll.
              setTimeout(() => {
                e.target.scrollIntoView({ block: "center", behavior: "smooth" });
              }, 300);
            }}
            placeholder={recording ? "Ouvindo… fale agora" : "digite ou clique no microfone…"}
            rows={1}
            disabled={loading || recording}
            style={{
              flex: 1, resize: "none", border: "none", outline: "none", background: "transparent",
              fontFamily: "'Manrope', system-ui, sans-serif", fontSize: 13.5, color: "var(--fg-1)",
              padding: "8px 4px", maxHeight: 100, minHeight: 22, lineHeight: 1.45,
            }}
          />
          {/* Input file escondido — câmera (mobile) ou file picker (desktop).
              Sem `capture` deixa iOS/Android mostrar menu nativo "Tirar foto" ou "Escolher da galeria" */}
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            style={{ display: "none" }}
            onChange={handleImagePicked}
          />
          <button
            onClick={() => {
              if (!isPro) {
                if (showUpgrade) showUpgrade("Anexar foto de notas/recibos é um recurso Pro. Faça upgrade pra usar.");
                return;
              }
              fileInputRef.current?.click();
            }}
            disabled={loading || recording}
            title={isPro ? "Anexar foto de nota/recibo" : "Recurso Pro — clique pra ver"}
            style={{
              width: 36, height: 36, borderRadius: 999, cursor: (loading || recording) ? "default" : "pointer",
              background: "transparent",
              color: isPro ? "var(--green)" : "var(--fg-3)",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontFamily: "inherit", flexShrink: 0,
              border: "1px solid var(--border)",
              transition: "background .2s ease, color .2s ease",
              opacity: (loading || recording) ? 0.5 : 1,
            }}
          >
            <Icon name="camera" size={15} />
          </button>
          <button
            onClick={recording ? stopRecording : startRecording}
            disabled={loading}
            title={recording ? "Parar gravação" : "Falar"}
            style={{
              width: 36, height: 36, borderRadius: 999, cursor: loading ? "default" : "pointer",
              background: recording ? "var(--danger)" : "transparent",
              color: recording ? "white" : "var(--green)",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontFamily: "inherit", flexShrink: 0,
              animation: recording ? "pulse 1.2s ease-in-out infinite" : "none",
              border: recording ? "none" : "1px solid var(--border)",
              transition: "background .2s ease, color .2s ease, border-color .2s ease",
            }}
          >
            <Icon name={recording ? "x" : "mic"} size={15} />
          </button>
          <button
            onClick={handleSubmit}
            disabled={!input.trim() || loading || recording}
            title="Enviar (Enter)"
            className={(!input.trim() || loading || recording) ? "" : "qc-send-active"}
            style={{
              width: 36, height: 36, borderRadius: 999,
              cursor: (!input.trim() || loading || recording) ? "default" : "pointer",
              background: (!input.trim() || loading || recording) ? "var(--surface-1)" : "var(--green)",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontFamily: "inherit", flexShrink: 0,
              border: (!input.trim() || loading || recording) ? "1px solid var(--border)" : "none",
              color: (!input.trim() || loading || recording) ? "var(--fg-3)" : undefined,
              boxShadow: (!input.trim() || loading || recording) ? "none" : "0 3px 10px color-mix(in oklab, var(--green) 35%, transparent)",
              transition: "background .2s ease, color .2s ease, box-shadow .2s ease",
            }}
          >
            <Icon name="arrowUp" size={15} />
          </button>
        </div>
        <style>{`
          @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.6 } }
          @keyframes blink { 0%,80%,100% { opacity: 0.25 } 40% { opacity: 1 } }
        `}</style>
      </div>
    </div>
  );
}

function Dot({ accent, delay }) {
  return <span style={{ width: 6, height: 6, borderRadius: 999, background: accent, animation: `blink 1.2s infinite`, animationDelay: `${delay}ms` }} />;
}

// Bolha de mensagem (usuário, IA, ou preview com lançamentos editáveis)
function ChatBubble({ msg, accent, ws, onConfirm, onCancel, onEdit, onRemove }) {
  const isUser = msg.role === "user";
  const time = msg.ts ? new Date(msg.ts).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }) : "";

  if (isUser) {
    return (
      <div style={{ alignSelf: "flex-end", maxWidth: "85%" }}>
        {msg.imageThumbUrl && (
          <img
            src={msg.imageThumbUrl}
            alt="Foto anexada"
            style={{
              display: "block",
              maxWidth: 200, maxHeight: 220,
              borderRadius: 14,
              border: "1px solid var(--border)",
              marginBottom: 6, marginLeft: "auto",
              objectFit: "cover",
            }}
          />
        )}
        <div className="qc-bubble-user" style={{
          padding: "10px 16px", background: "var(--green)",
          borderRadius: "18px 18px 4px 18px",
          fontFamily: "'Manrope', system-ui, sans-serif", fontSize: 13.5, lineHeight: 1.45, wordBreak: "break-word", fontWeight: 500,
        }}>
          {msg.text}
        </div>
        <div className="eyebrow" style={{ fontSize: 9.5, textAlign: "right", marginTop: 4, marginRight: 6 }}>{time}</div>
      </div>
    );
  }

  // IA
  return (
    <div style={{ alignSelf: "flex-start", maxWidth: "92%" }}>
      <div style={{
        padding: "12px 16px", background: msg.isError ? "var(--danger-bg)" : "var(--surface-1)",
        border: msg.isError ? "1px solid color-mix(in oklab, var(--danger) 25%, transparent)" : "1px solid var(--border)",
        borderRadius: "18px 18px 18px 4px",
        fontFamily: "'Manrope', system-ui, sans-serif", fontSize: 13.5, lineHeight: 1.45, color: msg.isError ? "var(--danger)" : "var(--fg-1)",
      }}>
        {msg.text}
        {msg.txs && msg.status === "preview" && (
          <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
            {msg.txs.map((t, ti) => (
              <PreviewTxRow key={ti} tx={t} ws={ws} onEdit={(f, v) => onEdit(ti, f, v)} onRemove={() => onRemove(ti)} />
            ))}
            <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
              <button
                onClick={onConfirm}
                className="btn-primary"
                style={{ flex: 1, padding: "9px 14px", fontSize: 12.5, justifyContent: "center" }}
              >
                <Icon name="check" size={12} /> Confirmar e lançar
              </button>
              <button onClick={onCancel} className="btn-ghost" style={{ fontSize: 12 }}>
                Cancelar
              </button>
            </div>
          </div>
        )}
        {msg.status === "saved" && (
          <div style={{ marginTop: 10, padding: "7px 12px", background: "var(--success-bg)", color: "var(--success)", borderRadius: 999, fontSize: 11.5, fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "'JetBrains Mono', ui-monospace, monospace", letterSpacing: "0.08em", textTransform: "uppercase" }}>
            <Icon name="check" size={11} /> lançado
          </div>
        )}
        {msg.status === "cancelled" && (
          <div className="eyebrow" style={{ marginTop: 8, fontSize: 10, fontStyle: "italic" }}>cancelado</div>
        )}
      </div>
      <div className="eyebrow" style={{ fontSize: 9.5, marginTop: 4, marginLeft: 6 }}>{time}</div>
    </div>
  );
}

// Linha de preview editável (valor, descrição, categoria)
function PreviewTxRow({ tx, ws, onEdit, onRemove }) {
  const isIncome = tx.type === "income";
  const cats = isIncome ? (ws.incomeCategories || []) : (ws.categories || []);
  const tone = isIncome ? "var(--success)" : "var(--danger)";

  return (
    <div style={{
      padding: 8, borderRadius: 8, background: "var(--surface-2)", border: "1px solid var(--border)",
      display: "flex", flexDirection: "column", gap: 6,
    }}>
      <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
        <span style={{ fontSize: 9, fontWeight: 700, padding: "2px 6px", borderRadius: 3, background: `color-mix(in oklab, ${tone} 12%, transparent)`, color: tone, letterSpacing: 0.4, flexShrink: 0 }}>
          {isIncome ? "ENTRADA" : "SAÍDA"}
        </span>
        <input
          type="text"
          value={tx.desc || ""}
          onChange={e => onEdit("desc", e.target.value)}
          placeholder="Descrição"
          style={{
            flex: 1, border: "1px solid var(--border)", borderRadius: 5, padding: "4px 7px",
            fontFamily: "inherit", fontSize: 12, background: "var(--surface-1)", color: "var(--fg-1)", minWidth: 0,
          }}
        />
        <button
          onClick={onRemove}
          title="Remover este lançamento"
          style={{ width: 22, height: 22, padding: 0, border: "none", background: "transparent", color: "var(--danger)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}
        >
          <Icon name="trash" size={11} />
        </button>
      </div>
      <div style={{ display: "flex", gap: 6 }}>
        <input
          type="number"
          step="0.01"
          value={tx.value ?? ""}
          onChange={e => onEdit("value", parseFloat(e.target.value) || 0)}
          placeholder="0,00"
          style={{
            width: 90, border: "1px solid var(--border)", borderRadius: 5, padding: "4px 7px",
            fontFamily: "inherit", fontSize: 12, fontVariantNumeric: "tabular-nums", fontWeight: 600,
            background: "var(--surface-1)", color: tone, textAlign: "right",
          }}
        />
        <select
          value={tx.category || ""}
          onChange={e => onEdit("category", e.target.value)}
          style={{
            flex: 1, border: "1px solid var(--border)", borderRadius: 5, padding: "4px 7px",
            fontFamily: "inherit", fontSize: 11.5, background: "var(--surface-1)", color: "var(--fg-1)", minWidth: 0,
          }}
        >
          {cats.map(c => <option key={c} value={c}>{c}</option>)}
          {!cats.includes(tx.category) && tx.category && (
            <option value={tx.category}>{tx.category} (não cadastrada)</option>
          )}
        </select>
      </div>
    </div>
  );
}

// ============ IA: parser de mensagens casuais ============
async function aiParseQuickEntry(text, ws) {
  const isBusiness = ws.type !== "personal";
  const expCats = (ws.categories || []).join(", ") || "OUTROS";
  const incCats = (ws.incomeCategories || []).join(", ") || "OUTRAS RECEITAS";
  const today = new Date().toISOString().slice(0, 10);

  const systemMsg = `Você extrai lançamentos financeiros de mensagens em português do Brasil. As mensagens podem ser CAÓTICAS, multi-linha, com listas, cálculos, formato WhatsApp, headers, gírias, e nomes que não batem com categoria nenhuma. SEU TRABALHO É EXTRAIR TUDO.

CONTEXTO:
- Conta ${isBusiness ? "empresarial" : "pessoal (orçamento doméstico)"}
- Data atual: ${today}
- Classificações DESPESA disponíveis (use APENAS estas, fallback OUTROS): ${expCats}
- Classificações RECEITA disponíveis (use APENAS estas, fallback OUTRAS RECEITAS): ${incCats}

═══════════════════════════════════════════
REGRA #1 — EXTRAÇÃO EXAUSTIVA (MAIS IMPORTANTE)
═══════════════════════════════════════════
Uma mensagem pode ter MUITOS lançamentos (5, 10, 30, 50+). EXTRAIA TODOS.
Listas multi-linha onde CADA LINHA é um lançamento são MUITO comuns:
  5.500 Locomoção
  5.152,16 Campeonato Prévias
  2.367,45 Campeonato Influencer
  8.567 Jacaré
→ 4 lançamentos: Locomoção (5500), Campeonato Prévias (5152.16), Campeonato Influencer (2367.45), Jacaré (8567).

NUNCA pule item por:
- "não tem categoria boa" → use OUTROS, preserva o nome
- "nome é estranho" → preserva exatamente como está (Jacaré, SESC Morrinhos, Comissão Guigo)
- "valor sem moeda explícita" → assume Reais
- "linha repetida" → cria 2 lançamentos iguais se a mensagem tem 2 vezes

═══════════════════════════════════════════
REGRA #2 — NÚMEROS NO FORMATO BRASILEIRO
═══════════════════════════════════════════
CRÍTICO: ponto é separador de MILHAR, vírgula é DECIMAL.
- "5.500" → 5500 (NÃO 5.5)
- "5.500,00" → 5500.00
- "5.152,16" → 5152.16
- "30.000,00" → 30000.00
- "1.930" → 1930
- "R$ 15,90" → 15.90
- "1.000.000" → 1000000
- "0,99" → 0.99
- "15 reais" → 15
- "quinze reais" → 15

═══════════════════════════════════════════
REGRA #3 — CÁLCULOS E FÓRMULAS
═══════════════════════════════════════════
Quando a mensagem tem expressões matemáticas, use o VALOR FINAL (depois do "="):
- "30.000,00 - 1.300,00 - 2.400,00 - 7.000,00 = 19.300" → valor FINAL: 19300 (não 30000)
- "19.300 X 10% = 1.930" → valor FINAL: 1930 (a comissão, não o total)
- "100 + 200 = 300" → valor: 300
Se a mensagem tem context tipo "Comissão Guigo:" + cálculo → desc: "Comissão Guigo", value: resultado final.

═══════════════════════════════════════════
REGRA #4 — HEADERS QUE AGRUPAM CONTEXTO
═══════════════════════════════════════════
Linhas tipo "Gastei:", "Recebi:", "Pagamentos:", "Receitas:", "Comissões:" são HEADERS:
- "Gastei:" antes de uma lista → TODOS os items dessa lista são expense
- "Recebi:" antes de uma lista → TODOS os items são income
- "Comissão Guigo:" + lista/cálculo → o ITEM principal é "Comissão Guigo" (income por default, comissão é receita)
Headers terminam quando vem outra header OU quando muda completamente o assunto.

═══════════════════════════════════════════
REGRA #5 — DETECÇÃO DE TIPO (expense vs income)
═══════════════════════════════════════════
EXPENSE (saída):
- Verbos: "gastei", "paguei", "comprei", "torrei", "saiu", "fui", "passei no"
- Header "Gastei:" / "Despesas:" → todos da lista são expense
- Items sem verbo mas em contexto de gasto

INCOME (entrada):
- Verbos: "recebi", "ganhei", "caiu", "entrou", "me pagaram", "depositei"
- Header "Recebi:" / "Receitas:" / "Comissões:" → todos da lista são income
- Palavras-chave: "comissão", "salário", "freelance", "pró-labore", "venda", "show", "produção", "por show"
- "X Shows total", "valor por show" → INCOME (produção é receita)

Default: SE não tiver pista clara, contexto da mensagem manda.

═══════════════════════════════════════════
REGRA #6 — DESCRIÇÃO (NUNCA vazia)
═══════════════════════════════════════════
Preserve o NOME EXATO da mensagem, capitalizado.
Exemplos críticos:
- "5.500 Locomoção" → desc: "Locomoção"
- "8.567 Jacaré" → desc: "Jacaré"
- "Campeonato Influencer" → desc: "Campeonato Influencer"
- "SESC MORRINHOS" → desc: "SESC Morrinhos" (preserva sigla, ajusta capitalização do resto)
- "7.000 Comissão vendedor" → desc: "Comissão Vendedor"
- "Comissão Guigo" → desc: "Comissão Guigo"
- "Por Show 1.530,66" → desc: "Por Show"
- "3 Shows total" → desc: "Shows" (quantidade vira contexto, não valor)

═══════════════════════════════════════════
REGRA #7 — CATEGORIA (use APENAS da lista disponível)
═══════════════════════════════════════════
Mapeamento padrão:
- Uber, 99, Cabify, posto, combustível, estacionamento, locomoção → TRANSPORTE
- iFood, Rappi, restaurante, lanche, pastel, pizza, mercado, padaria → ALIMENTAÇÃO
- Netflix, Spotify, Disney+ → ASSINATURAS
- Cinema, jogo, viagem, evento, show, bar → LAZER
- Farmácia, médico, hospital, remédio, plano de saúde, dentista → SAÚDE
- Escola, faculdade, curso → EDUCAÇÃO
- Aluguel, energia, água, condomínio, internet → MORADIA
- Google Ads, Meta Ads → MARKETING (empresa)
- Adobe, Microsoft 365, AWS → TECNOLOGIA (empresa)
- Salário pago, pró-labore → FOLHA (empresa)
- DARF, ICMS, imposto → TRIBUTOS (empresa)
- Contador → CUSTOS FIXOS (empresa)
- Compras, fornecedores → CUSTOS VARIÁVEIS (empresa)

PARA RECEITAS:
- Salário, freelance, comissão, show, produção, venda → escolha da lista de RECEITA disponível

═══════════════════════════════════════════
SE A CATEGORIA ESPECÍFICA NÃO EXISTE NA LISTA → USE OUTROS (despesa) OU OUTRAS RECEITAS (receita).
Items como "Jacaré", "Campeonato Influencer", "Comissão Guigo", "SESC Morrinhos" provavelmente não têm categoria específica → vão pra OUTROS/OUTRAS RECEITAS, mas a DESCRIÇÃO preserva o nome real.
═══════════════════════════════════════════

REGRA #8 — DATA
Padrão hoje (${today}). "ontem" → -1 dia. "anteontem" → -2. "semana passada" → -7. "domingo passado" → data exata. Caso contrário, hoje.

REGRA #9 — PAGAMENTO
Default "Pix". "no cartão" → "Cartão". "boleto" → "Boleto". "dinheiro" → "Dinheiro". "transferência" / "ted" → "Transferência".

═══════════════════════════════════════════
EXEMPLO REAL (formato WhatsApp caótico):
═══════════════════════════════════════════
USUÁRIO:
"""
Gastei:
5.500 Locomoção
5.152,16 Campeonato Prévias
2.367,45 Campeonato Influencer
8.567 Jacaré
7.000 Comissão vendedor
"""

RESPOSTA (5 transactions):
[
  {type: expense, value: 5500, desc: "Locomoção", category: "TRANSPORTE"},
  {type: expense, value: 5152.16, desc: "Campeonato Prévias", category: "OUTROS"},
  {type: expense, value: 2367.45, desc: "Campeonato Influencer", category: "OUTROS"},
  {type: expense, value: 8567, desc: "Jacaré", category: "OUTROS"},
  {type: expense, value: 7000, desc: "Comissão Vendedor", category: "OUTROS"}
]

USUÁRIO:
"""
Comissão Guigo:
SESC MORRINHOS
30.000,00 - 1.300,00 - 2.400,00 - 7.000,00 = 19.300
19.300 X 10% = 1.930
"""

RESPOSTA (1 transaction — comissão é o valor final, 1930):
[{type: income, value: 1930, desc: "Comissão Guigo - SESC Morrinhos", category: "OUTRAS RECEITAS"}]

USUÁRIO:
"""
4.592,00 Produção
1.530,66 Por Show
3 Shows total
"""

RESPOSTA (2 transactions — "3 Shows total" é contexto, não lançamento):
[
  {type: income, value: 4592, desc: "Produção", category: "OUTRAS RECEITAS"},
  {type: income, value: 1530.66, desc: "Por Show", category: "OUTRAS RECEITAS"}
]

═══════════════════════════════════════════
ZERO ITEM PULADO. ZERO INVENÇÃO. EXTRAIA TUDO. PRESERVE NOMES REAIS.
═══════════════════════════════════════════

Se a mensagem NÃO for sobre dinheiro (ex: "oi", "como vai") → devolva transactions vazio.

OUTPUT (JSON estrito, sem markdown, sem comentários):
{
  "transactions": [
    {"type":"expense"|"income","date":"YYYY-MM-DD","desc":"...","value":0,"category":"...","subcategory":"...","payment":"..."}
  ]
}`;

  const userMsg = `MENSAGEM DO USUÁRIO:
"""
${text}
"""

Extraia TODOS os lançamentos em JSON. Não pule nenhum item. Se mensagem tem 10 itens, devolva 10 transactions. Se não tem categoria boa pra algum, use OUTROS.`;

  // maxTokens 4000 — comporta ~100 lançamentos por mensagem (era 800 = ~22)
  const out = await window.db.callAi({ system: systemMsg, user: userMsg, maxTokens: 4000 });
  const fenced = out.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
  const jsonStr = fenced ? fenced[1] : (out.match(/\{[\s\S]+\}/)?.[0] || out);
  let parsed;
  try { parsed = JSON.parse(jsonStr); }
  catch (e) {
    console.error("[Chat IA] JSON inválido:", out.slice(0, 500));
    throw new Error("Não consegui entender a resposta da IA.");
  }
  return { transactions: Array.isArray(parsed.transactions) ? parsed.transactions : [] };
}

// ============ IA: parser de imagem (nota fiscal, recibo, preço) ============
async function aiParseImage(file, ws) {
  // File → base64 puro (sem prefix data:...;base64,)
  const base64 = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result || "";
      const idx = String(result).indexOf(",");
      resolve(idx >= 0 ? String(result).slice(idx + 1) : String(result));
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });

  const isBusiness = ws.type !== "personal";
  const expCats = (ws.categories || []).join(", ") || "OUTROS";
  const incCats = (ws.incomeCategories || []).join(", ") || "OUTRAS RECEITAS";
  const payments = (ws.payments || ["Pix", "Cartão", "Dinheiro", "Boleto", "Transferência"]).join(", ");
  const today = new Date().toISOString().slice(0, 10);

  const systemMsg = `Você analisa fotos de notas fiscais, recibos, comprovantes, etiquetas de preço e cupons.
Extrai os dados em JSON pra criar lançamentos financeiros.

CONTEXTO:
- Conta ${isBusiness ? "empresarial" : "pessoal"}
- Data atual: ${today}
- Classificações DESPESA disponíveis (use APENAS estas, fallback OUTROS): ${expCats}
- Classificações RECEITA disponíveis (use APENAS estas, fallback OUTRAS RECEITAS): ${incCats}
- Formas de pagamento aceitas: ${payments}

REGRAS DE LEITURA:
1. Se for CUPOM FISCAL / RECIBO com vários itens: agregue em 1 transaction só, com o TOTAL e desc = nome do estabelecimento. Não separe item por item.
2. Se for ETIQUETA DE PREÇO de um produto: 1 transaction com valor = preço, desc = nome do produto, type=expense.
3. Se for COMPROVANTE DE TRANSFERÊNCIA/PIX recebido: 1 transaction type=income, desc = quem enviou.
4. Se for COMPROVANTE DE PAGAMENTO (saída): 1 transaction type=expense, desc = pra quem foi.
5. VALOR: sempre número decimal positivo (sem R$, sem ponto de milhar, ponto decimal). Ex: 1.234,56 → 1234.56.
6. DATA: se a foto mostrar data, use no formato YYYY-MM-DD. Se não conseguir ler, use ${today}.
7. CATEGORIA: escolha a melhor da lista disponível. Mapeamento típico:
   - Mercado, padaria, açougue, restaurante, iFood → ALIMENTAÇÃO
   - Posto, combustível, Uber, 99, estacionamento → TRANSPORTE
   - Farmácia, médico, hospital → SAÚDE
   - Netflix, Spotify, plataforma streaming → ASSINATURAS
   - Cinema, viagem, evento → LAZER
   - Aluguel, energia, água, internet → MORADIA
   - Nada encaixa → "OUTROS"
8. PAYMENT: se a foto deixar claro (ex: "Pix", "Crédito Visa"), use. Default: "Pix".
9. Se a foto NÃO contiver dados financeiros legíveis (ex: foto borrada, sem texto, foto pessoal), retorne transactions vazio.

OUTPUT (JSON estrito, sem markdown, sem comentários):
{
  "transactions": [
    {"type":"expense"|"income","date":"YYYY-MM-DD","desc":"...","value":0,"category":"...","subcategory":"...","payment":"..."}
  ]
}`;

  const userMsg = `Analise a foto e extraia o lançamento financeiro em JSON.`;

  const out = await window.db.callAiWithImage({
    system: systemMsg,
    user: userMsg,
    imageBase64: base64,
    mimeType: file.type || "image/jpeg",
    maxTokens: 1000,
  });

  const fenced = out.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
  const jsonStr = fenced ? fenced[1] : (out.match(/\{[\s\S]+\}/)?.[0] || out);
  let parsed;
  try { parsed = JSON.parse(jsonStr); }
  catch (e) {
    console.error("[Chat IA foto] JSON inválido:", out.slice(0, 500));
    throw new Error("Não consegui entender a resposta da IA.");
  }
  return { transactions: Array.isArray(parsed.transactions) ? parsed.transactions : [] };
}

window.QuickEntryChat = QuickEntryChat;
