import type { APIRoute } from 'astro';
import fs from 'node:fs';
import path from 'node:path';
// Provider abstraction is shared with the build script.
// @ts-expect-error - .mjs import has no types
import { chat, embed, llmConfig, embedConfig } from '../../../scripts/lib/providers.mjs';

export const prerender = false;

const TOP_K = Number(process.env.ORACLE_TOP_K ?? 6);
const MAX_TOKENS = Number(process.env.ORACLE_MAX_TOKENS ?? 800);

type Chunk = {
	id: string;
	url: string;
	pageTitle: string;
	sectionTitle: string;
	language: string;
	content: string;
};

type Index = {
	provider: string | null;
	model: string | null;
	dim: number;
	builtAt: string | null;
	chunks: Chunk[];
	embeddings: string[];
};

let cachedIndex: { chunks: Chunk[]; vectors: Float32Array[] } | null = null;

function loadIndex() {
	if (cachedIndex) return cachedIndex;
	const indexPath = path.resolve(process.cwd(), 'public/oracle-index.json');
	if (!fs.existsSync(indexPath)) {
		cachedIndex = { chunks: [], vectors: [] };
		return cachedIndex;
	}
	const raw = JSON.parse(fs.readFileSync(indexPath, 'utf8')) as Index;
	const vectors = raw.embeddings.map((b64) => {
		const buf = Buffer.from(b64, 'base64');
		return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
	});
	cachedIndex = { chunks: raw.chunks, vectors };
	return cachedIndex;
}

function cosine(a: Float32Array, b: Float32Array): number {
	let dot = 0,
		na = 0,
		nb = 0;
	const len = Math.min(a.length, b.length);
	for (let i = 0; i < len; i++) {
		dot += a[i] * b[i];
		na += a[i] * a[i];
		nb += b[i] * b[i];
	}
	const denom = Math.sqrt(na) * Math.sqrt(nb);
	return denom === 0 ? 0 : dot / denom;
}

