/* AI provider definitions and dispatcher.
   Each provider exports: { id, name, needsKey, defaultBaseUrl, models, call }.
   Published on window.PROVIDERS and window.callAI. */

const SETTINGS_KEY = "ai_provider_settings_v1";

/* ---- response normalizers ---- */
function extractTextFromOpenAI(resp) {
  return resp?.choices?.[0]?.message?.content || "";
}
function extractTextFromAnthropic(resp) {
  if (Array.isArray(resp?.content)) {
    return resp.content.map(b => b.text || "").join("");
  }
  return resp?.content || resp?.completion || "";
}
function extractTextFromGemini(resp) {
  const parts = resp?.candidates?.[0]?.content?.parts;
  if (Array.isArray(parts)) return parts.map(p => p.text || "").join("");
  return "";
}

/* ---- shared fetch with friendly errors ---- */
async function postJSON(url, opts = {}) {
  let res;
  try {
    res = await fetch(url, opts);
  } catch (err) {
    throw new Error(
      "Network call failed. If you're calling a local server (Ollama, LM Studio), make sure CORS is allowed and the server is running. " +
      "If calling a hosted API directly from the browser, the provider may block cross-origin requests. " +
      "Original: " + err.message
    );
  }
  if (!res.ok) {
    const txt = await res.text().catch(() => "");
    let detail = txt.slice(0, 400);
    try {
      const errJson = JSON.parse(txt);
      detail = errJson?.error?.message || detail;
    } catch (_) { /* keep raw text */ }
    if (res.status === 429) {
      const zeroQuota = /limit:\s*0\b/i.test(txt);
      throw new Error(
        zeroQuota
          ? "No free-tier quota for this model (HTTP 429, limit: 0). Your API key works, but Google has not granted free requests for this model on your account. " +
            "Gemini 2.5 Pro is often paid-only — switch to Gemini 2.5 Flash or Flash-Lite, link a billing account at aistudio.google.com (free tier may still apply), or use Built-in / another provider."
          : "Rate limit / quota exceeded (HTTP 429). Your key is valid but this model or account has no free-tier requests left. " +
            "Try Gemini 2.5 Flash or Flash-Lite, wait for the daily quota reset, enable billing in Google AI Studio, or use another provider. " +
            "Details: " + detail
      );
    }
    if (res.status === 401 || res.status === 403) {
      throw new Error(`Authentication failed (HTTP ${res.status}). Check your API key. ${detail}`);
    }
    throw new Error(`HTTP ${res.status} — ${detail}`);
  }
  return res.json();
}

async function callOpenAIChatCompletions({ baseUrl, apiKey, model, system, user, maxTokens, extraHeaders }) {
  const base = baseUrl.replace(/\/+$/, "");
  const resp = await postJSON(base + "/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(apiKey ? { Authorization: "Bearer " + apiKey } : {}),
      ...extraHeaders,
    },
    body: JSON.stringify({
      model,
      max_tokens: maxTokens || 1000,
      messages: [
        { role: "system", content: system },
        { role: "user", content: user },
      ],
    }),
  });
  return extractTextFromOpenAI(resp);
}

