// engine.jsx — 真实能力层：多引擎 AI（可扩展）+ 导入（文本/Word/图片/PDF）+ 本地保存

// ============ AI 引擎注册表（可扩展）============
// api: 'openai'（兼容 /chat/completions）| 'anthropic' | 'gemini'
const AI_BUILTINS = [
  { id: 'deepseek', name: 'DeepSeek', sub: '深度求索', av: 'D', api: 'openai',    base: 'https://api.deepseek.com',                              models: ['deepseek-chat', 'deepseek-reasoner'], keyHint: 'sk-…' },
  { id: 'claude',   name: 'Claude',   sub: 'Anthropic', av: 'C', api: 'anthropic', base: 'https://api.anthropic.com',                             models: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'], keyHint: 'sk-ant-…' },
  { id: 'openai',   name: 'ChatGPT',  sub: 'OpenAI',    av: 'O', api: 'openai',    base: 'https://api.openai.com/v1',                             models: ['gpt-4o', 'gpt-4o-mini'], keyHint: 'sk-…' },
  { id: 'qwen',     name: '通义千问', sub: '阿里云',    av: '通', api: 'openai',    base: 'https://dashscope.aliyuncs.com/compatible-mode/v1',     models: ['qwen-max', 'qwen-plus', 'qwen-turbo'], keyHint: 'sk-…' },
  { id: 'grok',     name: 'Grok',     sub: 'xAI',       av: 'X', api: 'openai',    base: 'https://api.x.ai/v1',                                   models: ['grok-2-latest', 'grok-beta'], keyHint: 'xai-…' },
  { id: 'gemini',   name: 'Gemini',   sub: 'Google',    av: 'G', api: 'gemini',    base: 'https://generativelanguage.googleapis.com',            models: ['gemini-1.5-pro', 'gemini-1.5-flash'], keyHint: 'AIza…' },
  { id: 'local',    name: '本地模型', sub: 'Ollama 等', av: '本', api: 'openai',    base: 'http://localhost:11434/v1',                             models: ['llama3.1', 'qwen2.5'], keyHint: '可留空' },
];

function getCustomProviders() {
  try { return JSON.parse(localStorage.getItem('popor.ai.custom') || '[]'); } catch (_) { return []; }
}
function saveCustomProviders(list) { localStorage.setItem('popor.ai.custom', JSON.stringify(list)); }
function addCustomProvider(p) {
  const list = getCustomProviders();
  const id = 'custom-' + Date.now();
  list.push({ id, name: p.name || '自定义引擎', sub: '自定义', av: (p.name || 'C')[0], api: p.api || 'openai',
    base: p.base || '', models: (p.models && p.models.length) ? p.models : ['default'], keyHint: 'API Key', custom: true });
  saveCustomProviders(list);
  return id;
}
function removeCustomProvider(id) {
  saveCustomProviders(getCustomProviders().filter(p => p.id !== id));
  localStorage.removeItem('popor.ai.key.' + id);
}
function getProviders() { return AI_BUILTINS.concat(getCustomProviders()); }
function getProvider(id) { return getProviders().find(p => p.id === id) || AI_BUILTINS[0]; }

function getActiveId() { return localStorage.getItem('popor.ai.active') || 'deepseek'; }
function setActiveId(id) { localStorage.setItem('popor.ai.active', id); }
function getKey(id) { return localStorage.getItem('popor.ai.key.' + id) || ''; }
function setKey(id, v) { v ? localStorage.setItem('popor.ai.key.' + id, v) : localStorage.removeItem('popor.ai.key.' + id); }
function getBase(id) { return localStorage.getItem('popor.ai.base.' + id) || getProvider(id).base; }
function setBase(id, v) { v ? localStorage.setItem('popor.ai.base.' + id, v) : localStorage.removeItem('popor.ai.base.' + id); }
function getModel(id) { return localStorage.getItem('popor.ai.model.' + id) || getProvider(id).models[0]; }
function setModel(id, v) { localStorage.setItem('popor.ai.model.' + id, v); }
function hasActiveKey() { const id = getActiveId(); const p = getProvider(id); return p.id === 'local' || !!getKey(id); }
function activeProviderName() { return getProvider(getActiveId()).name; }
// 可选代理：填了就把请求转发给你部署的代理（绕过浏览器跨域）
function getProxy() { return localStorage.getItem('popor.ai.proxy') || ''; }
function setProxy(v) { v ? localStorage.setItem('popor.ai.proxy', v) : localStorage.removeItem('popor.ai.proxy'); }
function proxied(url) { const px = getProxy(); return px ? px.replace(/\/+$/, '') + '/' + url : url; }

// 统一对话入口：按当前引擎分派
async function aiChat(messages, opts) {
  opts = opts || {};
  const id = getActiveId();
  const p = getProvider(id);
  const key = getKey(id);
  if (!key && p.id !== 'local') { const e = new Error('未配置「' + p.name + '」密钥'); e.code = 'NO_KEY'; throw e; }
  const base = getBase(id).replace(/\/+$/, '');
  const model = opts.model || getModel(id);
  try {
    if (p.api === 'anthropic') return await callAnthropic(base, key, model, messages, opts);
    if (p.api === 'gemini')    return await callGemini(base, key, model, messages, opts);
    return await callOpenAI(base, key, model, messages, opts);
  } catch (err) {
    if (err.code) throw err;
    const e = new Error('网络/跨域受阻：浏览器未能直连「' + p.name + '」。可改用服务端代理。');
    e.code = 'NETWORK'; e.cause = err; throw e;
  }
}

async function httpJson(url, init) {
  const res = await fetch(url, init);
  if (!res.ok) {
    let detail = '';
    try { const j = await res.json(); detail = (j.error && (j.error.message || j.error.type)) || ''; } catch (_) {}
    const e = new Error('返回 ' + res.status + (detail ? ' · ' + detail : ''));
    e.code = res.status === 401 ? 'AUTH' : 'HTTP'; throw e;
  }
  return res.json();
}

async function callOpenAI(base, key, model, messages, opts) {
  const headers = { 'Content-Type': 'application/json' };
  if (key) headers['Authorization'] = 'Bearer ' + key;
  const data = await httpJson(proxied(base + '/chat/completions'), {
    method: 'POST', headers, signal: opts.signal,
    body: JSON.stringify({ model, messages, temperature: opts.temperature != null ? opts.temperature : 0.7, stream: false }),
  });
  return (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || '';
}

async function callAnthropic(base, key, model, messages, opts) {
  const sys = messages.filter(m => m.role === 'system').map(m => m.content).join('\n');
  const msgs = messages.filter(m => m.role !== 'system').map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content }));
  const data = await httpJson(proxied(base + '/v1/messages'), {
    method: 'POST', signal: opts.signal,
    headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' },
    body: JSON.stringify({ model, max_tokens: opts.max_tokens || 1024, system: sys || undefined, messages: msgs, temperature: opts.temperature != null ? opts.temperature : 0.7 }),
  });
  return (data.content && data.content.map(c => c.text || '').join('')) || '';
}

