/* integrations.jsx - RAG/chat API + Firestore persistence adapters */

const MOS_CONFIG_KEY = "integrations";
const MOS_LOCAL_LOG_KEY = "chatLogs";

const DEFAULT_ASSISTANT_SYSTEM_PROMPT = `You are a polished, commercially-minded product assistant for the business represented by the uploaded knowledge base.

Your job is to help prospects and customers understand the product, compare options, answer objections, and move toward a confident next step.

Rules:
- Use the uploaded knowledge base as the source of truth.
- Sound professional, warm, specific, and useful. Avoid generic chatbot language.
- When answering sales or evaluation questions, address the user's concern directly, then support the answer with concrete details from the documents.
- If the documents do not contain enough information, say that clearly and suggest what information would be needed.
- Do not invent prices, capabilities, policies, guarantees, timelines, or technical claims that are not supported by the uploaded knowledge base.
- Keep answers concise unless the user asks for detail.`;

const DEFAULT_ASSISTANT_OPENING = "Hi, I am your product assistant. I can help answer questions from the uploaded knowledge base, explain product details, compare options, address common objections, and point you to the most relevant information.";

const DEFAULT_INTEGRATIONS = {
  mode: "i3",
  tenantId: "",
  backendBaseUrl: "",
  ownerAddress: "0x0000000000000000000000000000000000000000",
  agentName: "Model OS Bot",
  activeAgent: null,
  assistantVoice: "sales",
  systemPrompt: DEFAULT_ASSISTANT_SYSTEM_PROMPT,
  openingMessage: DEFAULT_ASSISTANT_OPENING,
  chatEndpoint: "http://localhost:8000/api/chat",
  chatApiKey: "",
  firebaseApiKey: "",
  firebaseAuthDomain: "",
  firebaseProjectId: "",
  firebaseAppId: "",
  firestoreCollection: "",
  llmProvider: "openai",
  llmApiKey: "",
  llmModel: "gpt-4o-mini",
};

const LLM_PROVIDERS = {
  openai: {
    id: "openai",
    label: "OpenAI",
    placeholder: "sk-...",
    defaultModel: "gpt-4o-mini",
    models: ["gpt-4o-mini", "gpt-4.1-mini", "gpt-4.1"],
  },
  claude: {
    id: "claude",
    label: "Claude",
    placeholder: "sk-ant-...",
    defaultModel: "claude-3-5-sonnet-latest",
    models: ["claude-3-5-sonnet-latest", "claude-3-5-haiku-latest", "claude-3-opus-latest"],
  },
  gemini: {
    id: "gemini",
    label: "Gemini",
    placeholder: "AIza...",
    defaultModel: "gemini-1.5-flash",
    models: ["gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0-flash"],
  },
  glm: {
    id: "glm",
    label: "GLM",
    placeholder: "GLM API key",
    defaultModel: "glm-4.7",
    models: ["glm-4.7", "glm-4.5-flash", "glm-4.5-air", "glm-4.6", "glm-5.1"],
  },
};

function normalizeLlmSettings(settings = loadIntegrations()) {
  const provider = LLM_PROVIDERS[settings.llmProvider] ? settings.llmProvider : DEFAULT_INTEGRATIONS.llmProvider;
  const info = LLM_PROVIDERS[provider];
  return {
    provider,
    apiKey: String(settings.llmApiKey || "").trim(),
    model: String(settings.llmModel || info.defaultModel || "").trim(),
  };
}

function hasLlmApiKey(settings = loadIntegrations()) {
  return Boolean(normalizeLlmSettings(settings).apiKey);
}

function maskedKey(value = "") {
  const key = String(value || "");
  if (!key) return "";
  if (key.length <= 8) return "****";
  return `${key.slice(0, 4)}****${key.slice(-4)}`;
}

function isSignedInUser(authUser) {
  return Boolean(authUser && !authUser.isDemo);
}

function requireSignedInUser(authUser, message = "Sign in with Google before using Model OS.") {
  if (!isSignedInUser(authUser)) throw new Error(message);
  return authUser;
}

function currentAuthUser() {
  const user = window.firebaseAuth?.currentUser || null;
  return isSignedInUser(user) ? user : null;
}

function mosLoadJson(key, fallback) {
  const user = currentAuthUser();
  return user ? window.ModelOSUserStorage.load(user.uid, key, fallback) : fallback;
}

function mosSaveJson(key, value) {
  const user = currentAuthUser();
  if (!user) throw new Error("Sign in before saving account data.");
  window.ModelOSUserStorage.save(user.uid, key, value);
}

function loadIntegrations() {
  const user = currentAuthUser();
  const settings = {
    ...DEFAULT_INTEGRATIONS,
    ...(window.MODEL_OS_CONFIG || {}),
    ...mosLoadJson(MOS_CONFIG_KEY, {}),
  };
  const backendBaseUrl = String(settings.backendBaseUrl || "");
  if (
    backendBaseUrl.includes("localhost:3001") ||
    backendBaseUrl.includes("dev-nobel-53yijxi22a-uc.a.run.app") ||
    backendBaseUrl.includes("i3-app-new-53yijxi22a-uc.a.run.app")
  ) {
    settings.backendBaseUrl = DEFAULT_INTEGRATIONS.backendBaseUrl;
  }
  if (user) {
    settings.ownerAddress = user.uid;
    settings.tenantId = user.uid;
    settings.firestoreCollection = `users/${user.uid}/conversations`;
  }
  return settings;
}

