// Prism Translate — provider adapters.
// One unified interface: callLLM(provider, messages) and translateBatch/translateText.
// Runs in the service worker (host_permissions: <all_urls> bypass CORS for these hosts).

import { langName } from "./storage.js";

class ProviderError extends Error {}

// ---- Low-level chat call. Returns { text, usage:{inputTokens,outputTokens}, ms, model } ----
export async function callLLM(provider, { system, user, temperature, maxTokens = 4096, signal }) {
  const t0 = performance.now();
  let out;
  switch (provider.type) {
    case "openai":
      out = await callOpenAI(provider, { system, user, temperature, maxTokens, signal });
      break;
    case "claude":
      out = await callClaude(provider, { system, user, temperature, maxTokens, signal });
      break;
    case "gemini":
      out = await callGemini(provider, { system, user, temperature, maxTokens, signal });
      break;
    case "deepl":
      throw new ProviderError("DeepL 不支持自由对话，请用 translate* 接口");
    default:
      throw new ProviderError("未知服务商类型: " + provider.type);
  }
  out.ms = Math.round(performance.now() - t0);
  out.model = provider.model;
  return out;
}

function trimBase(url) {
  return (url || "").replace(/\/+$/, "");
}

async function callOpenAI(provider, { system, user, temperature, maxTokens, signal }) {
  const url = trimBase(provider.baseURL) + "/chat/completions";
  const body = {
    model: provider.model,
    messages: [
      ...(system ? [{ role: "system", content: system }] : []),
      { role: "user", content: user },
    ],
    temperature: temperature ?? provider.temperature ?? 0.2,
    max_tokens: maxTokens,
  };
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + provider.apiKey,
    },
    body: JSON.stringify(body),
    signal,
  });
  if (!res.ok) throw new ProviderError(`${provider.name} HTTP ${res.status}: ${(await safeText(res)).slice(0, 300)}`);
  const data = await res.json();
  const text = data.choices?.[0]?.message?.content?.trim() ?? "";
  return {
    text,
    usage: {
      inputTokens: data.usage?.prompt_tokens ?? 0,
      outputTokens: data.usage?.completion_tokens ?? 0,
    },
  };
}

async function callClaude(provider, { system, user, temperature, maxTokens, signal }) {
  const url = trimBase(provider.baseURL) + "/v1/messages";
  const body = {
    model: provider.model,
    max_tokens: maxTokens,
    temperature: temperature ?? provider.temperature ?? 0.2,
    ...(system ? { system } : {}),
    messages: [{ role: "user", content: user }],
  };
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": provider.apiKey,
      "anthropic-version": "2023-06-01",
      "anthropic-dangerous-direct-browser-access": "true",
    },
    body: JSON.stringify(body),
    signal,
  });
  if (!res.ok) throw new ProviderError(`${provider.name} HTTP ${res.status}: ${(await safeText(res)).slice(0, 300)}`);
  const data = await res.json();
  const text = (data.content || []).map((b) => b.text || "").join("").trim();
  return {
    text,
    usage: {
      inputTokens: data.usage?.input_tokens ?? 0,
      outputTokens: data.usage?.output_tokens ?? 0,
    },
  };
}

async function callGemini(provider, { system, user, temperature, maxTokens, signal }) {
  const base = trimBase(provider.baseURL);
  const url = `${base}/v1beta/models/${provider.model}:generateContent?key=${encodeURIComponent(provider.apiKey)}`;
  const body = {
    ...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}),
    contents: [{ role: "user", parts: [{ text: user }] }],
    generationConfig: {
      temperature: temperature ?? provider.temperature ?? 0.2,
      maxOutputTokens: maxTokens,
    },
  };
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal,
  });
  if (!res.ok) throw new ProviderError(`${provider.name} HTTP ${res.status}: ${(await safeText(res)).slice(0, 300)}`);
  const data = await res.json();
  const text = (data.candidates?.[0]?.content?.parts || []).map((p) => p.text || "").join("").trim();
  return {
    text,
    usage: {
      inputTokens: data.usageMetadata?.promptTokenCount ?? 0,
      outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0,
    },
  };
}