async function callGemini(base, key, model, messages, opts) {
  const sys = messages.filter(m => m.role === 'system').map(m => m.content).join('\n');
  const contents = messages.filter(m => m.role !== 'system').map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] }));
  const body = { contents };
  if (sys) body.systemInstruction = { parts: [{ text: sys }] };
  const data = await httpJson(proxied(base + '/v1beta/models/' + model + ':generateContent?key=' + encodeURIComponent(key)), {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: opts.signal, body: JSON.stringify(body),
  });
  const cand = data.candidates && data.candidates[0];
  return (cand && cand.content && cand.content.parts && cand.content.parts.map(p => p.text || '').join('')) || '';
}

async function aiPing() {
  return (await aiChat([{ role: 'user', content: '回复两个字：在的' }], { temperature: 0, max_tokens: 16 })).trim();
}

// ============ 语音引擎（TTS + 克隆）============
// 国外：ElevenLabs（克隆+合成）；国内：千问 Qwen-TTS、CosyVoice（通义/阿里云百炼，需走代理/服务端）
const TTS_PROVIDERS = [
  { id: 'elevenlabs', name: 'ElevenLabs', sub: '国外 · 克隆+合成', keyHint: 'ElevenLabs API Key' },
  { id: 'qwen',       name: '千问 · Qwen-TTS', sub: '国内 · 阿里云百炼', keyHint: 'DashScope API Key' },
  { id: 'cosyvoice',  name: 'CosyVoice',  sub: '国内 · 通义/阿里云百炼', keyHint: 'DashScope API Key' },
];
const EL_VOICES = [
  { id: '21m00Tcm4TlvDq8ikWAM', name: 'Rachel · 沉稳女声' },
  { id: 'pNInz6obpgDQGcFmaJgB', name: 'Adam · 磁性男声' },
  { id: 'EXAVITQu4vr4xnSDxMaL', name: 'Bella · 温柔女声' },
];
// 千问 Qwen-TTS 内置音色（阿里云百炼）
const QWEN_VOICES = [
  { id: 'Cherry',  name: 'Cherry · 清亮女声' },
  { id: 'Serena',  name: 'Serena · 沉稳女声' },
  { id: 'Ethan',   name: 'Ethan · 温润男声' },
  { id: 'Chelsie', name: 'Chelsie · 亲切女声' },
];
function ttsVoiceList(prov) { return (prov || getTtsProvider()) === 'qwen' ? QWEN_VOICES : EL_VOICES; }
function getTtsProvider() { return localStorage.getItem('popor.tts.prov') || 'elevenlabs'; }
function setTtsProvider(v) { localStorage.setItem('popor.tts.prov', v); }
function getTtsKey(prov) { return localStorage.getItem('popor.tts.key.' + (prov || getTtsProvider())) || ''; }
function setTtsKey(prov, v) { v ? localStorage.setItem('popor.tts.key.' + prov, v) : localStorage.removeItem('popor.tts.key.' + prov); }
function getTtsVoice() {
  const prov = getTtsProvider();
  const stored = localStorage.getItem('popor.tts.voice.' + prov) || localStorage.getItem('popor.tts.voice');
  const list = ttsVoiceList(prov);
  return (stored && list.some(v => v.id === stored)) ? stored : list[0].id;
}
function setTtsVoice(v) {
  localStorage.setItem('popor.tts.voice', v);
  localStorage.setItem('popor.tts.voice.' + getTtsProvider(), v);
}
function getClonedVoice() { try { return JSON.parse(localStorage.getItem('popor.tts.cloned') || 'null'); } catch (_) { return null; } }
function setClonedVoice(v) { v ? localStorage.setItem('popor.tts.cloned', JSON.stringify(v)) : localStorage.removeItem('popor.tts.cloned'); }
function hasTts() { return !!getTtsKey(); }

