/**
 * Portage de apps/chatbot/services.py.
 *
 * Différence principale avec la version Django : PostgreSQL/pgvector
 * n'existe pas sur MySQL. Les embeddings (768 dimensions) sont stockés
 * en JSON (voir models/index.js, KnowledgeChunk.embedding) et la
 * similarité cosinus est calculée en mémoire, côté Node, sur l'ensemble
 * des fragments de la langue demandée. Pour une base de connaissances
 * de quelques centaines de fragments (le cas réaliste pour ce site),
 * c'est instantané — pas besoin d'une extension vectorielle dédiée.
 *
 * Mêmes garanties de résilience que la version Python : aucune fonction
 * ici ne doit jamais lever d'exception non gérée ; en cas de problème on
 * loggue et on retourne une valeur de repli exploitable.
 */
const STOPWORDS = {
  fr: new Set([
    "le", "la", "les", "un", "une", "des", "de", "du", "et", "est",
    "que", "qui", "quoi", "comment", "pour", "avec", "dans", "sur",
    "au", "aux", "ce", "cet", "cette", "ces", "vous", "nous", "je",
    "tu", "il", "elle", "on", "se", "sa", "son", "ses", "mon", "ma",
    "mes", "ton", "ta", "tes", "votre", "vos", "notre", "nos", "y",
    "en", "a", "à", "ou", "où", "quand", "quel", "quelle", "quels",
    "quelles", "être", "avoir", "faire", "svp", "merci", "bonjour",
  ]),
  en: new Set([
    "the", "a", "an", "and", "is", "are", "that", "which", "what",
    "how", "for", "with", "in", "on", "to", "of", "this", "these",
    "those", "you", "we", "i", "he", "she", "it", "they", "my",
    "your", "our", "their", "be", "have", "do", "does", "please",
    "thanks", "hello", "hi", "when", "where", "who",
  ]),
};

const SYSTEM_PROMPT_FR = `Tu es l'assistant virtuel officiel de l'IGF Burundi
(Internet Governance Forum Burundi). Réponds uniquement à partir du
contexte fourni ci-dessous. Si l'information n'y figure pas, dis
clairement que tu ne disposes pas de cette information et invite
l'utilisateur à contacter l'IGF Burundi directement (page Contact).
Réponds dans la même langue que la question posée (français ou anglais).
Sois concis et précis.`;

const SYSTEM_PROMPT_EN = `You are the official virtual assistant of IGF
Burundi (Internet Governance Forum Burundi). Answer only using the
context provided below. If the information is not present, clearly say
you don't have that information and invite the user to contact IGF
Burundi directly (Contact page). Answer in the same language as the
question (French or English). Be concise and precise.`;

const NO_INFO_MESSAGE = {
  fr: "Je n'ai pas trouvé d'information à ce sujet dans notre base de connaissances. N'hésitez pas à nous contacter directement via la page Contact ou à info@igf.bi.",
  en: "I couldn't find information on that in our knowledge base. Feel free to reach us directly via the Contact page or at info@igf.bi.",
};

const TECHNICAL_ISSUE_MESSAGE = {
  fr: "Notre assistant intelligent est momentanément indisponible, mais voici ce que notre base de connaissances contient sur le sujet :",
  en: "Our smart assistant is temporarily unavailable, but here is what our knowledge base has on the topic:",
};

let _client = null;
let _clientInitFailed = false;

function getClient() {
  if (_client) return _client;
  if (_clientInitFailed) return null;
  const apiKey = process.env.GEMINI_API_KEY;
  if (!apiKey) {
    console.warn("GEMINI_API_KEY absent : le chatbot fonctionnera en mode dégradé uniquement.");
    _clientInitFailed = true;
    return null;
  }
  try {
    const { GoogleGenAI } = require("@google/genai");
    _client = new GoogleGenAI({ apiKey });
    return _client;
  } catch (err) {
    console.error("Impossible d'initialiser le client Gemini.", err);
    _clientInitFailed = true;
    return null;
  }
}

/** Convertit un texte en vecteur (768 dimensions). Retourne null (jamais
 *  d'exception) si l'appel échoue. */
async function getEmbedding(text, taskType = "RETRIEVAL_DOCUMENT") {
  const client = getClient();
  if (!client) return null;
  try {
    const result = await client.models.embedContent({
      model: process.env.CHATBOT_EMBEDDING_MODEL || "models/gemini-embedding-001",
      contents: text,
      config: { taskType, outputDimensionality: 768 },
    });
    return result.embeddings[0].values;
  } catch (err) {
    console.warn("Échec de l'embedding Gemini (quota épuisé ou API indisponible ?).", err.message);
    return null;
  }
}

