/* =========================================================
   Chatbot — Fran AI assistant
   Floating button + chat window. Talks to the Claude API via
   the Cloudflare Worker at CHAT_API_URL below.
   On detailed/contact intents, redirects to WhatsApp.

   ─── Bot abuse / cost containment ───────────────────────
   The chatbot proxies to Anthropic via a public Worker, so an
   abusive client can burn API budget. Hard limits live on the
   Worker (server/worker.js) — per-IP RPM/RPH, daily token cap,
   payload size, system-prompt injection. Client-side defences
   here are UX-level only:
     • Max MAX_MSGS_PER_SESSION user messages per browser session
     • Min MIN_INTERVAL_MS between sends (debounce open-mic abuse)
     • Max MAX_INPUT_CHARS per input (truncates instead of crashing)
     • Single in-flight request — abort the previous on new send
     • NO system prompt sent from client (Worker injects it).
       Removes the prompt from the JS bundle and stops prompt
       overrides via the open proxy.
   ========================================================= */

const { useEffect: useEf, useRef: useRf, useState: useSt } = React;

const WHATSAPP = "+41767312517";
const WA_URL = (msg) => `https://wa.me/${WHATSAPP.replace(/[^0-9]/g, "")}?text=${encodeURIComponent(msg)}`;

// Cloudflare Worker that proxies Claude. The Worker is responsible for
// origin allowlisting, rate-limit, payload validation, daily token cap
// and system-prompt injection. See server/worker.js for the contract.
const CHAT_API_URL = "https://claude-chatbot-api.frannypunto.workers.dev";

// Client-side soft caps. None of these are security boundaries — a
// determined attacker bypasses them by clearing storage or hitting the
// Worker directly. They exist to keep accidental loops + button-mash
// abuse from a real visitor in check.
const MAX_MSGS_PER_SESSION = 25;
const MIN_INTERVAL_MS      = 1500;
const MAX_INPUT_CHARS      = 1000;
const SESSION_KEY          = "fran-chat-session-count";

async function callChatAPI({ lang, messages }, signal) {
  const res = await fetch(CHAT_API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ lang, messages }),
    signal,
  });
  if (!res.ok) {
    throw new Error(`Chat API ${res.status}`);
  }
  // Try JSON first; fall back to plain text. Look in the common reply fields.
  const ct = res.headers.get("content-type") || "";
  if (ct.includes("application/json")) {
    const data = await res.json();
    const fromContent = Array.isArray(data?.content)
      ? data.content.map((c) => (typeof c === "string" ? c : c?.text || "")).join("").trim()
      : "";
    return (
      data?.reply ||
      data?.text ||
      data?.message ||
      data?.completion ||
      data?.output ||
      fromContent ||
      ""
    );
  }
  return (await res.text()).trim();
}

const CHAT_T = {
  en: {
    open: "Chat with Fran's AI",
    title: "Fran's AI assistant",
    sub: "Quick answers in seconds",
    online: "Online",
    placeholder: "Ask anything about my work, pricing, process…",
    send: "Send",
    welcome: "Hi 👋 I'm Fran's AI assistant. Ask me about services, pricing, process or anything else. For a detailed quote or booking a call, I'll send you to Fran directly on WhatsApp.",
    waCta: "Talk to Fran on WhatsApp",
    waNote: "For detailed quotes, project briefs or scheduling a call, it's faster to chat with Fran directly:",
    suggestions: ["What services do you offer?", "How much does a website cost?", "Do you work with clients in Switzerland?", "Talk to Fran directly"],
    thinking: "Thinking…",
    error: "Something went wrong. Want to message Fran on WhatsApp?",
    waSeed: "Hi Fran! I'm visiting your site and would like to chat about a project.",
  },
  de: {
    open: "Chatte mit Frans KI",
    title: "Frans KI-Assistent",
    sub: "Schnelle Antworten in Sekunden",
    online: "Online",
    placeholder: "Frag alles zu Projekten, Preisen, Ablauf…",
    send: "Senden",
    welcome: "Hi 👋 Ich bin Frans KI-Assistent. Frag mich zu Leistungen, Preisen oder dem Ablauf. Für eine ausführliche Offerte oder einen Anruf verbinde ich dich direkt mit Fran auf WhatsApp.",
    waCta: "Mit Fran auf WhatsApp schreiben",
    waNote: "Für detaillierte Offerten oder einen Termin chattest du am besten direkt mit Fran:",
    suggestions: ["Welche Leistungen bietest du an?", "Was kostet eine Website?", "Arbeitest du mit Kunden in der Schweiz?", "Direkt mit Fran sprechen"],
    thinking: "Denkt nach…",
    error: "Etwas ist schiefgelaufen. Mit Fran auf WhatsApp schreiben?",
    waSeed: "Hallo Fran! Ich besuche deine Website und möchte über ein Projekt sprechen.",
  },
  es: {
    open: "Chatea con la IA de Fran",
    title: "Asistente IA de Fran",
    sub: "Respuestas rápidas en segundos",
    online: "Online",
    placeholder: "Pregunta lo que quieras: servicios, precios, proceso…",
    send: "Enviar",
    welcome: "¡Hola! 👋 Soy el asistente IA de Fran. Pregúntame sobre servicios, precios o el proceso. Para presupuesto detallado o agendar una llamada, te paso directo a Fran por WhatsApp.",
    waCta: "Hablar con Fran por WhatsApp",
    waNote: "Para presupuestos detallados o agendar una llamada, lo más rápido es chatear con Fran directamente:",
    suggestions: ["¿Qué servicios ofreces?", "¿Cuánto cuesta una web?", "¿Trabajas en Suiza?", "Hablar con Fran directamente"],
    thinking: "Pensando…",
    error: "Algo ha fallado. ¿Te paso a Fran por WhatsApp?",
    waSeed: "¡Hola Fran! Estoy en tu web y me gustaría hablar de un proyecto.",
  },
};