// 文本 → 语音，返回可播放的 blob URL
async function ttsSynthesize(text, opts) {
  opts = opts || {};
  const prov = getTtsProvider();
  const key = getTtsKey(prov);
  if (!key) { const e = new Error('未配置语音引擎密钥'); e.code = 'NO_KEY'; throw e; }

  // 千问 Qwen-TTS（阿里云百炼）—— 返回音频 URL，再取回为 blob
  if (prov === 'qwen') {
    const voice = opts.voiceId || getTtsVoice();
    let res;
    try {
      res = await fetch(proxied('https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'), {
        method: 'POST',
        headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json' },
        body: JSON.stringify({ model: 'qwen-tts', input: { text: text, voice: voice } }),
      });
    } catch (err) { const e = new Error('千问 TTS 网络/跨域受阻，建议在墨吞·进阶填入代理地址'); e.code = 'NETWORK'; throw e; }
    if (!res.ok) {
      let d = ''; try { const j = await res.json(); d = j.message || (j.code || ''); } catch (_) {}
      const e = new Error('千问 TTS 失败 ' + res.status + (d ? ' · ' + d : '')); e.code = res.status === 401 ? 'AUTH' : 'HTTP'; throw e;
    }
    const data = await res.json();
    const url = data && data.output && data.output.audio && (data.output.audio.url || data.output.audio.data);
    if (!url) { const e = new Error('千问 TTS 未返回音频'); e.code = 'EMPTY'; throw e; }
    try { return URL.createObjectURL(await (await fetch(proxied(url))).blob()); }
    catch (_) { return url; }   // 取回受阻时直接给 URL，<audio> 仍可播
  }

  if (prov === 'cosyvoice') {
    const e = new Error('CosyVoice 需经服务端/代理调用，请先在墨吞·进阶配置代理，或改用千问 / ElevenLabs');
    e.code = 'PROXY'; throw e;
  }
  const voice = opts.voiceId || getTtsVoice();
  let res;
  try {
    res = await fetch(proxied('https://api.elevenlabs.io/v1/text-to-speech/' + voice), {
      method: 'POST',
      headers: { 'xi-api-key': key, 'Content-Type': 'application/json', 'Accept': 'audio/mpeg' },
      body: JSON.stringify({ text: text, model_id: 'eleven_multilingual_v2' }),
    });
  } catch (err) { const e = new Error('网络/跨域受阻，建议在墨吞·进阶填入代理地址'); e.code = 'NETWORK'; throw e; }
  if (!res.ok) {
    let d = ''; try { const j = await res.json(); d = (j.detail && (j.detail.message || j.detail)) || ''; } catch (_) {}
    const e = new Error('语音合成失败 ' + res.status + (d ? ' · ' + d : '')); e.code = res.status === 401 ? 'AUTH' : 'HTTP'; throw e;
  }
  return URL.createObjectURL(await res.blob());
}