function cosineSimilarity(a, b) {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  if (normA === 0 || normB === 0) return 0;
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

function extractKeywords(text, language) {
  const words = (text.toLowerCase().match(/[a-zàâäéèêëïîôöùûüç0-9-]+/g)) || [];
  const stopwords = STOPWORDS[language] || STOPWORDS.fr;
  return words.filter((w) => w.length >= 3 && !stopwords.has(w));
}

/** Filet de secours 100% local : aucun appel réseau. */
async function keywordSearch(question, language, topK) {
  const { KnowledgeChunk } = require("../models");
  const keywords = extractKeywords(question, language);
  if (keywords.length === 0) return [];

  const candidates = await KnowledgeChunk.findAll({
    where: { language },
    attributes: ["id", "content", "source_type"],
  });

  const scored = candidates
    .map((chunk) => {
      const contentLower = chunk.content.toLowerCase();
      const score = keywords.reduce(
        (sum, kw) => sum + (contentLower.split(kw).length - 1),
        0
      );
      return { chunk, score };
    })
    .filter((s) => s.score > 0)
    .sort((a, b) => b.score - a.score);

  return scored.slice(0, topK).map((s) => s.chunk);
}

/** Recherche vectorielle (si embedding disponible) avec repli mots-clés. */
async function retrieveContext(question, language = "fr", topK = 5) {
  const { KnowledgeChunk } = require("../models");
  const queryVector = await getEmbedding(question, "RETRIEVAL_QUERY");

  if (queryVector) {
    try {
      const candidates = await KnowledgeChunk.findAll({
        where: { language, embedding: { [require("sequelize").Op.not]: null } },
      });
      const scored = candidates
        .map((chunk) => ({ chunk, score: cosineSimilarity(queryVector, chunk.embedding) }))
        .sort((a, b) => b.score - a.score)
        .slice(0, topK)
        .map((s) => s.chunk);

      if (scored.length > 0) {
        // La base de connaissances manuelle prime sur le contenu auto-indexé.
        scored.sort((a, b) => (a.source_type === "knowledge_item" ? -1 : 0) - (b.source_type === "knowledge_item" ? -1 : 0));
        return scored;
      }
    } catch (err) {
      console.warn("Échec de la recherche vectorielle, bascule sur les mots-clés.", err.message);
    }
  }

  return keywordSearch(question, language, topK);
}

async function tryGeminiGeneration(question, language, contextText) {
  const client = getClient();
  if (!client) return null;
  try {
    const systemPrompt = language === "fr" ? SYSTEM_PROMPT_FR : SYSTEM_PROMPT_EN;
    const response = await client.models.generateContent({
      model: process.env.CHATBOT_MODEL || "models/gemini-2.5-flash",
      contents: question,
      config: {
        systemInstruction: `${systemPrompt}\n\nContexte:\n${contextText}`,
        maxOutputTokens: 800,
        temperature: 0.3,
      },
    });
    const text = (response.text || "").trim();
    return text || null;
  } catch (err) {
    console.warn("Échec de la génération Gemini, bascule sur la réponse extractive.", err.message);
    return null;
  }
}

/** Orchestre recherche + génération. Retourne { reply, source } où
 *  source vaut "llm" ou "fallback". Ne lève jamais d'exception. */
async function generateAnswer(question, language = "fr") {
  let contextChunks = [];
  try {
    contextChunks = await retrieveContext(question, language);
  } catch (err) {
    console.error("Échec inattendu de retrieveContext.", err);
  }

  if (contextChunks.length === 0) {
    return { reply: NO_INFO_MESSAGE[language] || NO_INFO_MESSAGE.fr, source: "fallback" };
  }

  const contextText = contextChunks.map((c) => c.content).join("\n\n");
  const llmAnswer = await tryGeminiGeneration(question, language, contextText);
  if (llmAnswer) return { reply: llmAnswer, source: "llm" };

  const intro = TECHNICAL_ISSUE_MESSAGE[language] || TECHNICAL_ISSUE_MESSAGE.fr;
  return { reply: `${intro}\n\n${contextChunks[0].content}`, source: "fallback" };
}

/** (Ré)indexe une entrée de la base de connaissances (KnowledgeItem). */
async function indexKnowledgeItem(item) {
  const { KnowledgeChunk } = require("../models");
  await KnowledgeChunk.destroy({ where: { source_type: "knowledge_item", source_id: item.id } });
  if (!item.is_published) return true;

  for (const [lang, suffix] of [["fr", "_fr"], ["en", "_en"]]) {
    const question = item[`question${suffix}`] || item.question_fr;
    const answer = item[`answer${suffix}`] || item.answer_fr;
    const text = `Q: ${question}\nR: ${answer}`;
    const embedding = await getEmbedding(text, "RETRIEVAL_DOCUMENT");
    await KnowledgeChunk.create({
      source_type: "knowledge_item", source_id: item.id, language: lang, content: text, embedding,
    });
  }
  return true;
}

module.exports = {
  getEmbedding, retrieveContext, generateAnswer, indexKnowledgeItem, NO_INFO_MESSAGE,
};