/* ---------------- providers ---------------- */
const PROVIDERS = [
  {
    id: "builtin",
    name: "Built-in (Claude, no key)",
    short: "Built-in",
    needsKey: false,
    needsBaseUrl: false,
    isDefault: true,
    description: "Uses the runtime's built-in Claude connection. No API key needed. Default.",
    models: [
      { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
    ],
    async call({ model, system, user, maxTokens }) {
      if (!window.claude || !window.claude.complete) {
        throw new Error("Built-in Claude is not available in this environment. Select another provider and supply an API key.");
      }
      const resp = await window.claude.complete({
        model: model || "claude-sonnet-4-20250514",
        max_tokens: maxTokens || 1000,
        system,
        messages: [{ role: "user", content: user }],
      });
      if (typeof resp === "string") return resp;
      if (resp?.content) return extractTextFromAnthropic(resp);
      if (resp?.completion) return resp.completion;
      return "";
    },
  },

  {
    id: "anthropic",
    name: "Anthropic API",
    short: "Anthropic",
    needsKey: true,
    needsBaseUrl: false,
    description: "Direct calls to api.anthropic.com. Requires the dangerous-direct-browser-access header — your API key will be exposed to the page.",
    keyHelp: "Get a key at console.anthropic.com — starts with sk-ant-",
    models: [
      { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" },
      { id: "claude-sonnet-4-20250514",   name: "Claude Sonnet 4" },
      { id: "claude-opus-4-1-20250805",   name: "Claude Opus 4.1" },
      { id: "claude-3-5-haiku-20241022",  name: "Claude Haiku 3.5" },
    ],
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Anthropic API key is required.");
      const resp = await postJSON("https://api.anthropic.com/v1/messages", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": apiKey,
          "anthropic-version": "2023-06-01",
          "anthropic-dangerous-direct-browser-access": "true",
        },
        body: JSON.stringify({
          model,
          max_tokens: maxTokens || 1000,
          system,
          messages: [{ role: "user", content: user }],
        }),
      });
      return extractTextFromAnthropic(resp);
    },
  },

  {
    id: "openai",
    name: "OpenAI",
    short: "OpenAI",
    needsKey: true,
    needsBaseUrl: false,
    description: "Direct calls to api.openai.com.",
    keyHelp: "Get a key at platform.openai.com — starts with sk-",
    models: [
      { id: "gpt-4.1",       name: "GPT-4.1" },
      { id: "gpt-4.1-mini",  name: "GPT-4.1 mini" },
      { id: "gpt-4o",        name: "GPT-4o" },
      { id: "gpt-4o-mini",   name: "GPT-4o mini" },
      { id: "o3-mini",       name: "o3-mini" },
    ],
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("OpenAI API key is required.");
      const resp = await postJSON("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": "Bearer " + apiKey,
        },
        body: JSON.stringify({
          model,
          max_completion_tokens: maxTokens || 1000,
          messages: [
            { role: "system", content: system },
            { role: "user",   content: user },
          ],
        }),
      });
      return extractTextFromOpenAI(resp);
    },
  },

  {
    id: "google",
    name: "Google Gemini",
    short: "Gemini",
    needsKey: true,
    needsBaseUrl: false,
    description: "Direct calls to generativelanguage.googleapis.com. Free tier: Flash / Flash-Lite only — Pro may return limit: 0 without billing.",
    keyHelp: "Get a key at aistudio.google.com/app/apikey",
    defaultModelId: "gemini-2.5-flash",
    models: [
      { id: "gemini-2.5-flash",         name: "Gemini 2.5 Flash (recommended)" },
      { id: "gemini-2.5-flash-lite",    name: "Gemini 2.5 Flash-Lite" },
      { id: "gemini-2.0-flash",         name: "Gemini 2.0 Flash" },
      { id: "gemini-2.5-pro",           name: "Gemini 2.5 Pro (paid / billing)" },
      { id: "gemini-1.5-pro",           name: "Gemini 1.5 Pro" },
    ],
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Google API key is required.");
      const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
      const resp = await postJSON(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          systemInstruction: { parts: [{ text: system }] },
          contents: [{ role: "user", parts: [{ text: user }] }],
          generationConfig: { maxOutputTokens: maxTokens || 1000 },
        }),
      });
      return extractTextFromGemini(resp);
    },
  },

  {
    id: "groq",
    name: "Groq",
    short: "Groq",
    needsKey: true,
    needsBaseUrl: false,
    description: "Fast inference via api.groq.com (OpenAI-compatible).",
    keyHelp: "Get a key at console.groq.com — starts with gsk_",
    defaultModelId: "llama-3.3-70b-versatile",
    models: [
      { id: "llama-3.3-70b-versatile",  name: "Llama 3.3 70B" },
      { id: "llama-3.1-8b-instant",    name: "Llama 3.1 8B Instant" },
      { id: "mixtral-8x7b-32768",      name: "Mixtral 8x7B" },
      { id: "gemma2-9b-it",            name: "Gemma 2 9B" },
    ],
    allowCustomModel: true,
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Groq API key is required.");
      return callOpenAIChatCompletions({
        baseUrl: "https://api.groq.com/openai/v1",
        apiKey, model, system, user, maxTokens,
      });
    },
  },

  {
    id: "openrouter",
    name: "OpenRouter",
    short: "OpenRouter",
    needsKey: true,
    needsBaseUrl: false,
    description: "100+ models via openrouter.ai (OpenAI-compatible). Use a preset or enter any model ID from openrouter.ai/models.",
    keyHelp: "Get a key at openrouter.ai/keys",
    defaultModelId: "openai/gpt-4o-mini",
    models: [
      { id: "openai/gpt-4o-mini",                    name: "GPT-4o mini" },
      { id: "anthropic/claude-sonnet-4",            name: "Claude Sonnet 4" },
      { id: "google/gemini-2.5-flash",              name: "Gemini 2.5 Flash" },
      { id: "meta-llama/llama-3.3-70b-instruct",    name: "Llama 3.3 70B" },
      { id: "meta-llama/llama-3.3-70b-instruct:free", name: "Llama 3.3 70B (free)" },
    ],
    allowCustomModel: true,
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("OpenRouter API key is required.");
      return callOpenAIChatCompletions({
        baseUrl: "https://openrouter.ai/api/v1",
        apiKey, model, system, user, maxTokens,
        extraHeaders: {
          "HTTP-Referer": (typeof location !== "undefined" && location.origin) ? location.origin : "http://localhost",
          "X-Title": "Post Generator",
        },
      });
    },
  },

  {
    id: "mistral",
    name: "Mistral",
    short: "Mistral",
    needsKey: true,
    needsBaseUrl: false,
    description: "Direct calls to api.mistral.ai. Free tier with rate limits — good writing quality; no credit card required to start.",
    keyHelp: "Get a key at console.mistral.ai — free tier available",
    defaultModelId: "mistral-small-latest",
    models: [
      { id: "mistral-small-latest",   name: "Mistral Small (recommended, free tier)" },
      { id: "open-mistral-nemo",      name: "Mistral Nemo 12B (free tier)" },
      { id: "mistral-medium-latest",  name: "Mistral Medium" },
      { id: "mistral-large-latest",   name: "Mistral Large" },
      { id: "codestral-latest",       name: "Codestral" },
    ],
    allowCustomModel: true,
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Mistral API key is required.");
      return callOpenAIChatCompletions({
        baseUrl: "https://api.mistral.ai/v1",
        apiKey, model, system, user, maxTokens,
      });
    },
  },

  {
    id: "deepseek",
    name: "DeepSeek",
    short: "DeepSeek",
    needsKey: true,
    needsBaseUrl: false,
    description: "Direct calls to api.deepseek.com. New accounts get free credits; very cheap after credits expire.",
    keyHelp: "Get a key at platform.deepseek.com",
    defaultModelId: "deepseek-chat",
    models: [
      { id: "deepseek-chat",      name: "DeepSeek Chat (recommended)" },
      { id: "deepseek-v4-flash",  name: "DeepSeek V4 Flash" },
      { id: "deepseek-v4-pro",    name: "DeepSeek V4 Pro" },
      { id: "deepseek-reasoner",  name: "DeepSeek Reasoner" },
    ],
    allowCustomModel: true,
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("DeepSeek API key is required.");
      return callOpenAIChatCompletions({
        baseUrl: "https://api.deepseek.com",
        apiKey, model, system, user, maxTokens,
      });
    },
  },

  {
    id: "huggingface",
    name: "Hugging Face Inference",
    short: "Hugging Face",
    needsKey: true,
    needsBaseUrl: false,
    description: "OpenAI-compatible router at router.huggingface.co. Free tier with rate limits — token needs Inference Providers permission.",
    keyHelp: "Get a token at huggingface.co/settings/tokens (Inference Providers scope)",
    defaultModelId: "meta-llama/Meta-Llama-3.1-8B-Instruct",
    models: [
      { id: "meta-llama/Meta-Llama-3.1-8B-Instruct", name: "Llama 3.1 8B Instruct" },
      { id: "meta-llama/Meta-Llama-3.1-70B-Instruct", name: "Llama 3.1 70B Instruct" },
      { id: "Qwen/Qwen2.5-7B-Instruct",             name: "Qwen 2.5 7B Instruct" },
      { id: "mistralai/Mistral-7B-Instruct-v0.3",   name: "Mistral 7B Instruct" },
      { id: "google/gemma-2-9b-it",                 name: "Gemma 2 9B" },
    ],
    allowCustomModel: true,
    async call({ apiKey, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Hugging Face token is required.");
      return callOpenAIChatCompletions({
        baseUrl: "https://router.huggingface.co/v1",
        apiKey, model, system, user, maxTokens,
      });
    },
  },

  {
    id: "cloudflare",
    name: "Cloudflare Workers AI",
    short: "Cloudflare",
    needsKey: true,
    needsBaseUrl: true,
    defaultBaseUrl: "https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/ai/v1",
    description: "Runs models on Cloudflare Workers AI. Free tier — replace YOUR_ACCOUNT_ID in the base URL with your Cloudflare account ID.",
    keyHelp: "Workers AI API token from dash.cloudflare.com → Workers AI → Use REST API",
    defaultModelId: "@cf/meta/llama-3.1-8b-instruct",
    models: [
      { id: "@cf/meta/llama-3.1-8b-instruct",  name: "Llama 3.1 8B Instruct" },
      { id: "@cf/meta/llama-3.2-3b-instruct",  name: "Llama 3.2 3B Instruct" },
      { id: "@cf/mistral/mistral-7b-instruct-v0.1", name: "Mistral 7B Instruct" },
      { id: "@cf/google/gemma-2-9b-it",        name: "Gemma 2 9B" },
      { id: "@cf/meta/llama-3.1-70b-instruct", name: "Llama 3.1 70B Instruct" },
    ],
    allowCustomModel: true,
    async call({ apiKey, baseUrl, model, system, user, maxTokens }) {
      if (!apiKey) throw new Error("Cloudflare API token is required.");
      if (!baseUrl || baseUrl.includes("YOUR_ACCOUNT_ID")) {
        throw new Error("Cloudflare base URL is required — replace YOUR_ACCOUNT_ID with your account ID from the Cloudflare dashboard.");
      }
      return callOpenAIChatCompletions({
        baseUrl,
        apiKey, model, system, user, maxTokens,
      });
    },
  },

  {
    id: "ollama",
    name: "Ollama (local)",
    short: "Ollama",
    needsKey: false,
    needsBaseUrl: true,
    defaultBaseUrl: "http://localhost:11434",
    description: "Runs models on your machine via Ollama. Start the server with `ollama serve`, pull a model, then enter the base URL. May require CORS — start Ollama with OLLAMA_ORIGINS='*' if calls fail.",
    models: [
      { id: "llama3.2",         name: "Llama 3.2" },
      { id: "llama3.1",         name: "Llama 3.1" },
      { id: "qwen2.5",          name: "Qwen 2.5" },
      { id: "mistral",          name: "Mistral" },
      { id: "gemma2",           name: "Gemma 2" },
      { id: "phi3",             name: "Phi 3" },
      { id: "deepseek-r1",      name: "DeepSeek R1" },
    ],
    allowCustomModel: true,
    async call({ baseUrl, model, system, user, maxTokens }) {
      const base = (baseUrl || "http://localhost:11434").replace(/\/+$/, "");
      const resp = await postJSON(base + "/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          model,
          stream: false,
          messages: [
            { role: "system", content: system },
            { role: "user",   content: user },
          ],
          options: { num_predict: maxTokens || 1000 },
        }),
      });
      return resp?.message?.content || "";
    },
  },

  {
    id: "openai-compatible",
    name: "Custom (OpenAI-compatible)",
    short: "Custom",
    needsKey: true,
    needsBaseUrl: true,
    defaultBaseUrl: "https://api.groq.com/openai/v1",
    description: "Any OpenAI-compatible endpoint — Groq, Together, Fireworks, OpenRouter, LM Studio, vLLM, etc. Provide a base URL + key + model name.",
    keyHelp: "From your provider's dashboard.",
    models: [],
    allowCustomModel: true,
    async call({ apiKey, baseUrl, model, system, user, maxTokens }) {
      if (!baseUrl) throw new Error("Base URL is required.");
      return callOpenAIChatCompletions({ baseUrl, apiKey, model, system, user, maxTokens });
    },
  },
];