// 即时克隆：上传一段你的录音 → 生成你的专属音色 voice_id
async function cloneVoice(name, audioBlob) {
  const prov = getTtsProvider();
  if (prov !== 'elevenlabs') { const e = new Error('当前仅 ElevenLabs 支持浏览器内即时克隆'); e.code = 'UNSUPPORTED'; throw e; }
  const key = getTtsKey('elevenlabs');
  if (!key) { const e = new Error('未配置 ElevenLabs 密钥'); e.code = 'NO_KEY'; throw e; }
  const fd = new FormData();
  fd.append('name', name || ('我的声音 · ' + new Date().toLocaleDateString()));
  fd.append('files', audioBlob, 'sample.webm');
  let res;
  try { res = await fetch(proxied('https://api.elevenlabs.io/v1/voices/add'), { method: 'POST', headers: { 'xi-api-key': key }, body: fd }); }
  catch (err) { const e = new Error('网络/跨域受阻，建议填入代理地址'); e.code = 'NETWORK'; throw e; }
  if (!res.ok) {
    let d = ''; try { const j = await res.json(); d = (j.detail && (j.detail.message || j.detail)) || ''; } catch (_) {}
    const e = new Error('创建音色失败 ' + res.status + (d ? ' · ' + d : '')); e.code = 'HTTP'; throw e;
  }
  const data = await res.json();
  const voice = { id: data.voice_id, name: name || '我的声音' };
  setClonedVoice(voice);
  return voice;
}

// ============ 导入：文件 → { title, body, image, kind } ============
function baseName(name) { return (name || '文件').replace(/\.[^.]+$/, ''); }

async function inflateRaw(u8) {
  if (typeof DecompressionStream === 'undefined') throw new Error('当前浏览器不支持解压，请用 TXT 导入');
  const ds = new DecompressionStream('deflate-raw');
  const stream = new Blob([u8]).stream().pipeThrough(ds);
  return new Uint8Array(await new Response(stream).arrayBuffer());
}
async function inflateAny(u8) {
  for (const fmt of ['deflate', 'deflate-raw']) {
    try {
      const ds = new DecompressionStream(fmt);
      const stream = new Blob([u8]).stream().pipeThrough(ds);
      return new Uint8Array(await new Response(stream).arrayBuffer());
    } catch (_) {}
  }
  return null;
}

