'use strict';

/**
 * Wall-E Embeddings Module
 *
 * Provides semantic vector search via sqlite-vec.
 * Gracefully degrades: if sqlite-vec is unavailable or no embedding API
 * is configured, all functions return empty results or no-op.
 *
 * Architecture:
 *   - embedding_map: maps entity IDs to models (shared across all providers)
 *   - embedding_vec_<key>: per-provider vec0 virtual tables
 *   - Active provider stored in brain kv_store (not env vars)
 *   - Old provider tables kept until explicitly deleted by user
 */

// --- Adapter interface (dependency injection) ---
// Default: use Wall-E brain module. CTM (or other consumers) can call init(adapter)
// to provide their own { getDb, getKv, setKv, listModelProviders, getModelProviderWithKey }.

let _adapter = null;

let _defaultAdapter = null;
function _getAdapter() {
  if (_adapter) return _adapter;
  // Lazy default: load brain module (backwards-compatible with Wall-E)
  if (!_defaultAdapter) {
    const brain = require('./brain');
    _defaultAdapter = {
      getDb: () => brain.getDb(),
      getKv: (k) => brain.getKv(k),
      setKv: (k, v) => brain.setKv(k, v),
      listModelProviders: () => brain.listModelProviders(),
      getModelProviderWithKey: (id) => brain.getModelProviderWithKey(id),
    };
  }
  return _defaultAdapter;
}

// --- sqlite-vec loading ---

let _vecLoaded = false;
let _dimensions = null;

function ensureVecLoaded() {
  if (_vecLoaded) return true;
  try {
    const sqliteVec = require('sqlite-vec');
    sqliteVec.load(_getAdapter().getDb());
    _vecLoaded = true;
    return true;
  } catch {
    return false;
  }
}

function isAvailable() {
  return _vecLoaded && getEmbeddingModel() !== null;
}

// --- Embedding model registry ---

const EMBEDDING_MODELS = {
  'text-embedding-004':   { dimensions: 768,  provider: 'google', maxBatch: 100,  key: 'gemini',  label: 'Google Gemini',  cost: 'Free', desc: '1500 req/min, unlimited' },
  'voyage-3-lite':        { dimensions: 512,  provider: 'voyage', maxBatch: 128,  key: 'voyage',  label: 'Voyage AI',      cost: '$0.02/1M', desc: 'Anthropic recommended' },
  'text-embedding-3-small': { dimensions: 1536, provider: 'openai', maxBatch: 2048, key: 'openai', label: 'OpenAI',         cost: '$0.02/1M', desc: 'text-embedding-3-small' },
  'text-embedding-3-large': { dimensions: 3072, provider: 'openai', maxBatch: 2048, key: 'openai_lg', label: 'OpenAI Large', cost: '$0.13/1M', desc: 'text-embedding-3-large' },
  'nomic-embed-text':     { dimensions: 768,  provider: 'ollama', maxBatch: 32,   key: 'ollama',  label: 'Ollama (local)', cost: 'Free', desc: 'nomic-embed-text, private', quantize: 'int8' },
  'mxbai-embed-large':    { dimensions: 1024, provider: 'ollama', maxBatch: 32,   key: 'ollama_lg', label: 'Ollama Large', cost: 'Free', desc: 'mxbai-embed-large, private' },
};

const EMBEDDING_POLICY = {
  LOCAL_FIRST: 'local_first',
  LOCAL_ONLY: 'local_only',
  CLOUD_ALLOWED: 'cloud_allowed',
  OFF: 'off',
};
const DEFAULT_EMBEDDING_POLICY = EMBEDDING_POLICY.LOCAL_FIRST;
const EMBEDDING_POLICY_VALUES = new Set(Object.values(EMBEDDING_POLICY));
const EMBEDDING_LOCAL_MODELS = ['nomic-embed-text', 'mxbai-embed-large'];
const EMBEDDING_CLOUD_MODELS = ['text-embedding-004', 'text-embedding-3-small', 'voyage-3-lite'];
const EMBEDDING_AUTO_MODEL_ORDER = [...EMBEDDING_LOCAL_MODELS, ...EMBEDDING_CLOUD_MODELS];

function _getKv(key) {
  try { return _getAdapter().getKv(key); } catch { return null; }
}

function _setKv(key, value) {
  try { _getAdapter().setKv(key, value); return true; } catch { return false; }
}

function _normalizeEmbeddingPolicy(value) {
  const normalized = String(value || '').trim().toLowerCase();
  return EMBEDDING_POLICY_VALUES.has(normalized) ? normalized : DEFAULT_EMBEDDING_POLICY;
}

function getEmbeddingPolicy() {
  return _normalizeEmbeddingPolicy(_getKv('embedding:auto_policy'));
}

function _getEmbeddingPolicyRecord() {
  const raw = _getKv('embedding:auto_policy');
  return {
    policy: _normalizeEmbeddingPolicy(raw),
    source: raw && EMBEDDING_POLICY_VALUES.has(String(raw).trim().toLowerCase()) ? 'saved' : 'default',
  };
}

function _isLocalProvider(provider) {
  return provider === 'ollama';
}

function _isCloudProvider(provider) {
  return Boolean(provider) && !_isLocalProvider(provider);
}

function _policyAllowsConfig(config, policy) {
  if (!config || policy === EMBEDDING_POLICY.OFF) return false;
  if (policy === EMBEDDING_POLICY.CLOUD_ALLOWED) return true;
  return _isLocalProvider(config.provider);
}

function _providerReachableForModel(modelName) {
  const config = EMBEDDING_MODELS[modelName];
  return config ? _hasApiKey(config.provider) : false;
}

function _embeddingCountForModel(modelName) {
  try {
    const db = _getAdapter().getDb();
    const row = db.prepare('SELECT count(*) as c FROM embedding_map WHERE model = ?').get(modelName);
    return row?.c || 0;
  } catch {
    return 0;
  }
}

function _findExistingIndexedModel(policy, policySource) {
  try {
    const db = _getAdapter().getDb();
    const rows = db.prepare('SELECT model, count(*) as c FROM embedding_map GROUP BY model ORDER BY c DESC').all();
    for (const row of rows) {
      const config = EMBEDDING_MODELS[row.model];
      if (!config) continue;
      // Backward compatibility: installs that already built a cloud index before
      // the policy setting existed should keep using it until the user changes
      // policy or manually switches providers.
      if (policySource === 'default' || _policyAllowsConfig(config, policy)) return row.model;
    }
  } catch {}
  return null;
}

function _firstAvailableModel(models, policy) {
  for (const model of models) {
    const config = EMBEDDING_MODELS[model];
    if (!config || !_policyAllowsConfig(config, policy)) continue;
    if (_hasApiKey(config.provider)) return model;
  }
  return null;
}

function _selectAutoCandidate(policy) {
  if (policy === EMBEDDING_POLICY.OFF) return null;
  return _firstAvailableModel(
    policy === EMBEDDING_POLICY.CLOUD_ALLOWED ? EMBEDDING_AUTO_MODEL_ORDER : EMBEDDING_LOCAL_MODELS,
    policy
  );
}