function saveIntegrations(next) {
  const settings = { ...loadIntegrations(), ...next };
  if (next.llmProvider && LLM_PROVIDERS[next.llmProvider] && !next.llmModel) {
    settings.llmModel = LLM_PROVIDERS[next.llmProvider].defaultModel;
  }
  mosSaveJson(MOS_CONFIG_KEY, settings);
  return settings;
}

function normalizeSource(source, i) {
  const doc = source.doc || source.document || source.title || source.name || source.file || `Source ${i + 1}`;
  const rawLocation = source.page || source.loc || source.location || source.url || "";
  const page = /^chunk\s*\d*$/i.test(String(rawLocation).trim()) ? "" : rawLocation;
  const score = Number(source.score ?? source.similarity ?? source.relevance ?? 0.75);
  return { doc, page: String(page), score: Number.isFinite(score) ? score : 0.75 };
}

function normalizeAnalysis(payload = {}) {
  const analysis = payload.analysis || payload.evaluation || payload.metrics || payload || {};
  const quality = Number(analysis.quality ?? payload.quality ?? payload.score ?? 0);
  const halluc = Number(analysis.hallucinationRisk ?? analysis.hallucination_risk ?? analysis.halluc ?? payload.hallucinationRisk ?? payload.halluc ?? 0);
  const voice = Number(analysis.brandVoice ?? analysis.brand_voice ?? analysis.voice ?? payload.brandVoice ?? payload.voice ?? 0);
  return {
    quality: Number.isFinite(quality) ? Math.max(0, Math.min(100, Math.round(quality))) : 0,
    halluc: Number.isFinite(halluc) ? Math.max(0, Math.min(100, Math.round(halluc))) : 0,
    voice: Number.isFinite(voice) ? Math.max(0, Math.min(100, Math.round(voice))) : 0,
    reason: String(analysis.reason || analysis.summary || "").trim(),
    unsupportedClaims: Array.isArray(analysis.unsupportedClaims) ? analysis.unsupportedClaims : [],
    supportedClaims: Array.isArray(analysis.supportedClaims) ? analysis.supportedClaims : [],
  };
}

function normalizeChatResponse(data, question) {
  const payload = data?.data || data?.result || data || {};
  const answer = payload.answer || payload.text || payload.response || payload.message || payload.output || "";
  const sources = payload.sources || payload.citations || payload.contexts || payload.documents || [];
  const rough = normalizeAnalysis(payload);

  return {
    role: "bot",
    text: String(answer || `I could not find a grounded answer for "${question}".`),
    sources: Array.isArray(sources) ? sources.map(normalizeSource) : [],
    quality: rough.quality || 0,
    halluc: rough.halluc || 0,
    voice: rough.voice || 0,
    analysis: payload.analysis || payload.evaluation ? rough : null,
    analysisStatus: payload.analysis || payload.evaluation ? "ready" : "pending",
    conversationId: payload.conversationId || payload.conversation_id || payload.threadId || payload.thread_id || null,
    raw: payload,
  };
}

function apiUrl(path) {
  const settings = loadIntegrations();
  return `${String(settings.backendBaseUrl || DEFAULT_INTEGRATIONS.backendBaseUrl || "").replace(/\/$/, "")}${path}`;
}

async function fetchWithTimeout(url, options = {}, timeoutMs = 45000, label = "request") {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...options, signal: controller.signal });
  } catch (error) {
    if (error?.name === "AbortError") {
      throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1000)}s. The model provider may be slow; try again or use a shorter instruction.`);
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

async function parseJsonResponse(res) {
  if (res.status === 304) return { notModified: true };
  const text = await res.text();
  let data = {};
  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { message: text };
    }
  }
  if (!res.ok) {
    throw new Error(data.error || data.message || `Request failed with ${res.status}`);
  }
  return data;
}

async function generateTrainingData(body, { onEvent, signal } = {}, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before generating training data.");
  const token = await authUser.getIdToken();
  const res = await fetch(apiUrl("/api/model-os/generate-training-data"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify(body || {}),
    signal,
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    let message = "";
    try { message = JSON.parse(text).error || ""; } catch { message = text; }
    throw new Error(message || `Generation failed with ${res.status}`);
  }
  if (!res.body) throw new Error("Streaming is not supported by this browser.");
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let boundary;
    while ((boundary = buffer.indexOf("\n\n")) >= 0) {
      const rawEvent = buffer.slice(0, boundary);
      buffer = buffer.slice(boundary + 2);
      let event = "message";
      const dataLines = [];
      for (const line of rawEvent.split("\n")) {
        if (line.startsWith("event:")) event = line.slice(6).trim();
        else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
      }
      if (!dataLines.length) continue;
      let payload = null;
      try { payload = JSON.parse(dataLines.join("\n")); } catch { continue; }
      if (onEvent) onEvent(event, payload);
    }
  }
}

async function loadStoredLlmKey(modelId, authUser = currentAuthUser()) {
  if (!modelId || !isSignedInUser(authUser)) return { success: true, configured: false, key: null };
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({ modelId });
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/llm-key?${params.toString()}`), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  }, 15000, "Loading the saved LLM key");
  return parseJsonResponse(res);
}