async function callDeepL(provider, texts, { targetLang, sourceLang, signal }) {
  const url = trimBase(provider.baseURL) + "/v2/translate";
  const params = new URLSearchParams();
  texts.forEach((t) => params.append("text", t));
  params.append("target_lang", deeplLang(targetLang));
  if (sourceLang && sourceLang !== "auto") params.append("source_lang", deeplLang(sourceLang));
  const t0 = performance.now();
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: "DeepL-Auth-Key " + provider.apiKey,
    },
    body: params.toString(),
    signal,
  });
  if (!res.ok) throw new ProviderError(`${provider.name} HTTP ${res.status}: ${(await safeText(res)).slice(0, 300)}`);
  const data = await res.json();
  return {
    translations: (data.translations || []).map((t) => t.text),
    ms: Math.round(performance.now() - t0),
    chars: texts.join("").length,
  };
}

function deeplLang(code) {
  const map = { "zh-CN": "ZH", "zh-TW": "ZH", en: "EN", ja: "JA", ko: "KO", fr: "FR", de: "DE", es: "ES", ru: "RU", pt: "PT", it: "IT" };
  return map[code] || code.toUpperCase().split("-")[0];
}

async function safeText(res) {
  try {
    return await res.text();
  } catch {
    return "";
  }
}

// ---- High-level: translate a single string ----
export async function translateText(provider, text, { targetLang, sourceLang, system, signal }) {
  if (provider.type === "deepl") {
    const r = await callDeepL(provider, [text], { targetLang, sourceLang, signal });
    return { text: r.translations[0] || "", usage: { inputTokens: 0, outputTokens: 0, chars: r.chars }, ms: r.ms, model: "deepl" };
  }
  const user = `把下面的文本翻译为${langName(targetLang)}：\n\n${text}`;
  return await callLLM(provider, { system, user, signal });
}

// ---- High-level: translate an array of segments in one request (batched), with fallback ----
export async function translateBatch(provider, segments, { targetLang, sourceLang, system, signal }) {
  if (!segments.length) return { translations: [], usage: { inputTokens: 0, outputTokens: 0 }, ms: 0 };

  if (provider.type === "deepl") {
    const r = await callDeepL(provider, segments, { targetLang, sourceLang, signal });
    return { translations: r.translations, usage: { inputTokens: 0, outputTokens: 0, chars: r.chars }, ms: r.ms, model: "deepl" };
  }

  if (segments.length === 1) {
    const r = await translateText(provider, segments[0], { targetLang, sourceLang, system, signal });
    return { translations: [r.text], usage: r.usage, ms: r.ms, model: r.model };
  }

  const numbered = segments.map((s, i) => ({ i, t: s }));
  const user =
    `把下面 JSON 数组里每一项翻译为${langName(targetLang)}。` +
    `严格返回一个 JSON 数组（仅数组，无解释、无 markdown 代码块），长度必须是 ${segments.length}，顺序与输入一致，每项是对应输入的译文字符串。\n\n` +
    JSON.stringify(numbered.map((n) => n.t));

  const r = await callLLM(provider, { system, user, signal });
  const parsed = parseJsonArray(r.text);
  if (parsed && parsed.length === segments.length) {
    return { translations: parsed.map((x) => String(x)), usage: r.usage, ms: r.ms, model: r.model };
  }

  // Fallback: per-segment, concurrency-limited, so a flaky batch never drops content.
  const translations = await mapLimit(segments, 4, async (seg) => {
    const one = await translateText(provider, seg, { targetLang, sourceLang, system, signal });
    return one.text;
  });
  return {
    translations,
    usage: { inputTokens: r.usage.inputTokens, outputTokens: r.usage.outputTokens },
    ms: r.ms,
    model: r.model,
    degraded: true,
  };
}

function parseJsonArray(s) {
  if (!s) return null;
  let txt = s.trim();
  // Strip ```json ... ``` fences if present.
  const fence = txt.match(/```(?:json)?\s*([\s\S]*?)```/i);
  if (fence) txt = fence[1].trim();
  const start = txt.indexOf("[");
  const end = txt.lastIndexOf("]");
  if (start === -1 || end === -1 || end < start) return null;
  try {
    const arr = JSON.parse(txt.slice(start, end + 1));
    return Array.isArray(arr) ? arr : null;
  } catch {
    return null;
  }
}

export async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let idx = 0;
  async function worker() {
    while (idx < items.length) {
      const cur = idx++;
      results[cur] = await fn(items[cur], cur);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}