function detectLanguage(text: string): string {
	if (/[぀-ヿ㐀-䶿一-鿿]/.test(text)) return 'ja';
	if (/\b(qué|cómo|dónde|cuándo|porqué|hola|gracias)\b/i.test(text)) return 'es';
	if (/\b(comment|qu'est-ce|pourquoi|où|quand|bonjour)\b/i.test(text)) return 'fr';
	if (/\b(wie|was|wo|wann|warum|danke|hallo)\b/i.test(text)) return 'de';
	return 'en';
}

const SYSTEM_PROMPTS: Record<string, string> = {
	en: 'You are the Nibiru Oracle, a helpful AI assistant for the Nibiru PHP framework. Use the provided documentation excerpts to answer the user. Be concise, give concrete code examples, and cite sources by their reference number like [1], [2]. If the docs do not cover the question, say so plainly.',
	de: 'Du bist das Nibiru-Orakel, ein hilfreicher KI-Assistent für das Nibiru-PHP-Framework. Nutze die bereitgestellten Dokumentationsauszüge zur Antwort. Sei prägnant, gib konkrete Code-Beispiele und zitiere Quellen mit ihrer Referenznummer wie [1], [2]. Wenn die Dokumentation die Frage nicht abdeckt, sag es klar.',
	ja: 'あなたは Nibiru Oracle です — Nibiru PHP フレームワークの親切な AI アシスタントです。提供されたドキュメント抜粋を使って回答してください。簡潔に、具体的なコード例を示し、参照番号 [1]、[2] のように引用してください。ドキュメントに該当がなければ、はっきりとそう述べてください。',
	es: 'Eres el Oráculo de Nibiru, un asistente de IA útil para el framework PHP Nibiru. Usa los fragmentos de documentación proporcionados para responder. Sé conciso, da ejemplos de código concretos y cita las fuentes por su número de referencia como [1], [2]. Si la documentación no cubre la pregunta, dilo claramente.',
	fr: "Tu es l'Oracle de Nibiru, un assistant IA utile pour le framework PHP Nibiru. Utilise les extraits de documentation fournis pour répondre. Sois concis, donne des exemples de code concrets et cite les sources par leur numéro de référence comme [1], [2]. Si la documentation ne couvre pas la question, dis-le clairement.",
};

export const POST: APIRoute = async ({ request }) => {
	const lcfg = llmConfig();
	if (lcfg.provider === 'anthropic' && !lcfg.hasAnthropicKey) {
		return json({
			answer:
				'The Oracle is silent — `LLM_PROVIDER=anthropic` is set but `ANTHROPIC_API_KEY` is missing. Set the key, or switch back to `LLM_PROVIDER=ollama` (which is the default and uses your neuronetz.ai instance).',
			sources: [],
		});
	}

	let body: { messages: Array<{ role: string; content: string }> };
	try {
		body = await request.json();
	} catch {
		return new Response('Invalid JSON', { status: 400 });
	}

	const messages = Array.isArray(body?.messages) ? body.messages : [];
	const lastUser = [...messages].reverse().find((m) => m.role === 'user');
	const userText = (lastUser?.content || '').trim();
	if (!userText) return new Response('Empty question', { status: 400 });

	const lang = detectLanguage(userText);
	const { chunks, vectors } = loadIndex();

	// --- Retrieval (RAG) -------------------------------------------------------
	let context: Chunk[] = [];
	if (chunks.length > 0) {
		try {
			const [qVec] = await embed(userText);
			if (qVec) {
				const queryF32 = new Float32Array(qVec);
				const scored = vectors
					.map((v, i) => ({ i, score: cosine(queryF32, v) }))
					.sort((a, b) => b.score - a.score);
				const sameLang = scored.filter((s) => chunks[s.i].language === lang);
				const pool = sameLang.length >= TOP_K ? sameLang : scored;
				context = pool.slice(0, TOP_K).map((s) => chunks[s.i]);
			}
		} catch (err) {
			// Embedding failed at runtime — degrade gracefully to chat-only.
			console.warn(
				`[oracle] runtime embedding failed: ${(err as Error).message}; continuing without RAG.`
			);
		}
	}

	const contextBlock = context
		.map((c, i) => `[${i + 1}] ${c.pageTitle} — ${c.sectionTitle}\n${c.content}`)
		.join('\n\n---\n\n');

	const system = SYSTEM_PROMPTS[lang] ?? SYSTEM_PROMPTS.en;
	const augmentedSystem = contextBlock
		? `${system}\n\nDocumentation excerpts:\n\n${contextBlock}`
		: system;

	// --- Generation ------------------------------------------------------------
	try {
		const { text, model, provider } = await chat({
			system: augmentedSystem,
			messages,
			maxTokens: MAX_TOKENS,
		});

		return json({
			answer: text,
			sources: context.map((c) => ({ title: c.sectionTitle, url: c.url })),
			language: lang,
			model,
			provider,
		});
	} catch (err: unknown) {
		const message = err instanceof Error ? err.message : String(err);
		return json(
			{
				answer: `The Oracle could not answer: ${message}`,
				sources: [],
			},
			500
		);
	}
};

function json(body: unknown, status = 200): Response {
	return new Response(JSON.stringify(body), {
		status,
		headers: { 'Content-Type': 'application/json' },
	});
}

// Tiny GET handler so health-check / debugging shows config without secrets.
export const GET: APIRoute = async () => {
	const llm = llmConfig();
	const emb = embedConfig();
	const indexPath = path.resolve(process.cwd(), 'public/oracle-index.json');
	const indexExists = fs.existsSync(indexPath);
	let chunkCount = 0;
	let indexProvider = null;
	let indexModel = null;
	if (indexExists) {
		try {
			const raw = JSON.parse(fs.readFileSync(indexPath, 'utf8')) as Index;
			chunkCount = raw.chunks?.length ?? 0;
			indexProvider = raw.provider;
			indexModel = raw.model;
		} catch {
			/* ignore */
		}
	}
	return json({
		status: 'ok',
		llm: {
			provider: llm.provider,
			ollamaUrl: llm.provider === 'ollama' ? llm.ollamaUrl : undefined,
			model: llm.provider === 'ollama' ? llm.ollamaChatModel : llm.anthropicModel,
		},
		embed: {
			provider: emb.provider,
			ollamaUrl: emb.provider === 'ollama' ? emb.ollamaUrl : undefined,
			model: emb.provider === 'ollama' ? emb.ollamaEmbedModel : emb.openaiEmbedModel,
		},
		index: {
			present: indexExists,
			chunks: chunkCount,
			provider: indexProvider,
			model: indexModel,
		},
	});
};