function getActiveEmbeddingModelInfo() {
  const explicit = process.env.WALLE_EMBEDDING_MODEL;
  if (explicit) {
    return EMBEDDING_MODELS[explicit]
      ? { model: explicit, source: 'env', policy: getEmbeddingPolicy(), policySource: 'env' }
      : { model: null, source: 'invalid_env', policy: getEmbeddingPolicy(), policySource: 'env' };
  }

  const policyRecord = _getEmbeddingPolicyRecord();
  if (policyRecord.policy === EMBEDDING_POLICY.OFF) {
    return { model: null, source: 'off', policy: policyRecord.policy, policySource: policyRecord.source };
  }

  const pref = _getKv('embedding:active_model');
  const selectionMode = _getKv('embedding:selection_mode') || (pref ? 'manual' : 'auto');
  if (pref && EMBEDDING_MODELS[pref]) {
    const config = EMBEDDING_MODELS[pref];
    const blockedByLocalOnly = policyRecord.source === 'saved'
      && policyRecord.policy === EMBEDDING_POLICY.LOCAL_ONLY
      && !_policyAllowsConfig(config, policyRecord.policy);
    if (blockedByLocalOnly) {
      // The explicit Local only policy is stronger than an older cloud manual
      // choice. The saved model is left in kv_store for audit/history, but it is
      // not effective while this policy is active.
    } else if (selectionMode !== 'auto') {
      return { model: pref, source: selectionMode, policy: policyRecord.policy, policySource: policyRecord.source };
    } else if (_policyAllowsConfig(config, policyRecord.policy) && _providerReachableForModel(pref)) {
      return { model: pref, source: 'auto', policy: policyRecord.policy, policySource: policyRecord.source };
    }
  }

  const indexedModel = _findExistingIndexedModel(policyRecord.policy, policyRecord.source);
  if (indexedModel) {
    return { model: indexedModel, source: 'existing_index', policy: policyRecord.policy, policySource: policyRecord.source };
  }

  const candidate = _selectAutoCandidate(policyRecord.policy);
  return {
    model: candidate,
    source: candidate ? 'auto_candidate' : 'none',
    policy: policyRecord.policy,
    policySource: policyRecord.source,
  };
}

/**
 * Determine the active embedding model.
 * Priority: explicit env var > manual/saved preference > compatible existing
 * index > auto policy candidate. Cloud candidates require explicit opt-in.
 */
function getEmbeddingModel() {
  return getActiveEmbeddingModelInfo().model || null;
}

function _hasApiKey(provider) {
  // Check process.env first, then fall back to model_providers table in brain DB
  switch (provider) {
    case 'google': return !!(process.env.GOOGLE_API_KEY || _hasProviderKey('google'));
    case 'voyage': return !!process.env.VOYAGE_API_KEY;
    case 'openai': return !!(process.env.OPENAI_API_KEY || _hasProviderKey('openai'));
    case 'ollama': return _isOllamaAvailable();
    default: return false;
  }
}

function _hasProviderKey(type) {
  try {
    const adapter = _getAdapter();
    const providers = adapter.listModelProviders();
    const match = providers.find(p => p.type === type && p.enabled);
    if (!match) return false;
    // Verify an actual API key exists (listModelProviders omits keys for security)
    const full = adapter.getModelProviderWithKey(match.id);
    return !!(full && full.api_key_encrypted);
  } catch { return false; }
}

let _ollamaAvailableCache = { value: false, ts: 0 };
function _isOllamaAvailable() {
  // Allow tests to disable Ollama detection
  if (process.env.WALLE_DISABLE_OLLAMA === '1') return false;
  // Cache for 60s to avoid repeated curl calls on every render
  if (Date.now() - _ollamaAvailableCache.ts < 60000) return _ollamaAvailableCache.value;
  try {
    require('child_process').execFileSync('curl', ['-s', '--max-time', '1', 'http://localhost:11434/api/tags'], { timeout: 2000, stdio: 'pipe' });
    _ollamaAvailableCache = { value: true, ts: Date.now() };
    return true;
  } catch {
    _ollamaAvailableCache = { value: false, ts: Date.now() };
    return false;
  }
}

// --- Telemetry ---

const EMBEDDING_SETUP_STATUS_TELEMETRY_INTERVAL_MS = 6 * 60 * 60 * 1000;
const EMBEDDING_USAGE_TELEMETRY_INTERVAL_MS = 60 * 60 * 1000;
let _lastSetupStatusTelemetry = { key: '', ts: 0 };
let _lastUsageTelemetryByKey = new Map();

function _countBucket(value) {
  const n = Number(value) || 0;
  if (n <= 0) return '0';
  if (n < 10) return '1-9';
  if (n < 100) return '10-99';
  if (n < 1000) return '100-999';
  if (n < 10000) return '1k-9k';
  if (n < 100000) return '10k-99k';
  return '100k+';
}

function _safeTelemetry() {
  try { return require('./telemetry'); } catch { return null; }
}

function _modelTelemetryFields(modelName) {
  const cfg = modelName && EMBEDDING_MODELS[modelName] ? EMBEDDING_MODELS[modelName] : null;
  return {
    model: cfg ? modelName : 'none',
    provider: cfg ? cfg.provider : 'none',
    model_key: cfg ? cfg.key : 'none',
    cost_tier: cfg ? (cfg.cost === 'Free' ? 'free' : 'paid') : 'none',
  };
}

function _embeddingTelemetrySnapshot() {
  const info = getActiveEmbeddingModelInfo();
  let model = info.model || null;
  let providerReachable = false;
  let stats = { total: 0, memories: 0, knowledge: 0 };
  try {
    const cfg = model && EMBEDDING_MODELS[model] ? EMBEDDING_MODELS[model] : null;
    providerReachable = cfg ? !!_hasApiKey(cfg.provider) : false;
  } catch {}
  try {
    if (model) stats = getEmbeddingStats();
  } catch {}

  let state = 'ready';
  if (!_vecLoaded) state = 'sqlite_vec_unavailable';
  else if (!model) state = 'no_provider';
  else if (!providerReachable) state = 'provider_unreachable';

  return {
    state,
    enabled: state === 'ready' ? 1 : 0,
    sqlite_vec: _vecLoaded ? 1 : 0,
    provider_reachable: providerReachable ? 1 : 0,
    policy: info.policy || DEFAULT_EMBEDDING_POLICY,
    selection_source: info.source || 'none',
    ..._modelTelemetryFields(model),
    embedded_bucket: _countBucket(stats.total),
    memory_bucket: _countBucket(stats.memories),
    knowledge_bucket: _countBucket(stats.knowledge),
  };
}

function trackEmbeddingStatus(reason = 'boot', options = {}) {
  const telemetry = options.telemetry || _safeTelemetry();
  if (!telemetry || typeof telemetry.track !== 'function') return;
  telemetry.track('embedding_status', {
    reason,
    ..._embeddingTelemetrySnapshot(),
  });
}