/** Docs & API key URLs — shown in Settings → Provider docs & keys */
const PROVIDER_DOC_LINKS = [
  { providerId: "google",       name: "Gemini",        docsUrl: "https://ai.google.dev/gemini-api/docs",              keysUrl: "https://aistudio.google.com/apikey" },
  { providerId: "openai",       name: "OpenAI",        docsUrl: "https://platform.openai.com/docs/",                  keysUrl: "https://platform.openai.com/api-keys" },
  { providerId: "anthropic",    name: "Anthropic",     docsUrl: "https://docs.anthropic.com/",                        keysUrl: "https://console.anthropic.com/" },
  { providerId: "groq",         name: "Groq",          docsUrl: "https://console.groq.com/docs",                      keysUrl: "https://console.groq.com/" },
  { providerId: "openrouter",   name: "OpenRouter",    docsUrl: "https://openrouter.ai/docs",                         keysUrl: "https://openrouter.ai/keys" },
  { providerId: "huggingface",  name: "Hugging Face",  docsUrl: "https://huggingface.co/docs",                        keysUrl: "https://huggingface.co/settings/tokens" },
  { providerId: "mistral",      name: "Mistral",       docsUrl: "https://docs.mistral.ai/",                           keysUrl: "https://console.mistral.ai/" },
  { providerId: "deepseek",     name: "DeepSeek",      docsUrl: "https://api-docs.deepseek.com/",                     keysUrl: "https://platform.deepseek.com/" },
  { providerId: "cloudflare",   name: "Cloudflare Workers AI", docsUrl: "https://developers.cloudflare.com/workers-ai/", keysUrl: "https://dash.cloudflare.com/" },
  { providerId: "ollama",       name: "Ollama",        docsUrl: "https://github.com/ollama/ollama/blob/main/docs/api.md", keysUrl: "https://ollama.com/download" },
];