async function saveStoredLlmKey({ modelId, provider, model, apiKey, authUser = currentAuthUser() }) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before saving the LLM API key.");
  if (!modelId) throw new Error("Create or select a bot before saving the LLM API key.");
  if (!apiKey) throw new Error("Add your OpenAI, Claude, Gemini, or GLM API key before saving.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/llm-key"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      modelId,
      tenantId: authUser.uid,
      provider,
      model,
      apiKey,
    }),
  }, 20000, "Saving the LLM API key");
  return parseJsonResponse(res);
}

async function ensureStoredLlmKey({ modelId, settings = loadIntegrations(), authUser = currentAuthUser() }) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before deploying the website widget.");
  if (!modelId) throw new Error("Create or select a bot before deploying the website widget.");
  const existing = await loadStoredLlmKey(modelId, authUser);
  if (existing.configured) return existing;
  const llm = normalizeLlmSettings(settings);
  if (!llm.apiKey) throw new Error("Save your OpenAI, Claude, Gemini, or GLM API key before deploying the website widget.");
  return saveStoredLlmKey({ modelId, provider: llm.provider, model: llm.model, apiKey: llm.apiKey, authUser });
}

function fileKindForName(name = "") {
  return name.toLowerCase().endsWith(".pdf") ? "pdf" : "doc";
}

async function ensureKnowledgeAgent(authUser) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before creating a bot.");
  const settings = loadIntegrations();
  if (settings.activeAgent?.modelId) return settings.activeAgent;

  const ownerAddress = authUser?.uid || settings.ownerAddress || DEFAULT_INTEGRATIONS.ownerAddress;
  const headers = { "Content-Type": "application/json" };
  if (authUser) {
    const token = await authUser.getIdToken();
    headers.Authorization = `Bearer ${token}`;
  }

  const res = await fetchWithTimeout(apiUrl("/api/personal-agent/models"), {
    method: "POST",
    headers,
    body: JSON.stringify({
      name: settings.agentName || DEFAULT_INTEGRATIONS.agentName,
      ownerAddress,
      isPublic: false,
      purpose: "Answer questions using PDF documents uploaded in Model OS.",
      useCase: "RAG chatbot for uploaded knowledge base PDFs.",
      systemPrompt: DEFAULT_ASSISTANT_SYSTEM_PROMPT,
      openingMessage: DEFAULT_ASSISTANT_OPENING,
      voicePreset: "sales"
    }),
  }, 30000, "Creating the RAG bot");

  const data = await parseJsonResponse(res);
  const agent = {
    modelId: data.id || data.modelId || data.slug,
    name: data.name || settings.agentName || DEFAULT_INTEGRATIONS.agentName,
    ownerAddress: data.ownerAddress || ownerAddress,
    systemPrompt: data.systemPrompt || DEFAULT_ASSISTANT_SYSTEM_PROMPT,
    openingMessage: data.openingMessage || DEFAULT_ASSISTANT_OPENING,
    voicePreset: data.voicePreset || "sales",
  };
  const savedSettings = saveIntegrations({ activeAgent: agent, mode: "i3" });
  const llm = normalizeLlmSettings(savedSettings);
  if (llm.apiKey) {
    try {
      await saveStoredLlmKey({ modelId: agent.modelId, provider: llm.provider, model: llm.model, apiKey: llm.apiKey, authUser });
    } catch (error) {
      console.warn("Failed to save LLM key for the bot:", error);
    }
  }
  return agent;
}