function trackEmbeddingSetupView() {
  const snapshot = _embeddingTelemetrySnapshot();
  const key = [
    snapshot.state,
    snapshot.provider,
    snapshot.model,
    snapshot.embedded_bucket,
    snapshot.memory_bucket,
  ].join('|');
  const now = Date.now();
  if (_lastSetupStatusTelemetry.key === key && now - _lastSetupStatusTelemetry.ts < EMBEDDING_SETUP_STATUS_TELEMETRY_INTERVAL_MS) {
    return;
  }
  _lastSetupStatusTelemetry = { key, ts: now };
  trackEmbeddingStatus('setup_view');
}

function _switchFailureReason(result) {
  const message = String(result && result.message || '').toLowerCase();
  if (message.includes('unknown model')) return 'unknown_model';
  if (message.includes('no api key')) return 'missing_key';
  return result && result.ok ? 'success' : 'failed';
}

function _trackEmbeddingProviderSwitch(previousModel, requestedModel, result) {
  const telemetry = _safeTelemetry();
  if (!telemetry || typeof telemetry.track !== 'function') return;
  const fromFields = _modelTelemetryFields(previousModel);
  const toFields = _modelTelemetryFields(requestedModel);
  telemetry.track('embedding_provider_switch', {
    ok: result && result.ok ? 1 : 0,
    reason: _switchFailureReason(result),
    from_provider: fromFields.provider,
    from_model: fromFields.model,
    to_provider: toFields.provider,
    to_model: toFields.model,
    same_provider: fromFields.provider === toFields.provider ? 1 : 0,
  });
  if (result && result.ok) trackEmbeddingStatus('provider_switch', { telemetry });
}

function _trackEmbeddingUsage(operation, resultCount, status = 'ok') {
  const telemetry = _safeTelemetry();
  if (!telemetry || typeof telemetry.track !== 'function') return;
  const snapshot = _embeddingTelemetrySnapshot();
  const safeOperation = String(operation || 'unknown').replace(/[^a-z0-9_-]/gi, '_').slice(0, 64) || 'unknown';
  const safeStatus = String(status || 'ok').replace(/[^a-z0-9_-]/gi, '_').slice(0, 32) || 'ok';
  const resultBucket = _countBucket(resultCount);
  const key = [safeOperation, safeStatus, snapshot.provider, snapshot.model, resultBucket].join('|');
  const now = Date.now();
  const last = _lastUsageTelemetryByKey.get(key) || 0;
  if (now - last < EMBEDDING_USAGE_TELEMETRY_INTERVAL_MS) return;
  _lastUsageTelemetryByKey.set(key, now);
  telemetry.track('embedding_usage', {
    operation: safeOperation,
    status: safeStatus,
    result_bucket: resultBucket,
    state: snapshot.state,
    provider: snapshot.provider,
    model: snapshot.model,
  });
}

function getModelConfig() {
  const model = getEmbeddingModel();
  return model ? { name: model, ...EMBEDDING_MODELS[model] } : null;
}

function getDimensions() {
  if (_dimensions !== null) return _dimensions;
  const config = getModelConfig();
  _dimensions = config ? config.dimensions : null;
  return _dimensions;
}

/**
 * Get all providers with their availability status.
 * Used by the setup page UI.
 */
function getProviderStatus() {
  const activeInfo = getActiveEmbeddingModelInfo();
  const activeModel = activeInfo.model;
  const policy = activeInfo.policy || getEmbeddingPolicy();
  const db = _getAdapter().getDb();

  return Object.entries(EMBEDDING_MODELS).map(([model, config]) => {
    const hasKey = _hasApiKey(config.provider);
    const isActive = model === activeModel;

    // Count embeddings for this provider
    let count = 0;
    let sizeEstimate = null;
    const vecTable = `embedding_vec_${config.key}`;
    try {
      const mapCount = db.prepare(`SELECT count(*) as c FROM embedding_map WHERE model = ?`).get(model);
      count = mapCount ? mapCount.c : 0;
      // Size estimate: dims * 4 bytes per float * count
      sizeEstimate = count * config.dimensions * 4;
    } catch {}

    // For the active provider, check if actually reachable (API key + provider up)
    const reachable = isActive ? _hasApiKey(config.provider) : undefined;

    return {
      model,
      key: config.key,
      label: config.label,
      desc: config.desc,
      cost: config.cost,
      dimensions: config.dimensions,
      provider: config.provider,
      hasKey,
      isActive,
      reachable,
      selectionSource: isActive ? activeInfo.source : undefined,
      autoEligible: _policyAllowsConfig(config, policy),
      cloudRequiresOptIn: _isCloudProvider(config.provider) && policy !== EMBEDDING_POLICY.CLOUD_ALLOWED,
      embeddingCount: count,
      sizeBytes: sizeEstimate,
    };
  });
}

function _resetBackfillForModelChange() {
  try {
    _setKv('backfill:embed:cursor', '0');
    _setKv('backfill:done', null);
  } catch {}
}

function _ensureVecTableForModel(modelName) {
  const config = EMBEDDING_MODELS[modelName];
  if (!config || !ensureVecLoaded()) return false;
  const db = _getAdapter().getDb();
  const vecTable = _vecTableName(config.key);
  const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(vecTable);
  if (!existing) {
    db.prepare(`CREATE VIRTUAL TABLE "${vecTable}" USING vec0(embedding ${_vecColType(config)})`).run();
  }
  return true;
}

function _trackEmbeddingAutoSelect(reason, result) {
  const telemetry = _safeTelemetry();
  if (!telemetry || typeof telemetry.track !== 'function') return;
  telemetry.track('embedding_auto_select', {
    reason,
    ok: result.ok ? 1 : 0,
    status: result.status || '',
    policy: result.policy || getEmbeddingPolicy(),
    model: result.model || 'none',
    provider: result.provider || 'none',
    selection_source: result.selectionSource || 'none',
  });
}

function autoSelectProvider(reason = 'auto') {
  const info = getActiveEmbeddingModelInfo();
  const config = info.model ? EMBEDDING_MODELS[info.model] : null;
  if (!info.model || !config) {
    const result = {
      ok: false,
      status: info.source === 'off' ? 'off' : 'waiting',
      reason: info.source,
      policy: info.policy || getEmbeddingPolicy(),
      selectionSource: info.source || 'none',
    };
    _trackEmbeddingAutoSelect(reason, result);
    return result;
  }
  if (!['auto_candidate', 'auto'].includes(info.source)) {
    return {
      ok: false,
      status: 'preserved',
      reason: info.source,
      model: info.model,
      provider: config.provider,
      policy: info.policy,
      selectionSource: info.source,
    };
  }

  const previous = _getKv('embedding:active_model');
  _setKv('embedding:active_model', info.model);
  _setKv('embedding:selection_mode', 'auto');
  _ensureVecTableForModel(info.model);
  if (previous !== info.model) _resetBackfillForModelChange();

  const result = {
    ok: true,
    status: previous === info.model ? 'unchanged' : 'selected',
    model: info.model,
    provider: config.provider,
    policy: info.policy,
    selectionSource: 'auto',
  };
  _trackEmbeddingAutoSelect(reason, result);
  if (previous !== info.model) trackEmbeddingStatus('auto_select');
  return result;
}