function getProviderDocLinks() {
  return PROVIDER_DOC_LINKS.filter(row => isProviderAvailable(row.providerId));
}

const LOCAL_ONLY_PROVIDER_IDS = new Set(["builtin", "ollama"]);

function isProviderAvailable(providerId) {
  if (!LOCAL_ONLY_PROVIDER_IDS.has(providerId)) return true;
  return typeof window.isLocalDevHost === "function" && window.isLocalDevHost();
}

function getAvailableProviders() {
  return PROVIDERS.filter(p => isProviderAvailable(p.id));
}

function getProvider(id) {
  const found = PROVIDERS.find(p => p.id === id);
  if (found && isProviderAvailable(id)) return found;
  return getAvailableProviders()[0] || PROVIDERS[0];
}

/* ---------------- settings persistence ---------------- */
const DEFAULT_SETTINGS = {
  providerId: "builtin",
  model: "claude-sonnet-4-20250514",
  apiKey: "",
  baseUrl: "",
  customModel: "",
  authorByline: "",
  authorAvatarDataUrl: "",
};

const HOSTED_DEFAULT_SETTINGS = {
  providerId: "google",
  model: "gemini-2.5-flash",
  apiKey: "",
  baseUrl: "",
  customModel: "",
  authorByline: "",
  authorAvatarDataUrl: "",
};

