// lib/mines/rag.js
//
// Reads chunk text from the SQLite DB (mines_document_chunks), NOT from disk —
// so the lib/mines/storage.js dataroom storage interface does not apply here.
//
// Retrieval-augmented generation read path for the Mines module.
//
// Numbers come from SQL (mines_assay_samples / mines_drill_holes /
// mines_extracted_facts). Narrative context comes from here: cosine similarity
// over mines_document_chunks.embedding, filtered by project_id and optionally
// document_category. When embeddings are unavailable (provider not installed,
// or no chunk has been embedded yet) it falls back to a SQLite LIKE text search
// so the interpretation memo can still be generated. See engineering-design §5.

import { getDb } from '../db.js';
import { embedText, cosineSimilarity, parseEmbedding } from './embeddings.js';

const DEFAULT_TOP_K = 5;
// Cap the candidate set pulled into memory for in-process cosine. Correct for
// up to ~50k chunks/project on a local machine (engineering-design §8A).
const MAX_CANDIDATES = 50000;

/**
 * Retrieve the most relevant document chunks for a query.
 *
 * @param {string} projectId        mines_projects.id
 * @param {string} query            Natural-language query.
 * @param {number} [topK=5]
 * @param {string} [categoryFilter] Restrict to a single document_category.
 * @param {object} [options]
 * @param {Function} [options.provider]  Embedding provider override (tests).
 * @returns {Promise<{ mode: 'embedding'|'text', chunks: Array<{
 *   id, source_document_id, page, text, language, document_category, similarity_score }> }>}
 */
export async function retrieveChunks(projectId, query, topK = DEFAULT_TOP_K, categoryFilter = null, options = {}) {
  const db = getDb();
  const k = Math.max(1, Number(topK) || DEFAULT_TOP_K);
  const q = String(query ?? '').trim();

  if (!projectId || !q) {
    return { mode: 'text', chunks: [] };
  }

  // Attempt the embedding path first.
  const queryEmbedding = await embedText(q, options);

  if (queryEmbedding) {
    const params = [projectId];
    let sql = `
      SELECT id, source_document_id, project_id, document_category, page, text, language, embedding
      FROM mines_document_chunks
      WHERE project_id = ? AND embedding IS NOT NULL`;
    if (categoryFilter) {
      sql += ' AND document_category = ?';
      params.push(categoryFilter);
    }
    sql += ' LIMIT ?';
    params.push(MAX_CANDIDATES);

    const rows = db.prepare(sql).all(...params);
    if (rows.length > 0) {
      const scored = [];
      for (const row of rows) {
        const vec = parseEmbedding(row.embedding);
        if (!vec) continue;
        const score = cosineSimilarity(queryEmbedding, vec);
        scored.push({
          id: row.id,
          source_document_id: row.source_document_id,
          page: row.page ?? null,
          text: row.text,
          language: row.language,
          document_category: row.document_category,
          similarity_score: score,
        });
      }
      if (scored.length > 0) {
        scored.sort((a, b) => b.similarity_score - a.similarity_score);
        return { mode: 'embedding', chunks: scored.slice(0, k) };
      }
    }
    // No embedded chunks matched — fall through to text search.
  }

  return { mode: 'text', chunks: textSearch(db, projectId, q, k, categoryFilter) };
}

/**
 * SQLite LIKE fallback. Ranks by number of distinct query terms matched, then
 * by total occurrence count. Deterministic and dependency-free.
 */
function textSearch(db, projectId, query, k, categoryFilter) {
  const terms = [...new Set(
    query.toLowerCase().split(/[^\p{L}\p{N}]+/u).map(t => t.trim()).filter(t => t.length >= 3)
  )].slice(0, 10);

  const params = [projectId];
  let sql = `
    SELECT id, source_document_id, page, text, language, document_category
    FROM mines_document_chunks
    WHERE project_id = ?`;
  if (categoryFilter) {
    sql += ' AND document_category = ?';
    params.push(categoryFilter);
  }
  sql += ' LIMIT ?';
  params.push(MAX_CANDIDATES);

  const rows = db.prepare(sql).all(...params);

  const scored = rows.map(row => {
    const haystack = String(row.text || '').toLowerCase();
    let matchedTerms = 0;
    let occurrences = 0;
    for (const term of terms) {
      let idx = haystack.indexOf(term);
      if (idx === -1) continue;
      matchedTerms++;
      while (idx !== -1) {
        occurrences++;
        idx = haystack.indexOf(term, idx + term.length);
      }
    }
    // Similarity proxy in [0,1]: fraction of query terms found, nudged by density.
    const termFraction = terms.length ? matchedTerms / terms.length : 0;
    const score = matchedTerms === 0 ? 0 : Math.min(1, termFraction + Math.min(0.2, occurrences / 100));
    return {
      id: row.id,
      source_document_id: row.source_document_id,
      page: row.page ?? null,
      text: row.text,
      language: row.language,
      document_category: row.document_category,
      similarity_score: score,
      _matchedTerms: matchedTerms,
      _occurrences: occurrences,
    };
  }).filter(r => r._matchedTerms > 0);

  scored.sort((a, b) =>
    b._matchedTerms - a._matchedTerms ||
    b._occurrences - a._occurrences ||
    String(a.id).localeCompare(String(b.id))
  );

  return scored.slice(0, k).map(({ _matchedTerms, _occurrences, ...rest }) => rest);
}
