import { readAsset } from "../../../_virtual/_utils_asset.mjs";
import { streamText } from "@intlayer/ai";
import { getMarkdownMetadata } from "@intlayer/core";
import { getBlogs, getDocs, getFrequentQuestions } from "@intlayer/docs";
import { OpenAI } from "openai";

//#region src/utils/AI/askDocQuestion/askDocQuestion.ts
const readEmbeddingsForFile = (fileKey) => {
	try {
		return JSON.parse(readAsset(`./embeddings/${fileKey.replace(".md", ".json")}`, "utf-8"));
	} catch {
		return {};
	}
};
/**
* Simple in-memory vector store to hold document embeddings and their content.
* Each entry contains:
* - fileKey: A unique key identifying the file
* - chunkNumber: The number of the chunk within the document
* - content: The chunk content
* - embedding: The numerical embedding vector for the chunk
*/
const vectorStore = [];
const MAX_RELEVANT_CHUNKS_NB = 20;
const MIN_RELEVANT_CHUNKS_SIMILARITY = .42;
const EMBEDDING_MODEL = "text-embedding-3-large";
const OVERLAP_TOKENS = 200;
const MAX_CHUNK_TOKENS = 800;
const CHAR_BY_TOKEN = 4.15;
const MAX_CHARS = MAX_CHUNK_TOKENS * CHAR_BY_TOKEN;
const OVERLAP_CHARS = OVERLAP_TOKENS * CHAR_BY_TOKEN;
const skipDocEmbeddingsIndex = process.env.SKIP_DOC_EMBEDDINGS_INDEX === "true";
/**
* Splits a given text into chunks ensuring each chunk does not exceed MAX_CHARS.
* @param text - The input text to split.
* @returns - Array of text chunks.
*/
const chunkText = (text) => {
	const chunks = [];
	let start = 0;
	while (start < text.length) {
		let end = Math.min(start + MAX_CHARS, text.length);
		if (end < text.length) {
			const lastSpace = text.lastIndexOf(" ", end);
			if (lastSpace > start) end = lastSpace;
		}
		chunks.push(text.substring(start, end));
		const nextStart = end - OVERLAP_CHARS;
		if (nextStart <= start) start = end;
		else start = nextStart;
	}
	return chunks;
};
/**
* Generates an embedding for a given text using OpenAI's embedding API.
* Trims the text if it exceeds the maximum allowed characters.
*
* @param text - The input text to generate an embedding for
* @returns The embedding vector as a number array
*/
const generateEmbedding = async (text) => {
	try {
		return (await new OpenAI({ apiKey: process.env.OPENAI_API_KEY }).embeddings.create({
			model: EMBEDDING_MODEL,
			input: text
		})).data[0].embedding;
	} catch (error) {
		console.error("Error generating embedding:", error);
		return [];
	}
};
/**
* Calculates the cosine similarity between two vectors.
* Cosine similarity measures the cosine of the angle between two vectors in an inner product space.
* Used to determine the similarity between chunks of text.
*
* @param vecA - The first vector
* @param vecB - The second vector
* @returns The cosine similarity score
*/
const cosineSimilarity = (vecA, vecB) => {
	return vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0) / (Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)) * Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)));
};
/**
* Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.
* Persists per-document embeddings under `embeddings/<fileKey>.json`.
* Handles cases where files have been updated and chunk counts have changed.
*/
const loadMarkdownFiles = async () => {
	const frequentQuestions = await getFrequentQuestions();
	const docs = await getDocs();
	const blogs = await getBlogs();
	const files = {
		...docs,
		...blogs,
		...frequentQuestions
	};
	for await (const fileKey of Object.keys(files)) {
		const fileMetadata = getMarkdownMetadata(files[fileKey]);
		const fileChunks = chunkText(files[fileKey]);
		const existingEmbeddings = readEmbeddingsForFile(fileKey);
		const existingChunksForFile = Object.keys(existingEmbeddings);
		const currentChunkCount = fileChunks.length;
		const previousChunkCount = existingChunksForFile.length;
		let shouldRegenerateFileEmbeddings = false;
		if (currentChunkCount !== previousChunkCount) {
			console.info(`File "${fileKey}" chunk count changed: ${previousChunkCount} -> ${currentChunkCount}. Regenerating embeddings.`);
			shouldRegenerateFileEmbeddings = !skipDocEmbeddingsIndex;
		}
		let resultForFile = {};
		for await (const chunkIndex of Object.keys(fileChunks)) {
			const chunkNumber = Number(chunkIndex) + 1;
			const chunksNumber = fileChunks.length;
			const fileChunk = fileChunks[chunkIndex];
			const chunkKeyName = `chunk_${chunkNumber}`;
			const embedding = !shouldRegenerateFileEmbeddings ? existingEmbeddings[chunkKeyName] : void 0;
			resultForFile = {
				...resultForFile,
				[chunkKeyName]: embedding
			};
			vectorStore.push({
				fileKey,
				chunkNumber,
				embedding,
				content: fileChunk,
				docUrl: fileMetadata.url,
				docName: fileMetadata.title
			});
			console.info(`- Loaded: ${fileKey}/${chunkKeyName}/${chunksNumber}`);
		}
	}
};
loadMarkdownFiles();
/**
* Searches the indexed documents for the most relevant chunks based on a query.
* Utilizes cosine similarity to find the closest matching embeddings.
*
* @param query - The search query provided by the user
* @returns An array of the top matching document chunks' content
*/
const searchChunkReference = async (query, maxResults = MAX_RELEVANT_CHUNKS_NB, minSimilarity = MIN_RELEVANT_CHUNKS_SIMILARITY) => {
	const queryEmbedding = await generateEmbedding(query);
	const selection = vectorStore.filter((chunk) => chunk.embedding).map((chunk) => ({
		...chunk,
		similarity: cosineSimilarity(queryEmbedding, chunk.embedding)
	})).filter((chunk) => chunk.similarity > minSimilarity).sort((a, b) => b.similarity - a.similarity).slice(0, maxResults);
	const orderedDocKeys = new Set(selection.map((chunk) => chunk.fileKey));
	return vectorStore.sort((a, _b) => orderedDocKeys.has(a.fileKey) ? -1 : 1).filter((chunk) => selection.some((v) => v.fileKey === chunk.fileKey && v.chunkNumber === chunk.chunkNumber));
};
const CHAT_GPT_PROMPT = readAsset("./PROMPT.md");
const initPrompt = {
	role: "system",
	content: CHAT_GPT_PROMPT
};
/**
* Handles the "Ask a question" endpoint in an Express.js route.
* Processes user messages, retrieves relevant documents, and interacts with AI models to generate responses.
*
* @param messages - An array of chat messages from the user and assistant
* @returns The assistant's response as a string
*/
const askDocQuestion = async (messages, aiConfig, options) => {
	const relevantFilesReferences = await searchChunkReference(messages.filter((message) => message.role === "user").map((message) => `- ${message.content}`).join("\n"));
	const aiMessages = [{
		role: "system",
		content: initPrompt.content.replace("{{relevantFilesReferences}}", relevantFilesReferences.length === 0 ? "Not relevant file found related to the question." : relevantFilesReferences.map((doc, idx) => [
			"-----",
			"---",
			`chunkId: ${idx}`,
			`docChunk: "${doc.chunkNumber}/${doc.fileKey.length}"`,
			`docName: "${doc.docName}"`,
			`docUrl: "${doc.docUrl}"`,
			`---`,
			doc.content,
			`-----`
		].join("\n")).join("\n\n"))
	}, ...messages.slice(-8)];
	if (!aiConfig) throw new Error("Failed to initialize AI configuration");
	let fullResponse = "";
	const stream = streamText({
		...aiConfig,
		messages: aiMessages
	});
	for await (const chunk of stream.textStream) {
		fullResponse += chunk;
		options?.onMessage?.(chunk);
	}
	const relatedFiles = [...new Set(relevantFilesReferences.map((doc) => doc.fileKey))];
	return {
		response: fullResponse ?? "Error: No result found",
		relatedFiles
	};
};

//#endregion
export { askDocQuestion, initPrompt, loadMarkdownFiles, searchChunkReference };
//# sourceMappingURL=askDocQuestion.mjs.map