/**
 * OpenAIEmbeddingProvider — works with any OpenAI-compatible embeddings API
 * including OpenAI, Azure OpenAI, and Ollama.
 */

import { EmbeddingError, type EmbeddingProvider, type EmbeddingVector } from './provider.js';

export interface OpenAIProviderConfig {
	/** e.g. "https://api.openai.com/v1" or "http://localhost:11434/v1" */
	baseUrl: string;
	apiKey: string;
	/** e.g. "text-embedding-3-small", "nomic-embed-text" */
	model: string;
	/** Override dimensions for models that support it (e.g. text-embedding-3-small). */
	dimensions?: number;
	/** Maximum number of texts per API request. Default: 100. */
	maxBatchSize?: number;
}

function chunk<T>(arr: T[], size: number): T[][] {
	const chunks: T[][] = [];
	for (let i = 0; i < arr.length; i += size) {
		chunks.push(arr.slice(i, i + size));
	}
	return chunks;
}

export class OpenAIEmbeddingProvider implements EmbeddingProvider {
	readonly name = 'openai';

	get model(): string {
		return this.config.model;
	}

	get dimensions(): number {
		return this.config.dimensions ?? 1536;
	}

	constructor(private readonly config: OpenAIProviderConfig) {}

	async embed(texts: string[]): Promise<EmbeddingVector[]> {
		const batchSize = this.config.maxBatchSize ?? 100;
		const batches = chunk(texts, batchSize);
		const allEmbeddings: EmbeddingVector[] = [];

		for (const batch of batches) {
			const body: Record<string, unknown> = {
				model: this.config.model,
				input: batch
			};
			if (this.config.dimensions !== undefined) {
				body.dimensions = this.config.dimensions;
			}

			const response = await fetch(`${this.config.baseUrl}/embeddings`, {
				method: 'POST',
				headers: {
					Authorization: `Bearer ${this.config.apiKey}`,
					'Content-Type': 'application/json'
				},
				body: JSON.stringify(body)
			});

			if (!response.ok) {
				let detail = '';
				try {
					const errBody = await response.json();
					detail = errBody?.error?.message ?? '';
				} catch {
					// ignore parse failure
				}
				throw new EmbeddingError(
					`OpenAI API error ${response.status}${detail ? `: ${detail}` : ''}`
				);
			}

			const data = (await response.json()) as {
				data: Array<{ embedding: number[]; index: number }>;
			};

			// API returns results in order; sort by index just in case.
			const sorted = [...data.data].sort((a, b) => a.index - b.index);
			for (const item of sorted) {
				allEmbeddings.push({
					values: new Float32Array(item.embedding),
					dimensions: item.embedding.length,
					model: this.config.model
				});
			}
		}

		return allEmbeddings;
	}

	async isAvailable(): Promise<boolean> {
		try {
			// Ping with a minimal single-item embed request.
			await this.embed(['ping']);
			return true;
		} catch {
			return false;
		}
	}
}
