// Оркестрация AI: сборка контекста документа, выбор режима (direct/backend),
// чат с цитатами на страницы.

import { getSettings } from "../common/storage.js";
import { streamChat } from "./providers.js";
import { profileSystem } from "./profiles.js";
import { answer as offlineAnswer } from "./offline.js";

/**
 * Документ для AI. pages — массив строк (текст по страницам, индекс = номер-1).
 */
export class DocumentContext {
  constructor() {
    this.pages = [];
    this.title = "";
    this.hash = "";
  }
  setPages(pages) {
    this.pages = pages;
  }
  get fullText() {
    return this.pages.map((t, i) => `[с. ${i + 1}]\n${t}`).join("\n\n");
  }
  /** Чанки документа: {page, pos, text} по ~700 символов. */
  _chunks(size = 700) {
    const out = [];
    this.pages.forEach((t, i) => {
      const text = t || "";
      for (let p = 0; p < text.length; p += size) out.push({ page: i + 1, pos: p, text: text.slice(p, p + size) });
    });
    return out;
  }

  /** Равномерная выборка (фолбэк, когда запрос пуст или ничего не найдено). */
  _evenContext(maxChars) {
    const perPage = Math.max(400, Math.floor(maxChars / Math.max(1, this.pages.length)));
    let out = "";
    for (let i = 0; i < this.pages.length && out.length < maxChars; i++) {
      out += `[с. ${i + 1}]\n${(this.pages[i] || "").slice(0, perPage)}\n\n`;
    }
    return out.slice(0, maxChars);
  }

  /**
   * Контекст под maxChars символов. Лексический retrieval (RAG-lite): выбираем
   * чанки, наиболее релевантные запросу, переупорядочиваем по странице/позиции,
   * помечаем [с. N]. Если запрос пуст/без совпадений — равномерная выборка.
   */
  contextForQuery(maxChars, query = "") {
    const full = this.fullText;
    if (full.length <= maxChars) return full;

    const terms = [...new Set((query.toLowerCase().match(/[а-яёa-z0-9]{4,}/gi) || []))];
    if (!terms.length) return this._evenContext(maxChars);

    const chunks = this._chunks();
    for (const c of chunks) {
      const low = c.text.toLowerCase();
      c.score = terms.reduce((s, term) => s + (low.split(term).length - 1), 0);
    }
    const ranked = chunks.filter((c) => c.score > 0).sort((a, b) => b.score - a.score);
    if (!ranked.length) return this._evenContext(maxChars);

    const picked = [];
    let len = 0;
    for (const c of ranked) {
      if (len + c.text.length + 16 > maxChars) continue;
      picked.push(c);
      len += c.text.length + 16;
    }
    picked.sort((a, b) => a.page - b.page || a.pos - b.pos);

    let out = "";
    let lastPage = -1;
    for (const c of picked) {
      if (c.page !== lastPage) { out += `\n[с. ${c.page}]\n`; lastPage = c.page; }
      out += c.text + "\n";
    }
    return out.slice(0, maxChars);
  }
}

/**
 * Запускает один запрос к AI. onDelta(text) — стрим ответа.
 * messages — история чата [{role, content}] (без системного и без контекста).
 */
export async function ask({ doc, messages, profileId, intent }, onDelta, signal) {
  const s = await getSettings();
  const profile = profileId || s.profile;
  const query = [...messages].reverse().find((m) => m.role === "user")?.content || "";

  // Офлайн lite-режим: локальный анализ без внешних вызовов (дефолт «работает сразу»).
  if (s.aiMode === "offline") {
    const text = offlineAnswer({ pages: doc.pages, query, intent });
    return streamLocal(text, onDelta);
  }

  if (s.aiMode === "backend") {
    return askBackend({ s, doc, messages, query }, onDelta, signal);
  }

  // Прямой режим (BYO-key).
  const system = profileSystem(profile);
  const context = doc.contextForQuery(s.maxContext, query);
  const composed = [
    { role: "system", content: system },
    {
      role: "system",
      content:
        "Содержимое документа (страницы помечены [с. N], используй их для ссылок):\n\n" +
        context,
    },
    ...messages,
  ];

  return streamChat(
    {
      provider: s.provider,
      apiKey: s.apiKey,
      baseUrl: s.baseUrl,
      model: s.model,
      temperature: s.temperature,
      messages: composed,
      scope: s.gigachatScope,
      folderId: s.yandexFolderId,
    },
    onDelta,
    signal,
  );
}

/** Имитация стрима для офлайн-ответа (по словам — для живого вывода). */
async function streamLocal(text, onDelta) {
  const tokens = text.match(/\S+\s*/g) || [text];
  for (const t of tokens) onDelta(t);
  return text;
}

/**
 * Режим backend: POST /ai/chat. Контракт — backend/app/routers/ai.py:
 *   запрос  { document_id, messages, mode, cite_sources, context, stream, provider, api_key, base_url, model }
 *   ответ   JSON { answer, provider, sources:[{page,snippet}], mode }  ИЛИ
 *           SSE  data:{delta} … data:{sources,provider} … data:[DONE]   (при stream:true)
 * BYO-key: креды провайдера передаём в теле; без них backend вернёт заглушку.
 */
async function askBackend({ s, doc, messages, query = "" }, onDelta, signal) {
  const res = await fetch(`${s.backendUrl.replace(/\/+$/, "")}/ai/chat`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "text/event-stream, application/json",
      ...(s.backendToken ? { Authorization: `Bearer ${s.backendToken}` } : {}),
    },
    body: JSON.stringify({
      document_id: doc.hash || undefined,
      messages,
      mode: "rag",
      cite_sources: s.cite_sources,
      context: doc.contextForQuery(s.maxContext, query),
      stream: true,
      provider: s.provider,
      api_key: s.apiKey || undefined,
      base_url: s.baseUrl || undefined,
      model: s.model || undefined,
    }),
    signal,
  });
  if (!res.ok) throw new Error(`Backend HTTP ${res.status}: ${await res.text()}`);

  const ct = res.headers.get("content-type") || "";
  let answer = "";
  let sources = [];
  let provider = "";

  if (ct.includes("text/event-stream") && res.body) {
    // SSE-стрим: delta-куски + финальное событие с sources/provider.
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buf = "";
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      const lines = buf.split("\n");
      buf = lines.pop() || "";
      for (const line of lines) {
        const tl = line.trim();
        if (!tl.startsWith("data:")) continue;
        const payload = tl.slice(5).trim();
        if (!payload || payload === "[DONE]") continue;
        try {
          const evt = JSON.parse(payload);
          if (evt.delta) { answer += evt.delta; onDelta(evt.delta); }
          if (evt.sources) sources = evt.sources;
          if (evt.provider) provider = evt.provider;
        } catch { /* служебная строка */ }
      }
    }
  } else {
    // JSON-ответ (бэкенд без стрима / stream проигнорирован).
    const data = await res.json();
    answer = data.answer || "";
    sources = data.sources || [];
    provider = data.provider || "";
    onDelta(answer);
  }

  // Дописываем источники/провайдера (для SSE — после потока; для JSON — после ответа).
  let tail = "";
  if (Array.isArray(sources) && sources.length && !/\[с\.\s*\d/.test(answer)) {
    const pages = [...new Set(sources.map((x) => x.page).filter(Boolean))];
    if (pages.length) tail += `\n\nИсточники: ${pages.map((p) => `[с. ${p}]`).join(" ")}`;
  }
  if (provider && provider !== "stub") tail += `\n\n_провайдер: ${provider}_`;
  if (tail) onDelta(tail);
  return answer + tail;
}