function getDefaultSettings() {
  return isProviderAvailable("builtin") ? { ...DEFAULT_SETTINGS } : { ...HOSTED_DEFAULT_SETTINGS };
}

function sanitizeSettings(settings) {
  const merged = { ...getDefaultSettings(), ...settings };
  if (!isProviderAvailable(merged.providerId)) {
    const def = getDefaultSettings();
    return {
      ...def,
      apiKey: settings.apiKey || "",
      baseUrl: def.baseUrl || "",
      customModel: "",
      authorByline: settings.authorByline || "",
      authorAvatarDataUrl: settings.authorAvatarDataUrl || "",
    };
  }
  return merged;
}

async function loadSettings() {
  try {
    if (!window.storage || !window.storage.get) return sanitizeSettings(getDefaultSettings());
    const raw = await window.storage.get(SETTINGS_KEY);
    if (!raw) return sanitizeSettings(getDefaultSettings());
    const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
    return sanitizeSettings(parsed);
  } catch (err) {
    console.warn("settings.get failed", err);
    return sanitizeSettings(getDefaultSettings());
  }
}
async function saveSettings(settings) {
  try {
    if (!window.storage || !window.storage.set) return;
    await window.storage.set(SETTINGS_KEY, JSON.stringify(settings));
  } catch (err) {
    console.warn("settings.set failed", err);
  }
}

/* ---------------- dispatcher ---------------- */
async function callAI({ settings, system, user, maxTokens }) {
  const provider = getProvider(settings.providerId);
  const modelToUse = (provider.allowCustomModel && settings.customModel)
    ? settings.customModel
    : settings.model;
  if (!modelToUse) throw new Error("No model selected.");

  const text = await provider.call({
    apiKey:  settings.apiKey,
    baseUrl: settings.baseUrl || provider.defaultBaseUrl,
    model:   modelToUse,
    system, user, maxTokens,
  });
  return (text || "").trim();
}

function describeSettings(settings) {
  const p = getProvider(settings.providerId);
  const m = (p.allowCustomModel && settings.customModel) ? settings.customModel : settings.model;
  return `${p.short} · ${m || "(no model)"}`;
}

Object.assign(window, {
  PROVIDERS, PROVIDER_DOC_LINKS, getProviderDocLinks, getAvailableProviders, getProvider, isProviderAvailable,
  loadSettings, saveSettings, DEFAULT_SETTINGS, HOSTED_DEFAULT_SETTINGS, getDefaultSettings, sanitizeSettings,
  callAI, describeSettings,
  SETTINGS_KEY,
});