function getEmbeddingSetupState() {
  const policyRecord = _getEmbeddingPolicyRecord();
  const activeInfo = getActiveEmbeddingModelInfo();
  const activeConfig = activeInfo.model ? EMBEDDING_MODELS[activeInfo.model] : null;
  const localAvailable = Boolean(_firstAvailableModel(EMBEDDING_LOCAL_MODELS, EMBEDDING_POLICY.LOCAL_ONLY));
  const cloudAvailableModel = _firstAvailableModel(EMBEDDING_CLOUD_MODELS, EMBEDDING_POLICY.CLOUD_ALLOWED);
  const cloudAvailable = Boolean(cloudAvailableModel);
  const cloudOptInAvailable = !activeInfo.model
    && cloudAvailable
    && policyRecord.policy !== EMBEDDING_POLICY.CLOUD_ALLOWED
    && policyRecord.policy !== EMBEDDING_POLICY.OFF;

  let status = 'waiting_for_provider';
  let message = 'Memory Search will turn on automatically when a local embedding provider is available.';
  if (policyRecord.policy === EMBEDDING_POLICY.OFF) {
    status = 'off';
    message = 'Memory Search is off.';
  } else if (activeInfo.model) {
    status = activeInfo.source === 'manual' ? 'manual_active' : 'active';
    message = `${activeConfig.label} is powering Memory Search.`;
  } else if (cloudOptInAvailable) {
    status = 'cloud_opt_in_available';
    message = 'A cloud embedding provider is available. Opt in to enable semantic Memory Search.';
  } else if (!localAvailable && !cloudAvailable) {
    status = 'no_provider';
    message = 'Add a local embedding provider or cloud API key to enable semantic Memory Search.';
  }

  return {
    policy: policyRecord.policy,
    policySource: policyRecord.source,
    status,
    message,
    activeModel: activeInfo.model || null,
    activeProvider: activeConfig ? activeConfig.provider : null,
    activeLabel: activeConfig ? activeConfig.label : null,
    selectionSource: activeInfo.source || 'none',
    localAvailable,
    cloudAvailable,
    cloudAvailableModel,
    cloudOptInAvailable,
    totalIndexed: activeInfo.model ? _embeddingCountForModel(activeInfo.model) : 0,
  };
}

function setEmbeddingPolicy(policy) {
  const normalized = String(policy || '').trim().toLowerCase();
  if (!EMBEDDING_POLICY_VALUES.has(normalized)) {
    return { ok: false, message: `Unknown Memory Search policy: ${policy}` };
  }
  const previousPolicy = getEmbeddingPolicy();
  const previousModel = _getKv('embedding:active_model');
  const previousConfig = previousModel && EMBEDDING_MODELS[previousModel] ? EMBEDDING_MODELS[previousModel] : null;
  _setKv('embedding:auto_policy', normalized);
  if (normalized !== EMBEDDING_POLICY.OFF && _getKv('embedding:selection_mode') === 'auto') {
    _setKv('embedding:active_model', null);
  }
  if ((normalized === EMBEDDING_POLICY.LOCAL_ONLY || normalized === EMBEDDING_POLICY.OFF)
      && previousConfig
      && _isCloudProvider(previousConfig.provider)) {
    _setKv('embedding:active_model', null);
    _setKv('embedding:selection_mode', 'auto');
    _resetBackfillForModelChange();
  }
  const auto = autoSelectProvider('policy_update');
  const setup = getEmbeddingSetupState();
  const telemetry = _safeTelemetry();
  try {
    telemetry?.track?.('embedding_policy_update', {
      from: previousPolicy,
      to: normalized,
      status: setup.status,
      active_provider: setup.activeProvider || 'none',
    });
  } catch {}
  return { ok: true, policy: normalized, auto, setup };
}

// --- Embedding computation ---

async function computeEmbedding(text) {
  const result = await batchComputeEmbeddings([text]);
  return (result && result[0] != null) ? result[0] : null;
}

const _embedDisabledUntil = {}; // provider → timestamp: 1h backoff on 401/403/404

async function batchComputeEmbeddings(texts) {
  const config = getModelConfig();
  if (!config || texts.length === 0) return null;

  const { provider } = config;
  const embedFns = { google: _googleEmbed, voyage: _voyageEmbed, openai: _openaiEmbed, ollama: _ollamaEmbed };
  const fn = embedFns[provider];
  if (!fn) return null;

  // IMPORTANT: No cross-provider fallback for embeddings.
  // Vectors from different models have different dimensions and semantic spaces.
  // Mixing providers would corrupt the search index.
  // If the active provider fails, log the error and return null.
  // 404 backoff: if provider returns 404, disable for 1 hour (bad credentials/model)
  if (_embedDisabledUntil[provider] && Date.now() < _embedDisabledUntil[provider]) return null;
  try {
    return await fn(texts, config);
  } catch (err) {
    if (/\b404\b/.test(err.message) || /\b403\b/.test(err.message) || /\b401\b/.test(err.message)) {
      _embedDisabledUntil[provider] = Date.now() + 3600000;
      console.error(`[embeddings] ${provider} auth/404 error — disabled for 1h: ${err.message.slice(0, 120)}`);
    } else {
      console.error(`[embeddings] ${provider} failed: ${err.message}`);
    }
    return null;
  }
}

async function _googleEmbed(texts, config) {
  // Resolve credential: env var (static key) > brain DB > Gemini CLI OAuth
  let credential = _resolveGoogleCredential();
  if (!credential) throw new Error('No Google API key or OAuth token available');

  // If token is expired OAuth, try refreshing it
  if (credential.type === 'oauth' && credential.expired) {
    const refreshed = await _refreshGeminiOAuthToken();
    if (refreshed) credential = { type: 'oauth', token: refreshed, expired: false };
    else throw new Error('Google OAuth token expired and refresh failed');
  }

  // Use REST API directly — supports both API key and OAuth Bearer
  const baseUrl = 'https://generativelanguage.googleapis.com/v1beta';
  const url = credential.type === 'api_key'
    ? `${baseUrl}/models/${config.name}:batchEmbedContents?key=${credential.token}`
    : `${baseUrl}/models/${config.name}:batchEmbedContents`;
  const headers = { 'Content-Type': 'application/json' };
  if (credential.type === 'oauth') headers['Authorization'] = `Bearer ${credential.token}`;

  const resp = await fetch(url, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      requests: texts.map(text => ({ model: `models/${config.name}`, content: { parts: [{ text }] } })),
    }),
    signal: AbortSignal.timeout(30000),
  });

  if (!resp.ok) {
    const errBody = await resp.text().catch(() => '');
    // If 401/403 with OAuth, mark as expired for next attempt
    if ((resp.status === 401 || resp.status === 403) && credential.type === 'oauth') {
      throw new Error(`Google OAuth token rejected (${resp.status}) — may need re-authentication via Gemini CLI`);
    }
    throw new Error(`Google API error ${resp.status}: ${errBody.slice(0, 200)}`);
  }

  const data = await resp.json();
  if (!data.embeddings || !data.embeddings.length) throw new Error('Google batchEmbedContents returned no embeddings');
  return data.embeddings.map(e => e.values);
}