/* System prompt now lives server-side in the Cloudflare Worker
   (see server/worker.js). Keeping it out of the client bundle means:
     • visitors can't read it via View Source or DevTools
     • prompt-injection via the open proxy is blocked (Worker ignores
       any `system` field from the client and always injects its own)
     • we can update the assistant's behaviour without shipping a
       new JS bundle — redeploy the Worker only. */

const ChatBot = ({ lang }) => {
  const ct = CHAT_T[lang] || CHAT_T.en;
  const [open, setOpen] = useSt(false);
  const [msgs, setMsgs] = useSt([{ role: "assistant", content: ct.welcome }]);
  const [input, setInput] = useSt("");
  const [busy, setBusy] = useSt(false);
  const bodyRef = useRf(null);
  const inputRef = useRf(null);
  // Track last send for the MIN_INTERVAL_MS debounce + the in-flight
  // controller so we can abort if the user sends again mid-request.
  const lastSendRef = useRf(0);
  const abortRef    = useRf(null);

  // Reset welcome message on language change
  useEf(() => {
    setMsgs([{ role: "assistant", content: ct.welcome }]);
  }, [lang]);

  // Autoscroll on new message
  useEf(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [msgs, busy]);

  // Focus input when opening
  useEf(() => {
    if (open && inputRef.current) setTimeout(() => inputRef.current.focus(), 200);
  }, [open]);

  // Abort any in-flight request when the component unmounts.
  useEf(() => () => { if (abortRef.current) abortRef.current.abort(); }, []);

  // Per-session message counter. sessionStorage resets when the tab
  // closes — feels less hostile than a long-lived cap. In-memory fallback
  // if storage is blocked (private mode, strict cookie settings).
  const getSessionCount = () => {
    try { return parseInt(sessionStorage.getItem(SESSION_KEY) || "0", 10) || 0; }
    catch { return 0; }
  };
  const bumpSessionCount = () => {
    try {
      const n = getSessionCount() + 1;
      sessionStorage.setItem(SESSION_KEY, String(n));
      return n;
    } catch { return 0; }
  };

  const send = async (text) => {
    let q = (text ?? input).trim();
    if (!q || busy) return;

    // Truncate over-long input client-side. The Worker rejects bigger
    // payloads anyway; truncating here gives a nicer UX than a 400.
    if (q.length > MAX_INPUT_CHARS) q = q.slice(0, MAX_INPUT_CHARS);

    // Debounce: refuse if user is hammering Send faster than the cap.
    const now = Date.now();
    if (now - lastSendRef.current < MIN_INTERVAL_MS) return;

    // Per-session ceiling — soft-redirect to WhatsApp once exhausted
    // rather than silently failing.
    if (getSessionCount() >= MAX_MSGS_PER_SESSION) {
      setMsgs((m) => [...m, { role: "user", content: q }, { role: "assistant", content: ct.waNote, whatsapp: true }]);
      setInput("");
      return;
    }

    setInput("");

    // If user clicks "talk to Fran" suggestion → straight to WhatsApp
    const lc = q.toLowerCase();
    if (lc.includes("whatsapp") || lc === ct.suggestions[3].toLowerCase()) {
      setMsgs((m) => [...m, { role: "user", content: q }, { role: "assistant", content: ct.waNote, whatsapp: true }]);
      return;
    }

    setMsgs((m) => [...m, { role: "user", content: q }]);
    setBusy(true);
    lastSendRef.current = now;
    bumpSessionCount();

    // Cancel any prior in-flight request — only one active fetch.
    if (abortRef.current) abortRef.current.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    try {
      // Build messages array (use only recent history to keep prompt small).
      // The Worker injects the system prompt server-side; we only send
      // `lang` so it picks the right language variant.
      const history = [...msgs, { role: "user", content: q }]
        .slice(-8)
        .map((m) => ({ role: m.role, content: m.content }));

      const reply = await callChatAPI({ lang, messages: history }, controller.signal);

      const hasWa = /\[WHATSAPP\]/i.test(reply);
      const clean = reply.replace(/\[WHATSAPP\]/gi, "").trim();
      setMsgs((m) => [...m, { role: "assistant", content: clean || ct.waNote, whatsapp: hasWa }]);
    } catch (e) {
      // Swallow abort errors silently — they're our own doing.
      if (e?.name !== "AbortError") {
        setMsgs((m) => [...m, { role: "assistant", content: ct.error, whatsapp: true }]);
      }
    } finally {
      if (abortRef.current === controller) abortRef.current = null;
      setBusy(false);
    }
  };

  return (
    <>
      <button
        className={`chat-fab ${open ? "open" : ""}`}
        onClick={() => setOpen(!open)}
        aria-label={ct.open}
      >
        {open ? (
          <span className="fab-icon">✕</span>
        ) : (
          <>
            <span className="fab-icon">
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
              </svg>
            </span>
            <span className="fab-pulse" />
          </>
        )}
      </button>

      <div className={`chat-window ${open ? "open" : ""}`}>
        <div className="chat-header">
          <div className="chat-avatar">
            <span>FR</span>
            <span className="led" />
          </div>
          <div className="chat-h-text">
            <div className="chat-title">{ct.title}</div>
            <div className="chat-sub">{ct.sub}</div>
          </div>
          <button className="chat-close" onClick={() => setOpen(false)} aria-label="Close">✕</button>
        </div>

        <div className="chat-body" ref={bodyRef}>
          {msgs.map((m, i) => (
            <div key={i} className={`chat-msg ${m.role}`}>
              <div className="bubble">{m.content}</div>
              {m.whatsapp && (
                <a className="wa-btn" href={WA_URL(ct.waSeed)} target="_blank" rel="noopener noreferrer">
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
                    <path d="M20.52 3.48A11.93 11.93 0 0 0 12.04 0C5.46 0 .12 5.34.12 11.92c0 2.1.55 4.15 1.6 5.96L0 24l6.27-1.64a11.9 11.9 0 0 0 5.77 1.47h.01c6.58 0 11.92-5.34 11.92-11.92 0-3.18-1.24-6.18-3.45-8.43zM12.04 21.79h-.01a9.86 9.86 0 0 1-5.03-1.38l-.36-.21-3.72.97 1-3.62-.23-.37a9.86 9.86 0 0 1-1.51-5.26c0-5.46 4.45-9.91 9.92-9.91 2.65 0 5.13 1.03 7 2.9a9.85 9.85 0 0 1 2.9 7c0 5.47-4.44 9.92-9.91 9.92zm5.44-7.42c-.3-.15-1.76-.87-2.04-.97-.27-.1-.47-.15-.67.15s-.77.97-.94 1.17c-.17.2-.35.22-.65.07-.3-.15-1.26-.46-2.4-1.48a9.04 9.04 0 0 1-1.67-2.07c-.17-.3-.02-.46.13-.61.13-.13.3-.35.45-.52.15-.17.2-.3.3-.5.1-.2.05-.37-.02-.52-.07-.15-.67-1.62-.92-2.22-.24-.58-.49-.5-.67-.51l-.57-.01c-.2 0-.52.07-.79.37-.27.3-1.04 1.02-1.04 2.48 0 1.46 1.06 2.87 1.21 3.07.15.2 2.09 3.2 5.07 4.49.71.3 1.26.49 1.69.62.71.23 1.36.2 1.87.12.57-.08 1.76-.72 2-1.41.25-.7.25-1.29.17-1.41-.07-.13-.27-.2-.57-.35z"/>
                  </svg>
                  {ct.waCta}
                </a>
              )}
            </div>
          ))}
          {busy && (
            <div className="chat-msg assistant">
              <div className="bubble typing">
                <span /><span /><span />
              </div>
            </div>
          )}
        </div>

        {msgs.length <= 1 && !busy && (
          <div className="chat-suggestions">
            {ct.suggestions.map((s) => (
              <button key={s} onClick={() => send(s)}>{s}</button>
            ))}
          </div>
        )}

        <form
          className="chat-input"
          onSubmit={(e) => { e.preventDefault(); send(); }}
        >
          <input
            ref={inputRef}
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={ct.placeholder}
            disabled={busy}
            maxLength={MAX_INPUT_CHARS}
            autoComplete="off"
          />
          <button type="submit" disabled={busy || !input.trim()} aria-label={ct.send}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
              <line x1="22" y1="2" x2="11" y2="13"></line>
              <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
            </svg>
          </button>
        </form>
      </div>
    </>
  );
};

Object.assign(window, { ChatBot });
