// ============================================================
// EMBEDDINGS UTILITY — Generate & search through LiteLLM embeddings
// ============================================================

var axios = require('axios');
var { gatewayUrl } = require('./errors');
var { getLiteLLMHeaders } = require('./litellm');

var DEFAULT_MODEL = 'openai-text-embedding-3-large';
var DEFAULT_DIMS = 3072;

/**
 * Generate embedding for text using configured provider
 * @param {string} text - Text to embed (max ~2000 tokens)
 * @param {object} opts - Options: { model, dimensions }
 * @returns {Promise<number[]>} - Embedding vector
 */
async function generateEmbedding(text, opts) {
  opts = opts || {};
  var dbModel, dbDims;
  try {
    var db = require('../db/database');
    dbModel = await db.getSetting('embeddings.model') || '';
    dbDims  = await db.getSetting('embeddings.dimensions') || '';
  } catch(e) { /* DB not available during startup */ }
  var model = opts.model || dbModel || process.env.EMBEDDING_MODEL || DEFAULT_MODEL;
  var dimensions = opts.dimensions || (dbDims ? parseInt(dbDims) : 0) || parseInt(process.env.EMBEDDING_DIMENSIONS) || DEFAULT_DIMS;

  // Truncate text to ~2000 tokens (~8000 chars) to avoid API errors
  // NOTE: Large PDFs (e.g., 100MB) will be truncated to first ~8000 chars for embedding.
  // The full PDF content is still extracted and stored in the database body field.
  // This is expected behavior - embeddings are semantic representations, not full-text storage.
  var truncated = text.substring(0, 8000);
  if (!truncated.trim()) {
    throw new Error('Empty text provided for embedding');
  }

  if (process.env.LITELLM_API_BASE) {
    return await generateEmbeddingLiteLLM(truncated, model, dimensions);
  }

  throw new Error('No embedding provider configured. Set LITELLM_API_BASE');
}

/**
 * Generate embedding via LiteLLM proxy
 */
async function generateEmbeddingLiteLLM(text, model, dimensions) {
  try {
    var payload = {
      model: model,
      input: text
    };

    if (dimensions) {
      payload.dimensions = dimensions;
    }

    var response = await axios.post(gatewayUrl('/embeddings'), payload, {
      headers: getLiteLLMHeaders('application/json'),
      timeout: 30000
    });

    if (!response.data || !response.data.data || !response.data.data[0]) {
      throw new Error('Invalid response from LiteLLM embeddings API');
    }

    return response.data.data[0].embedding;
  } catch (err) {
    console.error('[Embeddings] LiteLLM error:', err.response?.data || err.message);
    throw new Error('LiteLLM embedding failed: ' + (err.response?.data?.error || err.message));
  }
}

/**
 * Search for similar content using cosine similarity
 * @param {string} queryText - Search query
 * @param {object} opts - Options: { limit, threshold, contentType }
 * @returns {Promise<Array>} - Matching content with similarity scores
 */
async function searchSimilar(queryText, opts) {
  opts = opts || {};
  var limit = opts.limit || 10;
  var threshold = opts.threshold || 0.5; // Cosine similarity threshold (0-1)

  var db = require('../db/database');

  // Generate embedding for query
  var queryEmbedding = await generateEmbedding(queryText);

  // Build WHERE clause for filtering
  var whereClause = 'WHERE c.published = true AND c.embedding IS NOT NULL';
  var params = [JSON.stringify(queryEmbedding), threshold, limit];
  var paramIdx = 4;

  if (opts.contentType) {
    whereClause += ' AND c.content_type = $' + paramIdx;
    params.push(opts.contentType);
    paramIdx++;
  }

  if (opts.categoryId) {
    whereClause += ' AND c.category_id = $' + paramIdx;
    params.push(opts.categoryId);
  }

  // Query with cosine similarity using pgvector
  // 1 - (a <=> b) converts distance to similarity (higher = more similar)
  var sql = `
    SELECT
      c.id, c.title, c.slug, c.subject, c.content_type, c.created_at,
      cat.name as category_name, cat.slug as category_slug,
      1 - (c.embedding <=> $1::vector) as similarity,
      (SELECT COUNT(*) FROM learning_questions q WHERE q.content_id = c.id) as question_count
    FROM learning_content c
    LEFT JOIN learning_categories cat ON c.category_id = cat.id
    ${whereClause}
    AND 1 - (c.embedding <=> $1::vector) >= $2
    ORDER BY c.embedding <=> $1::vector
    LIMIT $3
  `;

  var results = await db.all(sql, params);
  return results;
}

/**
 * Generate embedding for learning content (combines title + subject + body)
 * @param {object} content - { title, subject, body }
 * @returns {Promise<number[]>} - Embedding vector
 */
async function generateContentEmbedding(content) {
  // Combine title, subject, and body (weighted toward title)
  var text = [
    content.title || '',
    content.title || '', // Title twice for emphasis
    content.subject || '',
    stripHtml(content.body || '').substring(0, 6000)
  ].filter(Boolean).join('\n\n');

  return await generateEmbedding(text);
}

/**
 * Strip HTML tags from string
 */
function stripHtml(html) {
  return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}

function getLiteLLMModelId(model) {
  if (!model) return '';
  if (typeof model === 'string') return model;
  return model.id || model.model_name || '';
}

function isLiteLLMEmbeddingModel(model) {
  var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
  return mode === 'embedding';
}

function getLiteLLMEmbeddingDimensions(model) {
  var info = model && model.model_info ? model.model_info : {};
  var dims = info.output_vector_size || info.dimensions || info.embedding_dimensions || model.output_vector_size || model.dimensions || model.embedding_dimensions;
  var parsed = parseInt(dims, 10);
  return Number.isFinite(parsed) ? parsed : '?';
}

function getLiteLLMEmbeddingModels(models) {
  return (models || [])
    .filter(isLiteLLMEmbeddingModel)
    .map(function(model) {
      var id = getLiteLLMModelId(model);
      return { id: id, name: id, dims: getLiteLLMEmbeddingDimensions(model) };
    })
    .filter(function(model) { return !!model.id; });
}

/**
 * Check if embeddings are available (provider configured)
 */
function isEmbeddingsAvailable() {
  return !!process.env.LITELLM_API_BASE;
}

module.exports = {
  generateEmbedding,
  generateContentEmbedding,
  getLiteLLMEmbeddingModels,
  searchSimilar,
  isEmbeddingsAvailable,
  isLiteLLMEmbeddingModel,
  DEFAULT_MODEL,
  DEFAULT_DIMS
};