async function uploadAndIndexPdf(file, onProgress, authUser) {
  authUser = requireSignedInUser(authUser, "Please sign in with Google before uploading a PDF.");

  if (!file || !file.name.toLowerCase().endsWith(".pdf")) {
    throw new Error("Only PDF files are supported for this RAG demo.");
  }

  const settings = loadIntegrations();
  const agent = await ensureKnowledgeAgent(authUser);
  const fileId = `file_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
  const ownerAddress = authUser.uid || agent.ownerAddress || settings.ownerAddress || DEFAULT_INTEGRATIONS.ownerAddress;
  const token = await authUser.getIdToken();

  onProgress?.({ step: "uploading", fileId, filename: file.name, agent });

  const form = new FormData();
  form.append("file", file);
  form.append("fileId", fileId);
  form.append("modelId", agent.modelId);
  form.append("ownerAddress", ownerAddress);
  form.append("filename", file.name);
  form.append("mimeType", file.type || "application/pdf");

  const uploadRes = await fetchWithTimeout(apiUrl("/api/personal-agent/files/upload"), {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    body: form,
  }, 60000, "Uploading the PDF");
  const uploadData = await parseJsonResponse(uploadRes);

  onProgress?.({ step: "indexing", fileId, filename: file.name, agent, uploadData });

  const processRes = await fetchWithTimeout(apiUrl("/api/process-rag-file"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      fileId,
      modelId: agent.modelId,
      ownerAddress,
      storagePath: uploadData.storagePath,
      filename: file.name,
    }),
  }, 120000, "Building the RAG index");
  const processData = await parseJsonResponse(processRes);

  return {
    id: fileId,
    fileId,
    name: file.name,
    filename: file.name,
    kind: fileKindForName(file.name),
    size: `${Math.max(1, Math.round(file.size / 1024))} KB`,
    chunks: processData.chunksProcessed || processData.totalChunks || 0,
    status: processData.status === "ready" ? "indexed" : processData.status,
    modelId: agent.modelId,
    agent,
    raw: { uploadData, processData },
  };
}

async function loadKnowledgeStatus(modelId, authUser = currentAuthUser()) {
  if (!modelId || !isSignedInUser(authUser)) return null;
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({
    modelId,
    ts: String(Date.now()),
  });
  const res = await fetch(apiUrl(`/api/model-os/knowledge/status?${params.toString()}`), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  });
  return await parseJsonResponse(res);
}

async function callRagChat({ question, history, model, tenantId, authUser = currentAuthUser() }) {
  const settings = loadIntegrations();
  authUser = requireSignedInUser(authUser, "Sign in with Google before using the RAG chatbot.");
  const llm = normalizeLlmSettings(settings);
  if (!llm.apiKey) {
    throw new Error("Add your OpenAI, Claude, Gemini, or GLM API key before using RAG.");
  }
  if (settings.mode === "i3") {
    const agent = settings.activeAgent;
    if (!agent?.modelId) {
      throw new Error("Upload a PDF in Knowledge base first so Model OS can create a RAG bot.");
    }

    const token = await authUser.getIdToken();
    const res = await fetchWithTimeout(apiUrl("/api/model-os/chat"), {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
      body: JSON.stringify({
        modelId: agent.modelId,
        modelName: agent.name,
        tenantId: authUser.uid,
        question,
        messages: history.map(m => ({
          role: m.role === "bot" ? "assistant" : m.role,
          content: m.text,
        })),
        llmProvider: llm.provider,
        llmModel: llm.model,
        llmApiKey: llm.apiKey,
        llm: {
          provider: llm.provider,
          model: llm.model,
          apiKey: llm.apiKey,
        },
        topK: 2,
        maxTokens: 600,
      }),
    }, 120000, "Calling the RAG chatbot");
    return normalizeChatResponse(await parseJsonResponse(res), question);
  }

  if (settings.mode !== "api") return null;
  if (!settings.chatEndpoint) throw new Error("Chat API endpoint is not configured.");

  const headers = { "Content-Type": "application/json" };
  if (settings.chatApiKey) headers.Authorization = `Bearer ${settings.chatApiKey}`;

  const res = await fetch(settings.chatEndpoint, {
    method: "POST",
    headers,
    body: JSON.stringify({
      tenantId: tenantId || settings.tenantId,
      message: question,
      question,
      query: question,
      history: history.map(m => ({ role: m.role === "bot" ? "assistant" : "user", content: m.text })),
      model,
      llmProvider: llm.provider,
      llmModel: llm.model,
      llmApiKey: llm.apiKey,
      llm: {
        provider: llm.provider,
        model: llm.model,
        apiKey: llm.apiKey,
      },
      stream: false,
    }),
  });

  if (!res.ok) {
    const detail = await res.text().catch(() => "");
    throw new Error(`Chat API returned ${res.status}${detail ? `: ${detail.slice(0, 180)}` : ""}`);
  }
  return normalizeChatResponse(await res.json(), question);
}

async function evaluateRagAnswer({ question, answer, sources, conversationId, model, modelId, authUser = currentAuthUser() }) {
  const settings = loadIntegrations();
  authUser = requireSignedInUser(authUser, "Sign in with Google before evaluating answers.");
  const llm = normalizeLlmSettings(settings);
  if (!llm.apiKey) throw new Error("Add your OpenAI, Claude, Gemini, or GLM API key before evaluating answers.");
  const active = settings.activeAgent || {};
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/evaluate-answer"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      modelId: modelId || active.modelId,
      modelName: model || active.name,
      tenantId: authUser.uid,
      question,
      answer,
      sources: sources || [],
      conversationId,
      persistToFirestore: true,
      source: "playground",
      llmProvider: llm.provider,
      llmModel: llm.model,
      llmApiKey: llm.apiKey,
      llm: { provider: llm.provider, model: llm.model, apiKey: llm.apiKey },
      topK: 2,
      maxTokens: 450,
    }),
  }, 45000, "Evaluating the answer");
  const data = await parseJsonResponse(res);
  return normalizeAnalysis(data.analysis || data.evaluation || data.metrics || data);
}

function LlmApiKeyPanel({ settings, setSettings, activeAgent, authUser = currentAuthUser(), compact = false }) {
  const current = normalizeLlmSettings(settings);
  const providerInfo = LLM_PROVIDERS[current.provider] || LLM_PROVIDERS.openai;
  const modelId = activeAgent?.modelId || settings.activeAgent?.modelId || "";
  const [show, setShow] = React.useState(false);
  const [storedKey, setStoredKey] = React.useState(null);
  const [savingKey, setSavingKey] = React.useState(false);
  const [keyStatus, setKeyStatus] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    if (!modelId || !isSignedInUser(authUser)) {
      setStoredKey(null);
      return;
    }
    loadStoredLlmKey(modelId, authUser)
      .then(data => { if (!cancelled) setStoredKey(data.key || null); })
      .catch(() => { if (!cancelled) setStoredKey(null); });
    return () => { cancelled = true; };
  }, [modelId, authUser?.uid]);

  const update = (patch) => {
    const next = window.ModelOSIntegrations.save({ ...settings, ...patch });
    setSettings?.(next);
    setKeyStatus(null);
  };

  const changeProvider = (provider) => {
    const info = LLM_PROVIDERS[provider] || LLM_PROVIDERS.openai;
    update({ llmProvider: provider, llmModel: info.defaultModel });
  };

  const saveKey = async () => {
    if (!modelId) {
      setKeyStatus({ type: "info", text: "Create or select a bot, then save this key for the website widget." });
      return;
    }
    const latest = normalizeLlmSettings(loadIntegrations());
    if (!latest.apiKey) {
      setKeyStatus({ type: "error", text: "Add the provider API key before saving." });
      return;
    }
    setSavingKey(true);
    setKeyStatus(null);
    try {
      const data = await saveStoredLlmKey({ modelId, provider: latest.provider, model: latest.model, apiKey: latest.apiKey, authUser });
      setStoredKey(data.key || null);
      setKeyStatus({ type: "success", text: "Saved securely for this bot and website widget." });
    } catch (error) {
      setKeyStatus({ type: "error", text: error.message || "Could not save the LLM API key." });
    } finally {
      setSavingKey(false);
    }
  };

  const storedConfigured = Boolean(storedKey?.configured);
  const badgeText = storedConfigured ? `Saved: ${storedKey.provider}` : current.apiKey ? "Ready to save" : "Required";

  return (
    <div className="stack" style={{ gap: compact ? 9 : 12 }}>
      <div className="between" style={{ gap: 10 }}>
        <div>
          <div className="h3">LLM provider key</div>
          <div className="caption">Used by RAG chat and encrypted for this bot's website widget.</div>
        </div>
        <span className={cx("badge", storedConfigured || current.apiKey ? "badge-good" : "badge-warn")} style={{ flex: "none" }}>
          {I(storedConfigured || current.apiKey ? "check" : "lock", { style: { width: 12, height: 12 } })}
          {badgeText}
        </span>
      </div>

      <div>
        <span className="field-label">Provider</span>
        <div className="row" style={{ gap: 7, flexWrap: "wrap" }}>
          {Object.values(LLM_PROVIDERS).map(provider => (
            <button key={provider.id} type="button" className={cx("btn btn-sm", current.provider === provider.id ? "btn-primary" : "btn-secondary")} onClick={() => changeProvider(provider.id)}>
              {provider.label}
            </button>
          ))}
        </div>
      </div>

      <label>
        <span className="field-label">Provider API key</span>
        <div className="row" style={{ gap: 8 }}>
          <input className="input" type={show ? "text" : "password"} value={current.apiKey}
                 autoComplete="off" spellCheck="false" placeholder={storedConfigured ? storedKey.masked || providerInfo.placeholder : providerInfo.placeholder}
                 onChange={e => update({ llmApiKey: e.target.value })} />
          <button className="icon-btn" type="button" title={show ? "Hide API key" : "Show API key"} onClick={() => setShow(v => !v)} style={{ width: 38, height: 38, flex: "none" }}>
            {I(show ? "eye" : "lock", { style: { width: 16, height: 16 } })}
          </button>
        </div>
      </label>

      <div className="row wrap" style={{ gap: 8 }}>
        <button className="btn btn-secondary" type="button" onClick={saveKey} disabled={savingKey || !current.apiKey || !modelId}>
          {I("lock")} {savingKey ? "Saving..." : storedConfigured ? "Update saved key" : "Save for widget"}
        </button>
        {storedConfigured && <span className="caption">Encrypted on the backend for this bot.</span>}
      </div>
      {keyStatus && <div className="caption" style={{ color: keyStatus.type === "error" ? "var(--danger)" : "var(--ink-2)" }}>{keyStatus.text}</div>}
      <div className="caption">The full key is only sent to your backend when you save or run RAG chat. It is never included in website widget code.</div>
    </div>
  );
}
let firestoreAppKey = null;
let firestoreDb = null;

function getFirestore(settings = loadIntegrations()) {
  if (!settings.firebaseApiKey || !settings.firebaseProjectId || !settings.firebaseAppId) return null;
  if (!window.firebase?.initializeApp || !window.firebase?.firestore) return null;

  const appKey = `${settings.firebaseProjectId}:${settings.firebaseAppId}`;
  if (!firestoreDb || firestoreAppKey !== appKey) {
    const app = window.firebase.apps.find(a => a.options?.projectId === settings.firebaseProjectId)
      || window.firebase.initializeApp({
        apiKey: settings.firebaseApiKey,
        authDomain: settings.firebaseAuthDomain || `${settings.firebaseProjectId}.firebaseapp.com`,
        projectId: settings.firebaseProjectId,
        appId: settings.firebaseAppId,
      }, appKey);
    firestoreDb = app.firestore();
    firestoreAppKey = appKey;
  }
  return firestoreDb;
}

function loadLocalChatLogs() {
  return mosLoadJson(MOS_LOCAL_LOG_KEY, []);
}

function saveLocalChatLog(entry) {
  const existing = loadLocalChatLogs();
  const id = entry.id || entry.conversationId;
  const rest = id ? existing.filter(item => (item.id || item.conversationId) !== id) : existing;
  const previous = id ? existing.find(item => (item.id || item.conversationId) === id) : null;
  const next = [{ ...(previous || {}), ...entry }, ...rest].slice(0, 100);
  mosSaveJson(MOS_LOCAL_LOG_KEY, next);
}

async function persistChatLog({ question, answer, sources, metrics, evaluation, model, modelId, tenantId, conversationId, mode, authUser = currentAuthUser() }) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before saving conversations.");
  const settings = loadIntegrations();
  const entry = {
    id: conversationId || `local-${Date.now()}`,
    tenantId: authUser.uid,
    ownerUid: authUser.uid,
    question,
    answer,
    sources: sources || [],
    metrics: metrics || {},
    evaluation: evaluation || null,
    model: model || null,
    mode: mode || settings.mode,
    createdAt: new Date().toISOString(),
  };
  saveLocalChatLog(entry);
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/conversations"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({ ...entry, modelId }),
  }, 15000, "Saving the conversation");
  const data = await parseJsonResponse(res);
  return { ...entry, persisted: Boolean(data.persisted) };
}

async function loadTrainingDatasetStats(modelId, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before loading training data.");
  if (!modelId) throw new Error("Select a bot before loading training data.");
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({ modelId });
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/training-dataset/stats?${params.toString()}`), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  }, 15000, "Loading training data stats");
  const data = await parseJsonResponse(res);
  return data.stats || {};
}