/**
 * Resolve Google credential from multiple sources.
 * Priority: static API key (env) > brain DB key > Gemini CLI OAuth token
 */
function _resolveGoogleCredential() {
  // 1. Static API key from env
  const envKey = process.env.GOOGLE_API_KEY;
  if (envKey && !envKey.startsWith('ya29.')) {
    return { type: 'api_key', token: envKey, expired: false };
  }

  // 2. Key from DB (may be static or OAuth)
  try {
    const adapter = _getAdapter();
    const providers = adapter.listModelProviders();
    const gp = providers.find(p => p.type === 'google');
    if (gp) {
      const full = adapter.getModelProviderWithKey(gp.id);
      const key = full?.api_key_encrypted;
      if (key && !key.startsWith('ya29.')) {
        return { type: 'api_key', token: key, expired: false };
      }
    }
  } catch {}

  // 3. Gemini CLI OAuth token (~/.gemini/oauth_creds.json)
  try {
    const fs = require('fs');
    const path = require('path');
    const credsPath = path.join(process.env.HOME, '.gemini', 'oauth_creds.json');
    if (fs.existsSync(credsPath)) {
      const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8'));
      if (creds.access_token) {
        const expired = creds.expiry_date ? Date.now() > creds.expiry_date : true;
        return { type: 'oauth', token: creds.access_token, expired, refreshToken: creds.refresh_token };
      }
    }
  } catch {}

  return null;
}

// Gemini CLI's public OAuth client credentials (from @google/gemini-cli bundle)
const GEMINI_CLI_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
const GEMINI_CLI_CLIENT_SECRET = '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';

/**
 * Refresh Gemini CLI OAuth token using the stored refresh_token.
 * Updates ~/.gemini/oauth_creds.json and brain DB on success.
 */
async function _refreshGeminiOAuthToken() {
  try {
    const fs = require('fs');
    const path = require('path');
    const credsPath = path.join(process.env.HOME, '.gemini', 'oauth_creds.json');
    const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8'));
    if (!creds.refresh_token) return null;

    const resp = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: creds.refresh_token,
        client_id: GEMINI_CLI_CLIENT_ID,
        client_secret: GEMINI_CLI_CLIENT_SECRET,
      }),
      signal: AbortSignal.timeout(10000),
    });
    const data = await resp.json();
    if (!data.access_token) return null;

    // Save refreshed token
    creds.access_token = data.access_token;
    creds.expiry_date = Date.now() + (data.expires_in || 3600) * 1000;
    fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });

    // Also update DB if Google provider exists with old OAuth token
    try {
      const adapter = _getAdapter();
      const providers = adapter.listModelProviders();
      const gp = providers.find(p => p.type === 'google');
      if (gp && adapter.upsertModelProvider) {
        adapter.upsertModelProvider({ id: gp.id, name: gp.name, type: 'google', baseUrl: gp.base_url, apiKeyEncrypted: data.access_token, enabled: true });
      }
    } catch {}

    console.log('[embeddings] Refreshed Google OAuth token');
    return data.access_token;
  } catch (err) {
    console.error('[embeddings] OAuth refresh failed:', err.message);
    return null;
  }
}

async function _voyageEmbed(texts, config) {
  const apiKey = process.env.VOYAGE_API_KEY;
  if (!apiKey) return null;

  const resp = await fetch('https://api.voyageai.com/v1/embeddings', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
    body: JSON.stringify({ model: config.name, input: texts, input_type: 'document' }),
    signal: AbortSignal.timeout(30000),
  });
  if (!resp.ok) throw new Error(`Voyage API error: ${resp.status}`);
  const data = await resp.json();
  return data.data.map(d => d.embedding);
}

async function _openaiEmbed(texts, config) {
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) return null;

  const resp = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
    body: JSON.stringify({ model: config.name, input: texts }),
    signal: AbortSignal.timeout(30000),
  });
  if (!resp.ok) throw new Error(`OpenAI API error: ${resp.status}`);
  const data = await resp.json();
  return data.data.map(d => d.embedding);
}

async function _ollamaEmbed(texts, config) {
  const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
  // Ollama processes embeddings sequentially — batch too large and it hangs.
  // Split into sub-batches of 32 (nomic-embed-text sweet spot) for reliability.
  const SUB_BATCH = 32;
  const allEmbeddings = [];
  for (let i = 0; i < texts.length; i += SUB_BATCH) {
    const chunk = texts.slice(i, i + SUB_BATCH);
    const resp = await fetch(`${baseUrl}/api/embed`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ model: config.name, input: chunk }),
      signal: AbortSignal.timeout(120000), // 2 min timeout (model cold-start can be slow)
    });
    if (!resp.ok) throw new Error(`Ollama API error: ${resp.status}`);
    const data = await resp.json();
    if (!data.embeddings) throw new Error('Ollama returned no embeddings');
    allEmbeddings.push(...data.embeddings);
  }
  return allEmbeddings;
}

// --- Per-model database tables ---

function _vecTableName(modelKey) {
  return `embedding_vec_${modelKey}`;
}

// vec0 column type for a model: int8[N] for models that opt into quantization
// (config.quantize==='int8'), float[N] otherwise. int8 stores 1 byte/dim vs 4 — ~6x
// smaller on disk + faster KNN, at ~95% recall@10 for unit-normalized vectors (nomic).
function _vecColType(config) {
  return `${config.quantize === 'int8' ? 'int8' : 'float'}[${config.dimensions}]`;
}