function parseZip(buf) {
  const dv = new DataView(buf);
  const u8 = new Uint8Array(buf);
  let eocd = -1;
  for (let i = u8.length - 22; i >= 0; i--) { if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; } }
  if (eocd < 0) throw new Error('文件不是有效的 docx');
  const cdCount = dv.getUint16(eocd + 10, true);
  let p = dv.getUint32(eocd + 16, true);
  const entries = [];
  for (let i = 0; i < cdCount; i++) {
    if (dv.getUint32(p, true) !== 0x02014b50) break;
    const method = dv.getUint16(p + 10, true);
    const compSize = dv.getUint32(p + 20, true);
    const nameLen = dv.getUint16(p + 28, true);
    const extraLen = dv.getUint16(p + 30, true);
    const commentLen = dv.getUint16(p + 32, true);
    const localOffset = dv.getUint32(p + 42, true);
    const name = new TextDecoder().decode(u8.subarray(p + 46, p + 46 + nameLen));
    entries.push({ name, method, compSize, localOffset });
    p += 46 + nameLen + extraLen + commentLen;
  }
  return { dv, u8, entries };
}
async function readZipEntry(z, entry) {
  const { dv, u8 } = z;
  const lo = entry.localOffset;
  const nameLen = dv.getUint16(lo + 26, true);
  const extraLen = dv.getUint16(lo + 28, true);
  const start = lo + 30 + nameLen + extraLen;
  const data = u8.subarray(start, start + entry.compSize);
  return entry.method === 0 ? data : inflateRaw(data);
}
function decodeEntities(s) { const ta = document.createElement('textarea'); ta.innerHTML = s; return ta.value; }
function docxXmlToText(xml) {
  let s = xml;
  s = s.replace(/<w:tab\b[^>]*\/?>/g, '\t').replace(/<w:br\b[^>]*\/?>/g, '\n').replace(/<\/w:p>/g, '\n').replace(/<[^>]+>/g, '');
  s = decodeEntities(s);
  return s.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
}
function extractLegacyDoc(buf) {
  const u8 = new Uint8Array(buf);
  let out = '';
  for (let i = 0; i < u8.length - 1; i++) {
    const lo = u8[i], hi = u8[i + 1];
    if (hi === 0 && lo >= 32 && lo < 127) { out += String.fromCharCode(lo); i++; continue; }
    if (lo === 0x0d || lo === 0x07) out += '\n';
  }
  return out.replace(/[^\S\n]{4,}/g, '  ').replace(/\n{3,}/g, '\n\n').trim();
}

// PDF 文本尽力提取（FlateDecode 流 → Tj/TJ 文字）
function unescapePdf(s) {
  return s.replace(/\\([nrtbf()\\]|[0-7]{1,3})/g, (m, g) => {
    if (g === 'n') return '\n'; if (g === 'r') return ''; if (g === 't') return '\t';
    if (g === 'b' || g === 'f') return ''; if (g === '(' || g === ')' || g === '\\') return g;
    return String.fromCharCode(parseInt(g, 8));
  });
}
function extractPdfOps(content) {
  let out = '';
  const re = /\[((?:[^\[\]\\]|\\.)*)\]\s*TJ|(\((?:\\.|[^()\\])*\))\s*Tj/g;
  let m;
  while ((m = re.exec(content)) !== null) {
    const seg = m[0];
    const pre = /\((?:\\.|[^()\\])*\)/g; let p2;
    while ((p2 = pre.exec(seg)) !== null) out += unescapePdf(p2[0].slice(1, -1));
    out += seg.endsWith('Tj') ? '\n' : ' ';
  }
  return out;
}
async function pdfToText(buf) {
  const u8 = new Uint8Array(buf);
  const dec = new TextDecoder('latin1');
  const whole = dec.decode(u8);
  let result = '';
  const sre = /stream\r?\n/g; let m;
  while ((m = sre.exec(whole)) !== null) {
    const start = m.index + m[0].length;
    const end = whole.indexOf('endstream', start);
    if (end < 0) continue;
    const bytes = u8.subarray(start, end);
    const inf = await inflateAny(bytes);
    if (inf) {
      const txt = dec.decode(inf);
      if (/(Tj|TJ)/.test(txt)) result += extractPdfOps(txt) + '\n';
    }
  }
  return result.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
}