async function listDataVersions(modelId, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before loading data versions.");
  if (!modelId) return [];
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({ modelId });
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/data-versions?${params.toString()}`), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  }, 15000, "Loading data versions");
  const data = await parseJsonResponse(res);
  return Array.isArray(data.versions) ? data.versions : [];
}

// LLM-as-a-Judge rubric stored server-side; generation, improvement, and
// evaluation all use the same document.
async function getJudgeRubric(authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before loading the judge rubric.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/judge-rubric"), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  }, 15000, "Loading the judge rubric");
  return parseJsonResponse(res);
}

async function createDataVersion({ modelId, name, parentVersionId, inherit }, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before creating a data version.");
  if (!modelId) throw new Error("Select a bot before creating a data version.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/data-versions"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({ modelId, name: name || "", parentVersionId: parentVersionId || null, inherit: inherit !== false }),
  }, 60000, "Creating data version");
  return parseJsonResponse(res);
}

async function updateVersionMembers(versionId, { add, remove }, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before updating a data version.");
  if (!versionId) throw new Error("Select a data version first.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/data-versions/${encodeURIComponent(versionId)}/members`), {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({ add: add || [], remove: remove || [] }),
  }, 15000, "Updating data version");
  return parseJsonResponse(res);
}

async function listDataRecommendations(versionId, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before loading recommendations.");
  if (!versionId) return [];
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({ versionId });
  const res = await fetchWithTimeout(apiUrl(`/api/sft/data-recommendations?${params.toString()}`), {
    cache: "no-store",
    headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
  }, 15000, "Loading recommendations");
  const data = await parseJsonResponse(res);
  return Array.isArray(data.recommendations) ? data.recommendations : [];
}

