// lib/mines/embeddings.js
//
// Embedding provider abstraction for the Mines RAG pipeline.
//
// Default provider: @xenova/transformers with Xenova/multilingual-e5-small
// (768-dim, ONNX, fully local, handles Spanish natively). The package is an
// OPTIONAL runtime dependency — it is loaded via a bundler-ignored dynamic
// import so `next build` succeeds whether or not it is installed. When the
// provider is unavailable, embedChunks returns chunks with embedding = null
// and the caller marks the document 'embedding_failed' (RAG then falls back to
// text search — see lib/mines/rag.js).
//
// Embeddings are stored as a JSON-serialised float array in
// mines_document_chunks.embedding (TEXT). Cosine similarity runs in-process.
// See specs/active/mines-engineering-design.md §8A for the pgvector seam.

const DEFAULT_MODEL = process.env.MINES_EMBEDDING_MODEL || 'Xenova/multilingual-e5-small';
const DEFAULT_BATCH_SIZE = 100;

// Cached default-embedder promise. `undefined` = not yet attempted;
// resolves to an embed function or `null` when the package is unavailable.
let _defaultEmbedderPromise;

/**
 * Resolve the default local embedder. Returns a function
 * `(text) => Promise<number[]>` or null when @xenova/transformers is not
 * installed. The webpack/turbopack ignore comments keep the optional package
 * out of the Next.js bundle so the build never tries to resolve it.
 */
async function getDefaultEmbedder() {
  if (_defaultEmbedderPromise === undefined) {
    _defaultEmbedderPromise = (async () => {
      let mod = null;
      try {
        mod = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@xenova/transformers');
      } catch {
        return null;
      }
      const pipeline = mod?.pipeline || mod?.default?.pipeline;
      if (typeof pipeline !== 'function') return null;
      const extractor = await pipeline('feature-extraction', DEFAULT_MODEL);
      return async (text) => {
        const out = await extractor(String(text ?? ''), { pooling: 'mean', normalize: true });
        return Array.from(out.data);
      };
    })().catch(() => null);
  }
  return _defaultEmbedderPromise;
}

/**
 * Returns true if a local embedding provider is available right now.
 * @param {object} [options]
 * @param {Function} [options.provider]  Override embedder (used by tests).
 */
export async function isEmbeddingAvailable(options = {}) {
  if (typeof options.provider === 'function') return true;
  return (await getDefaultEmbedder()) !== null;
}

/**
 * Embed a single text string.
 * @param {string} text
 * @param {object} [options]
 * @param {Function} [options.provider]  `(text) => Promise<number[]>` override.
 * @returns {Promise<number[]|null>}  Embedding vector, or null when unavailable.
 */
export async function embedText(text, options = {}) {
  const embed = typeof options.provider === 'function' ? options.provider : await getDefaultEmbedder();
  if (!embed) return null;
  try {
    const vec = await embed(text);
    return Array.isArray(vec) ? vec : null;
  } catch {
    return null;
  }
}

/**
 * Embed an array of chunks in batches. Mutates each chunk, setting
 * `chunk.embedding` to a float array (or null on per-chunk failure / when no
 * provider is available).
 *
 * @param {Array<{ id?: string, text: string, embedding?: number[]|null }>} chunks
 * @param {object} [options]
 * @param {Function} [options.provider]   `(text) => Promise<number[]>` override.
 * @param {number}   [options.batchSize]  Max chunks per pass. Default 100.
 * @returns {Promise<{ chunks: Array, embedded: number, available: boolean }>}
 */
export async function embedChunks(chunks, options = {}) {
  const list = Array.isArray(chunks) ? chunks : [];
  if (list.length === 0) return { chunks: [], embedded: 0, available: false };

  const embed = typeof options.provider === 'function' ? options.provider : await getDefaultEmbedder();
  if (!embed) {
    for (const c of list) c.embedding = null;
    return { chunks: list, embedded: 0, available: false };
  }

  const batchSize = Math.max(1, Number(options.batchSize) || DEFAULT_BATCH_SIZE);
  let embedded = 0;
  for (let i = 0; i < list.length; i += batchSize) {
    const batch = list.slice(i, i + batchSize);
    for (const chunk of batch) {
      try {
        const vec = await embed(chunk.text);
        if (Array.isArray(vec)) {
          chunk.embedding = vec;
          embedded++;
        } else {
          chunk.embedding = null;
        }
      } catch {
        chunk.embedding = null;
      }
    }
  }
  return { chunks: list, embedded, available: true };
}

/**
 * Cosine similarity of two equal-length numeric vectors.
 * Returns 0 for missing, mismatched-length, or zero-magnitude inputs.
 */
export function cosineSimilarity(a, b) {
  if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
  let dot = 0;
  let magA = 0;
  let magB = 0;
  for (let i = 0; i < a.length; i++) {
    const x = Number(a[i]) || 0;
    const y = Number(b[i]) || 0;
    dot += x * y;
    magA += x * x;
    magB += y * y;
  }
  if (magA === 0 || magB === 0) return 0;
  return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}

/** Serialise an embedding vector to TEXT for storage. null/invalid → null. */
export function serializeEmbedding(vec) {
  if (!Array.isArray(vec) || vec.length === 0) return null;
  return JSON.stringify(vec);
}

/** Parse a stored embedding TEXT back to a float array. Invalid → null. */
export function parseEmbedding(text) {
  if (text === null || text === undefined || text === '') return null;
  if (Array.isArray(text)) return text;
  try {
    const parsed = JSON.parse(text);
    return Array.isArray(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

/** Test-only: reset the cached default embedder so a fresh probe runs next call. */
export function __resetEmbedderForTests() {
  _defaultEmbedderPromise = undefined;
}