function readAsDataURL(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(r.result);
    r.onerror = reject;
    r.readAsDataURL(file);
  });
}

// blob: / object URL → 自包含的 data: URL（导出后仍可独立打开）
async function blobUrlToDataUrl(url) {
  if (!url || /^data:/.test(url)) return url;
  const blob = await (await fetch(url)).blob();
  return readAsDataURL(blob);
}

async function readImportedFile(file) {
  const lname = (file.name || '').toLowerCase();
  const type = file.type || '';
  if (type.startsWith('image/') || /\.(png|jpe?g|gif|webp|heic|bmp)$/.test(lname)) {
    const image = await readAsDataURL(file);
    return { title: baseName(file.name), body: '', image, kind: 'image' };
  }
  if (lname.endsWith('.pdf') || type === 'application/pdf') {
    const buf = await file.arrayBuffer();
    let body = '';
    try { body = await pdfToText(buf); } catch (_) {}
    return { title: baseName(file.name), body, kind: 'pdf', lossy: !body };
  }
  if (lname.endsWith('.docx')) {
    const buf = await file.arrayBuffer();
    const z = parseZip(buf);
    const doc = z.entries.find(e => e.name === 'word/document.xml');
    if (!doc) throw new Error('docx 结构异常，未找到正文');
    const xml = new TextDecoder().decode(await readZipEntry(z, doc));
    return { title: baseName(file.name), body: docxXmlToText(xml), kind: 'docx' };
  }
  if (lname.endsWith('.doc')) {
    const buf = await file.arrayBuffer();
    return { title: baseName(file.name), body: extractLegacyDoc(buf), kind: 'doc', lossy: true };
  }
  return { title: baseName(file.name), body: await file.text(), kind: 'text' };
}

// ============ 保存：导出到本地 + 本机存档 ============
function downloadFile(filename, content, mime) {
  const blob = (content instanceof Blob) ? content : new Blob([content], { type: mime || 'text/plain;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1500);
}
function safeName(s) { return (s || '未命名').replace(/[\\/:*?"<>|]+/g, ' ').trim().slice(0, 60); }
function archiveLocally(entry) {
  let list = [];
  try { list = JSON.parse(localStorage.getItem('popor.archive') || '[]'); } catch (_) {}
  list.unshift(entry); list = list.slice(0, 200);
  localStorage.setItem('popor.archive', JSON.stringify(list));
  return list;
}
function contentHash(str) {
  let h1 = 0x811c9dc5, h2 = 0x1000193;
  for (let i = 0; i < str.length; i++) {
    const c = str.charCodeAt(i);
    h1 = (h1 ^ c) >>> 0; h1 = (h1 * 0x01000193) >>> 0;
    h2 = (h2 + c * (i + 1)) >>> 0;
  }
  return (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')).match(/.{1,4}/g).join(' ');
}

Object.assign(window, {
  // AI 引擎
  AI_BUILTINS, getProviders, getProvider, getCustomProviders, addCustomProvider, removeCustomProvider,
  getActiveId, setActiveId, getKey, setKey, getBase, setBase, getModel, setModel,
  hasActiveKey, activeProviderName, aiChat, aiPing,
  getProxy, setProxy,
  // 语音引擎
  TTS_PROVIDERS, EL_VOICES, QWEN_VOICES, ttsVoiceList, getTtsProvider, setTtsProvider, getTtsKey, setTtsKey,
  getTtsVoice, setTtsVoice, getClonedVoice, setClonedVoice, hasTts, ttsSynthesize, cloneVoice,
  // 兼容旧名
  hasDeepSeek: hasActiveKey, deepseekChat: aiChat, deepseekPing: aiPing,
  // 导入 / 保存
  readImportedFile, baseName, downloadFile, safeName, archiveLocally, contentHash,
  readAsDataURL, blobUrlToDataUrl,
});