// The bind expression that turns a JSON-array param into the stored/queried vector.
// Derived from the ACTUAL declared table type (not just config) so the code is correct
// whether the table is still float32 (pre-migration) or already int8 (post-migration) —
// no fragile coupling between the schema and a flag. 'unit' assumes inputs in [-1,1]
// (true for L2-normalized models like nomic-embed-text), the only mode sqlite-vec supports.
function _declaredVecType(db, table) {
  try {
    const r = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?").get(table);
    if (!r || !r.sql) return null;
    if (/\bint8\s*\[/.test(r.sql)) return 'int8';
    if (/\bfloat\s*\[/.test(r.sql)) return 'float';
  } catch {}
  return null;
}
function _vecBindExpr(db, config) {
  return _declaredVecType(db, _vecTableName(config.key)) === 'int8'
    ? "vec_quantize_int8(vec_f32(?),'unit')"
    : 'vec_f32(?)';
}

/**
 * Create embedding tables for the active model.
 * Each model gets its own vec table. The mapping table is shared.
 */
function createEmbeddingTables() {
  if (!ensureVecLoaded()) return false;
  const config = getModelConfig();
  if (!config) return false;

  const db = _getAdapter().getDb();

  // Shared mapping table
  db.prepare(`
    CREATE TABLE IF NOT EXISTS embedding_map (
      rowid INTEGER PRIMARY KEY AUTOINCREMENT,
      entity_id TEXT NOT NULL,
      entity_type TEXT NOT NULL,
      model TEXT NOT NULL,
      created_at TEXT,
      UNIQUE(entity_id, model)
    )
  `).run();
  db.prepare('CREATE INDEX IF NOT EXISTS idx_embedding_map_type ON embedding_map(entity_type)').run();
  db.prepare('CREATE INDEX IF NOT EXISTS idx_embedding_map_model ON embedding_map(model)').run();

  // Per-model vec table
  const vecTable = _vecTableName(config.key);
  const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(vecTable);
  if (!existing) {
    db.prepare(`CREATE VIRTUAL TABLE "${vecTable}" USING vec0(embedding ${_vecColType(config)})`).run();
    console.log(`[embeddings] Created vec table: ${vecTable} (${_vecColType(config)})`);
  }

  // Migrate from old single-table layout if it exists
  _migrateOldTable(db, config);

  return true;
}

/**
 * Migrate from the old single embedding_vec table to per-model tables.
 * Moves data to the new table and drops the old one.
 */
function _migrateOldTable(db, config) {
  const oldExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embedding_vec'").get();
  if (!oldExists) return;

  const oldCount = db.prepare('SELECT count(*) as c FROM embedding_map WHERE model = ?').get(config.name)?.c || 0;
  if (oldCount === 0) {
    // Old table has data but model column might be from a different model — just drop
    _dropTable(db, 'embedding_vec');
    return;
  }

  // Move vectors from old table to new per-model table
  const vecTable = _vecTableName(config.key);
  try {
    const rows = db.prepare('SELECT em.rowid as map_rowid FROM embedding_map em WHERE em.model = ?').all(config.name);
    const newTableHas = db.prepare(`SELECT count(*) as c FROM "${vecTable}"`).get()?.c || 0;
    if (newTableHas === 0 && rows.length > 0) {
      console.log(`[embeddings] Migrating ${rows.length} vectors from embedding_vec → ${vecTable}`);
      for (const row of rows) {
        try {
          const vec = db.prepare('SELECT embedding FROM embedding_vec WHERE rowid = ?').get(BigInt(row.map_rowid));
          if (vec) {
            db.prepare(`INSERT OR IGNORE INTO "${vecTable}"(rowid, embedding) VALUES (?, ?)`).run(BigInt(row.map_rowid), vec.embedding);
          }
        } catch {}
      }
    }
  } catch (err) {
    console.error('[embeddings] Migration error (non-fatal):', err.message);
  }

  _dropTable(db, 'embedding_vec');
}

function _dropTable(db, baseName) {
  const tables = db.prepare(
    "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? || '%'"
  ).all(baseName);
  for (const t of tables) {
    db.prepare(`DROP TABLE IF EXISTS "${t.name}"`).run();
  }
}

// --- Storage ---

function storeEmbedding(entityId, entityType, embedding) {
  if (!_vecLoaded) return;
  const config = getModelConfig();
  if (!config) return;

  const db = _getAdapter().getDb();
  const vecTable = _vecTableName(config.key);

  try {
    const upsert = db.transaction(() => {
      const existing = db.prepare('SELECT rowid FROM embedding_map WHERE entity_id = ? AND model = ?').get(entityId, config.name);
      let rowid;

      if (existing) {
        rowid = existing.rowid;
        db.prepare("UPDATE embedding_map SET created_at = datetime('now') WHERE rowid = ?").run(rowid);
        db.prepare(`DELETE FROM "${vecTable}" WHERE rowid = ?`).run(BigInt(rowid));
      } else {
        // Remove entries for this entity from OTHER models (switching providers)
        // so the UNIQUE constraint on entity_id doesn't block the insert
        const oldEntry = db.prepare('SELECT rowid, model FROM embedding_map WHERE entity_id = ?').get(entityId);
        if (oldEntry && oldEntry.model !== config.name) {
          const oldVecTable = _vecTableName(EMBEDDING_MODELS[oldEntry.model]?.key || oldEntry.model);
          try { db.prepare(`DELETE FROM "${oldVecTable}" WHERE rowid = ?`).run(BigInt(oldEntry.rowid)); } catch {}
          db.prepare('DELETE FROM embedding_map WHERE rowid = ?').run(oldEntry.rowid);
        }
        const info = db.prepare("INSERT INTO embedding_map(entity_id, entity_type, model, created_at) VALUES (?, ?, ?, datetime('now'))").run(entityId, entityType, config.name);
        rowid = info.lastInsertRowid;
      }

      db.prepare(`INSERT INTO "${vecTable}"(rowid, embedding) VALUES (?, ${_vecBindExpr(db, config)})`).run(
        BigInt(rowid), JSON.stringify(embedding)
      );
    });
    upsert();
  } catch (err) {
    // Rate-limit repeated error logging (e.g. missing vec0 table spams 700K+ lines)
    if (!storeEmbedding._errCount) storeEmbedding._errCount = 0;
    storeEmbedding._errCount++;
    if (storeEmbedding._errCount <= 3 || storeEmbedding._errCount % 1000 === 0) {
      console.error(`[embeddings] Failed to store embedding for ${entityId}: ${err.message}${storeEmbedding._errCount > 3 ? ` (${storeEmbedding._errCount} failures total)` : ''}`);
    }
    // If vec0 table doesn't exist, disable to stop all future attempts
    if (err.message.includes('no such module: vec0') || err.message.includes('no such table')) {
      console.error('[embeddings] Disabling vec storage — table not available. Re-run init() to recreate.');
      _vecLoaded = false;
    }
  }
}

function hasEmbedding(entityId, entityType) {
  if (!_vecLoaded) return false;
  const model = getEmbeddingModel();
  if (!model) return false;
  try {
    if (entityType) {
      return !!_getAdapter().getDb().prepare('SELECT 1 FROM embedding_map WHERE entity_id = ? AND entity_type = ? AND model = ?').get(entityId, entityType, model);
    }
    return !!_getAdapter().getDb().prepare('SELECT 1 FROM embedding_map WHERE entity_id = ? AND model = ?').get(entityId, model);
  } catch { return false; }
}

function getEmbeddingStats() {
  if (!_vecLoaded) return { total: 0, memories: 0, knowledge: 0, model: null };
  const model = getEmbeddingModel();
  if (!model) return { total: 0, memories: 0, knowledge: 0, model: null };
  try {
    const db = _getAdapter().getDb();
    const total = db.prepare('SELECT count(*) as c FROM embedding_map WHERE model = ?').get(model).c;
    const memories = db.prepare("SELECT count(*) as c FROM embedding_map WHERE model = ? AND entity_type = 'memory'").get(model).c;
    const knowledge = db.prepare("SELECT count(*) as c FROM embedding_map WHERE model = ? AND entity_type = 'knowledge'").get(model).c;
    return { total, memories, knowledge, model };
  } catch {
    return { total: 0, memories: 0, knowledge: 0, model };
  }
}

/**
 * Get stats for ALL providers (for setup page).
 */
function getAllEmbeddingStats() {
  if (!_vecLoaded) return {};
  try {
    const db = _getAdapter().getDb();
    const rows = db.prepare('SELECT model, count(*) as c FROM embedding_map GROUP BY model').all();
    const stats = {};
    for (const row of rows) {
      stats[row.model] = row.c;
    }
    return stats;
  } catch { return {}; }
}

/**
 * Delete all embeddings for a specific model.
 * Returns { deleted: number }.
 */
function deleteEmbeddingsForModel(modelName) {
  const config = EMBEDDING_MODELS[modelName];
  if (!config) return { deleted: 0 };
  if (!_vecLoaded) return { deleted: 0 };

  const db = _getAdapter().getDb();
  const vecTable = _vecTableName(config.key);

  try {
    const count = db.prepare('SELECT count(*) as c FROM embedding_map WHERE model = ?').get(modelName)?.c || 0;
    db.transaction(() => {
      db.prepare('DELETE FROM embedding_map WHERE model = ?').run(modelName);
      _dropTable(db, vecTable);
    })();
    console.log(`[embeddings] Deleted ${count} embeddings for ${modelName}`);
    return { deleted: count };
  } catch (err) {
    console.error('[embeddings] Delete failed:', err.message);
    return { deleted: 0, error: err.message };
  }
}

// Delete all embeddings for one entity (its embedding_map rows across models + the
// matching vec-table rows keyed by rowid). Call this whenever an entity is hard-deleted
// so we don't leak orphaned vectors (the source of the audited 47,649 orphans — skills
// that re-sync by `DELETE FROM memories WHERE id = ?` left the embeddings behind).
function deleteEmbeddingsForEntity(entityId) {
  if (!entityId) return { deleted: 0 };
  // Load vec0 into this connection if not already (skill subprocesses may not have
  // touched the embeddings module yet, but their brain.initDb() opened the DB).
  if (!ensureVecLoaded()) return { deleted: 0 };
  const db = _getAdapter().getDb();
  try {
    const rows = db.prepare('SELECT rowid, model FROM embedding_map WHERE entity_id = ?').all(entityId);
    if (!rows.length) return { deleted: 0 };
    db.transaction(() => {
      for (const r of rows) {
        const cfg = EMBEDDING_MODELS[r.model];
        if (cfg) {
          try { db.prepare(`DELETE FROM "${_vecTableName(cfg.key)}" WHERE rowid = ?`).run(BigInt(r.rowid)); } catch {}
        }
      }
      db.prepare('DELETE FROM embedding_map WHERE entity_id = ?').run(entityId);
    })();
    return { deleted: rows.length };
  } catch (err) {
    return { deleted: 0, error: err.message };
  }
}

// One-time / periodic sweep: delete embeddings whose entity no longer exists. Only sweeps
// entity types whose source table we can verify (memory→memories, knowledge→knowledge);
// other types are left untouched. Batched per-transaction so each holds the cross-process
// write lock only briefly (lock-friendly, per the CTM lesson). entity_type is parameterized;
// the source-table names are internal constants (no user input → no injection).
const _ORPHAN_ENTITY_TABLES = { memory: 'memories', knowledge: 'knowledge' };
function pruneOrphanEmbeddings({ batchSize = 200 } = {}) {
  if (!ensureVecLoaded()) return { deleted: 0 };
  const db = _getAdapter().getDb();
  const batch = Math.max(50, Math.min(2000, Number(batchSize) || 200));
  let deleted = 0;
  for (const [etype, table] of Object.entries(_ORPHAN_ENTITY_TABLES)) {
    // Guard: only sweep if the source table exists, else skip (never mass-delete blind).
    const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table);
    if (!hasTable) continue;
    for (let i = 0; i < 5000; i += 1) { // batch backstop
      const orphans = db.prepare(
        `SELECT em.rowid AS rowid, em.model AS model
           FROM embedding_map em
          WHERE em.entity_type = ?
            AND NOT EXISTS (SELECT 1 FROM ${table} t WHERE t.id = em.entity_id)
          LIMIT ?`
      ).all(etype, batch);
      if (!orphans.length) break;
      db.transaction(() => {
        for (const o of orphans) {
          const cfg = EMBEDDING_MODELS[o.model];
          if (cfg) {
            try { db.prepare(`DELETE FROM "${_vecTableName(cfg.key)}" WHERE rowid = ?`).run(BigInt(o.rowid)); } catch {}
          }
          db.prepare('DELETE FROM embedding_map WHERE rowid = ?').run(o.rowid);
        }
      })();
      deleted += orphans.length;
      if (orphans.length < batch) break;
    }
  }
  if (deleted) console.log(`[embeddings] pruneOrphanEmbeddings removed ${deleted} orphaned embeddings`);
  return { deleted };
}

// --- Vector search ---

function searchSimilarMemories(queryEmbedding, limit = 10, filters = {}) {
  if (!_vecLoaded) return [];
  const config = getModelConfig();
  if (!config) return [];

  try {
    const db = _getAdapter().getDb();
    const vecTable = _vecTableName(config.key);
    const kFetch = Math.min(limit * 3, 100);

    let sql = `
      SELECT m.*, v.distance
      FROM "${vecTable}" v
      JOIN embedding_map em ON em.rowid = v.rowid
      JOIN memories m ON m.id = em.entity_id
      WHERE v.embedding MATCH ${_vecBindExpr(db, config)} AND k = ?
        AND em.entity_type = 'memory' AND em.model = ?
    `;
    const params = [JSON.stringify(queryEmbedding), kFetch, config.name];

    if (filters.source) { sql += ' AND m.source = ?'; params.push(filters.source); }
    if (filters.domain) { sql += ' AND m.domain = ?'; params.push(filters.domain); }
    if (filters.topic)  { sql += ' AND m.topic = ?';  params.push(filters.topic); }

    sql += ' ORDER BY v.distance LIMIT ?';
    params.push(limit);

    const rows = db.prepare(sql).all(...params);
    _trackEmbeddingUsage('memory_search', rows.length);
    return rows;
  } catch (err) {
    console.error('[embeddings] Vector search failed:', err.message);
    _trackEmbeddingUsage('memory_search', 0, 'error');
    return [];
  }
}

function searchSimilarKnowledge(queryEmbedding, limit = 10) {
  if (!_vecLoaded) return [];
  const config = getModelConfig();
  if (!config) return [];

  try {
    const db = _getAdapter().getDb();
    const vecTable = _vecTableName(config.key);
    const kFetch = Math.min(limit * 3, 100);

    const rows = db.prepare(`
      SELECT kn.*, v.distance
      FROM "${vecTable}" v
      JOIN embedding_map em ON em.rowid = v.rowid
      JOIN knowledge kn ON kn.id = em.entity_id
      WHERE v.embedding MATCH ${_vecBindExpr(db, config)} AND k = ?
        AND em.entity_type = 'knowledge' AND em.model = ?
        AND kn.status = 'active'
      ORDER BY v.distance
      LIMIT ?
    `).all(JSON.stringify(queryEmbedding), kFetch, config.name, limit);
    _trackEmbeddingUsage('knowledge_search', rows.length);
    return rows;
  } catch (err) {
    console.error('[embeddings] Knowledge vector search failed:', err.message);
    _trackEmbeddingUsage('knowledge_search', 0, 'error');
    return [];
  }
}

/**
 * Generic vector search — returns embedding_map rows with distance.
 * Unlike searchSimilarMemories/Knowledge, this doesn't JOIN a specific entity table.
 * Consumers can then join/lookup results themselves.
 */
function searchSimilarEntities(queryEmbedding, limit = 10, entityType = null) {
  if (!_vecLoaded) return [];
  const config = getModelConfig();
  if (!config) return [];

  try {
    const db = _getAdapter().getDb();
    const vecTable = _vecTableName(config.key);
    const kFetch = Math.min(limit * 3, 100);

    let sql = `
      SELECT em.entity_id, em.entity_type, v.distance
      FROM "${vecTable}" v
      JOIN embedding_map em ON em.rowid = v.rowid
      WHERE v.embedding MATCH ${_vecBindExpr(db, config)} AND k = ?
        AND em.model = ?
    `;
    const params = [JSON.stringify(queryEmbedding), kFetch, config.name];

    if (entityType) { sql += ' AND em.entity_type = ?'; params.push(entityType); }

    sql += ' ORDER BY v.distance LIMIT ?';
    params.push(limit);

    const rows = db.prepare(sql).all(...params);
    _trackEmbeddingUsage(entityType ? `${entityType}_entity_search` : 'entity_search', rows.length);
    return rows;
  } catch (err) {
    console.error('[embeddings] Entity vector search failed:', err.message);
    _trackEmbeddingUsage(entityType ? `${entityType}_entity_search` : 'entity_search', 0, 'error');
    return [];
  }
}

// --- Hybrid search (Reciprocal Rank Fusion) ---

function reciprocalRankFusion(lexicalResults, vectorResults, limit, k = 60) {
  const scores = new Map();
  const items = new Map();

  for (let i = 0; i < lexicalResults.length; i++) {
    const id = lexicalResults[i].id;
    scores.set(id, (scores.get(id) || 0) + 1 / (k + i + 1));
    items.set(id, lexicalResults[i]);
  }

  for (let i = 0; i < vectorResults.length; i++) {
    const id = vectorResults[i].id;
    scores.set(id, (scores.get(id) || 0) + 1 / (k + i + 1));
    if (!items.has(id)) items.set(id, vectorResults[i]);
  }

  return Array.from(scores.entries())
    .sort((a, b) => b[1] - a[1])
    .slice(0, limit)
    .map(([id, score]) => ({ ...items.get(id), _rrf_score: score }));
}

// --- Provider switching ---

/**
 * Switch the active embedding provider.
 * Creates the new vec table and resets backfill cursors.
 * Old provider's embeddings are preserved until explicitly deleted.
 * @param {string} modelName - e.g., 'text-embedding-004'
 * @returns {{ ok: boolean, message: string }}
 */
function switchProvider(modelName) {
  const previousModel = getEmbeddingModel();
  const config = EMBEDDING_MODELS[modelName];
  if (!config) {
    const result = { ok: false, message: `Unknown model: ${modelName}` };
    _trackEmbeddingProviderSwitch(previousModel, modelName, result);
    return result;
  }
  if (!_hasApiKey(config.provider)) {
    const result = { ok: false, message: `No API key for ${config.label}` };
    _trackEmbeddingProviderSwitch(previousModel, modelName, result);
    return result;
  }

  // Save preference
  _setKv('embedding:active_model', modelName);
  _setKv('embedding:selection_mode', 'manual');

  // Reset cached dimensions so they're recalculated
  _dimensions = null;

  // Create the new vec table if needed
  _ensureVecTableForModel(modelName);

  // Reset backfill cursors so the new model gets backfilled
  _resetBackfillForModelChange();

  console.log(`[embeddings] Switched to ${config.label} (${modelName}, ${config.dimensions}d)`);
  const result = { ok: true, message: `Switched to ${config.label}` };
  _trackEmbeddingProviderSwitch(previousModel, modelName, result);
  return result;
}

// --- Initialization ---

function init(adapter) {
  // Sub-phase timing so the boot profile pinpoints what's slow inside embeddings.init
  // (on the live brain this whole call was ~15s; isolate which step). No-op if profiler absent.
  let _boot; try { _boot = require('./lib/boot-profile'); } catch { _boot = { measure: (_l, f) => f() }; }
  if (adapter) _adapter = adapter;
  if (!_boot.measure('embeddings.ensureVecLoaded', () => ensureVecLoaded())) {
    console.log('[embeddings] sqlite-vec not available — vector search disabled');
    return false;
  }
  _boot.measure('embeddings.autoSelectProvider', () => autoSelectProvider('boot'));
  if (!getEmbeddingModel()) {
    console.log('[embeddings] No embedding provider active — vector search disabled until local provider is available or cloud embeddings are allowed');
    return false;
  }
  const ok = _boot.measure('embeddings.createEmbeddingTables', () => createEmbeddingTables());
  if (ok) {
    const stats = _boot.measure('embeddings.getEmbeddingStats', () => getEmbeddingStats());
    const config = getModelConfig();
    console.log(`[embeddings] Initialized (${stats.total} embeddings, model: ${config.name}, dims: ${config.dimensions}, provider: ${config.label})`);
  }
  return ok;
}

function _resetForTesting() {
  _vecLoaded = false;
  _dimensions = null;
  _adapter = null;
  _defaultAdapter = null;
  _ollamaAvailableCache = { value: false, ts: 0 };
  _lastSetupStatusTelemetry = { key: '', ts: 0 };
  _lastUsageTelemetryByKey = new Map();
}

module.exports = {
  init, ensureVecLoaded, isAvailable, createEmbeddingTables,
  getEmbeddingModel, getActiveEmbeddingModelInfo, getEmbeddingPolicy, setEmbeddingPolicy,
  getEmbeddingSetupState, autoSelectProvider,
  getModelConfig, getDimensions, EMBEDDING_MODELS, EMBEDDING_POLICY,
  trackEmbeddingStatus, trackEmbeddingSetupView,
  computeEmbedding, batchComputeEmbeddings,
  storeEmbedding, hasEmbedding, getEmbeddingStats, getAllEmbeddingStats,
  getProviderStatus, switchProvider, deleteEmbeddingsForModel,
  deleteEmbeddingsForEntity, pruneOrphanEmbeddings,
  searchSimilarMemories, searchSimilarKnowledge, searchSimilarEntities, reciprocalRankFusion,
  _resetForTesting,
};