async function prepareTrainingDataset(modelId, authUser = currentAuthUser(), versionId) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before preparing training data.");
  if (!modelId) throw new Error("Select a bot before preparing training data.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/sft/training-dataset/prepare"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({ modelId, versionId: versionId || null }),
  }, 120000, "Preparing training data");
  return parseJsonResponse(res);
}

async function applyRecommendation(body, { onEvent, signal } = {}, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before improving the dataset.");
  const token = await authUser.getIdToken();
  const res = await fetch(apiUrl("/api/model-os/apply-recommendation"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify(body || {}),
    signal,
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    let message = "";
    try { message = JSON.parse(text).error || ""; } catch { message = text; }
    throw new Error(message || `Improvement failed with ${res.status}`);
  }
  if (!res.body) throw new Error("Streaming is not supported by this browser.");
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let boundary;
    while ((boundary = buffer.indexOf("\n\n")) >= 0) {
      const rawEvent = buffer.slice(0, boundary);
      buffer = buffer.slice(boundary + 2);
      let event = "message";
      const dataLines = [];
      for (const line of rawEvent.split("\n")) {
        if (line.startsWith("event:")) event = line.slice(6).trim();
        else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
      }
      if (!dataLines.length) continue;
      let payload = null;
      try { payload = JSON.parse(dataLines.join("\n")); } catch { continue; }
      if (onEvent) onEvent(event, payload);
    }
  }
}

async function downloadTrainingDataset(modelId, split = "train", authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before exporting training data.");
  if (!modelId) throw new Error("Select a bot before exporting training data.");
  const token = await authUser.getIdToken();
  const params = new URLSearchParams({ modelId, split });
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/training-dataset?${params.toString()}`), {
    headers: { Authorization: `Bearer ${token}` },
  }, 60000, "Exporting training data");
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(text || `Export failed with ${res.status}`);
  }
  const blob = await res.blob();
  const disposition = res.headers.get("Content-Disposition") || "";
  const match = disposition.match(/filename="([^"]+)"/);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = match ? match[1] : `modelos-${split}-${modelId}.jsonl`;
  a.click();
  URL.revokeObjectURL(url);
}

async function updateConversationApproval(conversationId, patch, authUser = currentAuthUser()) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before updating training data.");
  const body = {};
  if (patch && patch.approved !== undefined) body.approved = Boolean(patch.approved);
  if (patch && patch.rejected !== undefined) body.rejected = Boolean(patch.rejected);
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl(`/api/model-os/conversations/${encodeURIComponent(conversationId)}`), {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify(body),
  }, 15000, "Updating training data");
  return parseJsonResponse(res);
}

async function loadFirestoreChatLogs(max = 3000, authUser = currentAuthUser()) {
  if (!isSignedInUser(authUser)) return [];
  const token = await authUser.getIdToken();
  const BATCH = 500;
  const all = [];
  let startAfter = "";
  for (let round = 0; round < 10; round++) {
    const params = new URLSearchParams({ limit: String(BATCH) });
    if (startAfter) params.set("startAfter", startAfter);
    const res = await fetchWithTimeout(apiUrl(`/api/model-os/conversations?${params.toString()}`), {
      cache: "no-store",
      headers: { Authorization: `Bearer ${token}`, "Cache-Control": "no-cache" },
    }, 15000, "Loading conversations");
    const data = await parseJsonResponse(res);
    const batch = Array.isArray(data.conversations) ? data.conversations : [];
    all.push(...batch);
    if (batch.length < BATCH || all.length >= max) break;
    const last = batch[batch.length - 1]?.createdAt;
    const lastMs = last ? Date.parse(last) : NaN;
    if (!Number.isFinite(lastMs)) break;
    startAfter = new Date(lastMs).toISOString();
  }
  return all;
}

async function listAgents(authUser) {
  if (!isSignedInUser(authUser)) return [];
  try {
    const ownerAddress = authUser.uid;
    const token = await authUser.getIdToken();
    const res = await fetchWithTimeout(
      apiUrl(`/api/personal-agent/models?ownerAddress=${encodeURIComponent(ownerAddress)}`),
      { headers: { Authorization: `Bearer ${token}` } },
      15000,
      "Listing agents",
    );
    const data = await parseJsonResponse(res);
    return data.models || [];
  } catch {
    return [];
  }
}

async function deleteFile(fileId, authUser) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before deleting files.");
  const ownerAddress = authUser.uid;
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(
    apiUrl(`/api/personal-agent/files/${encodeURIComponent(fileId)}?ownerAddress=${encodeURIComponent(ownerAddress)}`),
    { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
    60000,
    "Deleting file",
  );
  return parseJsonResponse(res);
}

async function generateSuggestedQuestions({ modelId, agentName, authUser = currentAuthUser() }) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before generating suggested questions.");
  const settings = loadIntegrations();
  const llm = normalizeLlmSettings(settings);
  if (!llm.apiKey) throw new Error("Add your OpenAI, Claude, Gemini, or GLM API key before generating questions.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/suggested-questions"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      modelId,
      agentName,
      llmProvider: llm.provider,
      llmModel: llm.model,
      llmApiKey: llm.apiKey,
      llm: { provider: llm.provider, model: llm.model, apiKey: llm.apiKey },
      maxTokens: 300,
    }),
  }, 60000, "Generating suggested questions");
  const data = await parseJsonResponse(res);
  return Array.isArray(data.questions) ? data.questions.slice(0, 3) : [];
}
async function optimizeAssistantSetup({ openingMessage, systemPrompt, instruction, target = "both", agentName, authUser = currentAuthUser() }) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before optimizing assistant setup.");
  const settings = loadIntegrations();
  const llm = normalizeLlmSettings(settings);
  if (!llm.apiKey) throw new Error("Add your OpenAI, Claude, Gemini, or GLM API key before optimizing.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/model-os/assistant-setup/optimize"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      openingMessage,
      systemPrompt,
      instruction,
      target,
      agentName,
      llmProvider: llm.provider,
      llmModel: llm.model,
      llmApiKey: llm.apiKey,
      llm: { provider: llm.provider, model: llm.model, apiKey: llm.apiKey },
      maxTokens: target === "systemPrompt" ? 650 : 260,
    }),
  }, 120000, "Optimizing assistant setup");
  return parseJsonResponse(res);
}
async function updateAgent(modelId, patch, authUser) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before updating bot setup.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(
    apiUrl(`/api/personal-agent/models/${encodeURIComponent(modelId)}`),
    {
      method: "PATCH",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
      body: JSON.stringify(patch || {}),
    },
    30000,
    "Saving assistant setup",
  );
  const data = await parseJsonResponse(res);
  const model = data.model || data;
  const settings = loadIntegrations();
  const current = settings.activeAgent;
  if (current?.modelId === modelId) {
    saveIntegrations({
      activeAgent: {
        ...current,
        name: model.name || current.name,
        systemPrompt: model.systemPrompt || current.systemPrompt,
        openingMessage: model.openingMessage || current.openingMessage,
        voicePreset: model.voicePreset || current.voicePreset,
        setupCustomized: model.setupCustomized === true,
      },
      systemPrompt: model.systemPrompt || settings.systemPrompt,
      openingMessage: model.openingMessage || settings.openingMessage,
      assistantVoice: model.voicePreset || settings.assistantVoice,
    });
  }
  return model;
}
async function deleteAgent(modelId, authUser) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before deleting bots.");
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(
    apiUrl(`/api/personal-agent/models/${encodeURIComponent(modelId)}`),
    { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
    60000,
    "Deleting bot",
  );
  return parseJsonResponse(res);
}
async function createAgent(name, authUser, options = {}) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before creating a bot.");
  const ownerAddress = authUser.uid;
  const settings = loadIntegrations();
  const systemPrompt = options.systemPrompt || DEFAULT_ASSISTANT_SYSTEM_PROMPT;
  const openingMessage = options.openingMessage || DEFAULT_ASSISTANT_OPENING;
  const voicePreset = options.voicePreset || "sales";
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/personal-agent/models"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      name: name || "Untitled bot",
      ownerAddress,
      isPublic: false,
      purpose: "Answer questions using PDF documents uploaded in Model OS.",
      useCase: "RAG chatbot for uploaded knowledge base PDFs.",
      systemPrompt,
      openingMessage,
      voicePreset,
      setupCustomized: options.setupCustomized === true
    }),
  }, 30000, "Creating the RAG bot");

  const data = await parseJsonResponse(res);
  const agent = {
    modelId: data.id || data.modelId || data.slug,
    name: data.name || name || "Untitled bot",
    ownerAddress: data.ownerAddress || ownerAddress,
    systemPrompt: data.systemPrompt || systemPrompt,
    openingMessage: data.openingMessage || openingMessage,
    voicePreset: data.voicePreset || voicePreset,
    setupCustomized: data.setupCustomized === true,
  };
  const savedSettings = saveIntegrations({ activeAgent: agent, mode: "i3" });
  const llm = normalizeLlmSettings(savedSettings);
  if (llm.apiKey) {
    try {
      await saveStoredLlmKey({ modelId: agent.modelId, provider: llm.provider, model: llm.model, apiKey: llm.apiKey, authUser });
    } catch (error) {
      console.warn("Failed to save LLM key for the bot:", error);
    }
  }
  return agent;
}

async function indexExistingFile(storagePath, filename, modelId, authUser) {
  authUser = requireSignedInUser(authUser, "Sign in with Google before indexing files.");
  const fileId = `file_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
  const token = await authUser.getIdToken();
  const res = await fetchWithTimeout(apiUrl("/api/process-rag-file"), {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({ fileId, modelId, storagePath, filename }),
  }, 120000, "Indexing file to new bot");
  const data = await parseJsonResponse(res);
  return { fileId, filename, status: data.status || "ready", chunks: data.chunksProcessed || data.totalChunks || 0 };
}

Object.assign(window, {
  ModelOSIntegrations: {
    defaults: DEFAULT_INTEGRATIONS,
    llmProviders: LLM_PROVIDERS,
    normalizeLlmSettings,
    hasLlmApiKey,
    ensureStoredLlmKey,
    saveStoredLlmKey,
    loadStoredLlmKey,
    load: loadIntegrations,
    save: saveIntegrations,
    ensureKnowledgeAgent,
    uploadAndIndexPdf,
    loadKnowledgeStatus,
    callRagChat,
    evaluateRagAnswer,
    persistChatLog,
    loadFirestoreChatLogs,
    updateConversationApproval,
    loadTrainingDatasetStats,
    downloadTrainingDataset,
    prepareTrainingDataset,
    listDataVersions,
    getJudgeRubric,
    createDataVersion,
    updateVersionMembers,
    listDataRecommendations,
    applyRecommendation,
    generateTrainingData,
    loadLocalChatLogs,
    listAgents,
    deleteFile,
    generateSuggestedQuestions,
    optimizeAssistantSetup,
    updateAgent,
    deleteAgent,
    createAgent,
    indexExistingFile,
  },
  ModelOSLlmApiKeyPanel: LlmApiKeyPanel,
});
