'use strict';
// Brain-DB space optimization core + CLI. Idempotent, safe to run repeatedly.
//   1. Embeddings: convert embedding_vec_ollama float32 -> int8 IN PLACE (same table name)
//   2. memories_fts -> external-content + maintenance triggers
//   3. content_raw: NULL where byte-identical to content (readers use content_raw ?? content)
//   4. lost_and_found: drop (orphaned .recover residue) — only when opts.dropLostAndFound
//   5. VACUUM (opts.vacuum)
//
// Exposed as a function so the deferred upgrader loop (loops/brain-optimize.js) and the CLI
// share one implementation. The caller must have sqlite-vec loaded on `db` (the daemon's
// connection already does; the CLI loads it below).
//
// CLI: <node25> migrate.js <dbPath> [--no-vacuum] [--keep-lost-and-found]

function declaredVecType(db, t) {
  const r = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?").get(t);
  if (!r || !r.sql) return null;
  if (/\bint8\s*\[/.test(r.sql)) return 'int8';
  if (/\bfloat\s*\[/.test(r.sql)) return 'float';
  return '?';
}
function has(db, name) { return !!db.prepare("SELECT 1 FROM sqlite_master WHERE name=?").get(name); }

// True if any optimization is still pending (used to skip no-op work / gate the loop).
function needsOptimization(db) {
  if (has(db, 'embedding_vec_ollama') && declaredVecType(db, 'embedding_vec_ollama') === 'float') return true;
  const fts = db.prepare("SELECT sql FROM sqlite_master WHERE name='memories_fts'").get();
  if (fts && !/content\s*=\s*'memories'/.test(fts.sql)) return true;
  if (has(db, 'memories')) {
    const red = db.prepare("SELECT 1 FROM memories WHERE content_raw IS NOT NULL AND content_raw=content LIMIT 1").get();
    if (red) return true;
  }
  return false;
}
function embeddingCount(db) {
  try { return db.prepare("SELECT COUNT(*) c FROM embedding_vec_ollama").get().c; } catch { return 0; }
}

function _migrateEmbeddings(db, log) {
  const T = 'embedding_vec_ollama';
  if (!has(db, T)) { log('embeddings: no', T, '- skip'); return; }
  const type = declaredVecType(db, T);
  if (type === 'int8') { log('embeddings: already int8 - skip'); return; }
  if (type !== 'float') { log('embeddings: unexpected type', type, '- skip'); return; }
  const dims = (db.prepare("SELECT sql FROM sqlite_master WHERE name=?").get(T).sql.match(/\[(\d+)\]/) || [])[1] || '768';
  const n = db.prepare(`SELECT COUNT(*) c FROM ${T}`).get().c;
  const tmp = T + '_i8mig';
  log(`embeddings: quantizing ${n} float32[${dims}] vectors -> int8...`);
  db.exec('BEGIN');
  db.exec(`DROP TABLE IF EXISTS "${tmp}"`);
  db.exec(`CREATE VIRTUAL TABLE "${tmp}" USING vec0(embedding int8[${dims}])`);
  db.exec(`INSERT INTO "${tmp}"(rowid, embedding) SELECT rowid, vec_quantize_int8(embedding,'unit') FROM "${T}"`);
  db.exec(`DROP TABLE "${T}"`);
  db.exec(`CREATE VIRTUAL TABLE "${T}" USING vec0(embedding int8[${dims}])`);
  db.exec(`INSERT INTO "${T}"(rowid, embedding) SELECT rowid, embedding FROM "${tmp}"`);
  db.exec(`DROP TABLE "${tmp}"`);
  db.exec('COMMIT');
  const after = db.prepare(`SELECT COUNT(*) c FROM ${T}`).get().c;
  if (after !== n) throw new Error(`embedding row count changed ${n} -> ${after}`);
  log(`embeddings: done, ${after} int8 vectors`);
}

function _ftsTriggers(db) {
  db.exec(`CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories BEGIN
    INSERT INTO memories_fts(rowid, content, source, participants, source_channel)
    VALUES (NEW.rowid, NEW.content, NEW.source, COALESCE(NEW.participants,''), COALESCE(NEW.source_channel,''));
  END`);
  db.exec(`CREATE TRIGGER memories_fts_delete AFTER DELETE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, content, source, participants, source_channel)
    VALUES('delete', OLD.rowid, OLD.content, OLD.source, COALESCE(OLD.participants,''), COALESCE(OLD.source_channel,''));
  END`);
  db.exec(`CREATE TRIGGER memories_fts_update AFTER UPDATE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, content, source, participants, source_channel)
    VALUES('delete', OLD.rowid, OLD.content, OLD.source, COALESCE(OLD.participants,''), COALESCE(OLD.source_channel,''));
    INSERT INTO memories_fts(rowid, content, source, participants, source_channel)
    VALUES (NEW.rowid, NEW.content, NEW.source, COALESCE(NEW.participants,''), COALESCE(NEW.source_channel,''));
  END`);
}
function _migrateFts(db, log) {
  const cur = db.prepare("SELECT sql FROM sqlite_master WHERE name='memories_fts'").get();
  if (!cur) { log('fts: no memories_fts - skip'); return; }
  if (/content\s*=\s*'memories'/.test(cur.sql)) { log('fts: already external-content - skip'); return; }
  log('fts: converting to external-content...');
  db.exec('BEGIN');
  db.exec('DROP TRIGGER IF EXISTS memories_fts_insert');
  db.exec('DROP TRIGGER IF EXISTS memories_fts_delete');
  db.exec('DROP TRIGGER IF EXISTS memories_fts_update');
  db.exec('DROP TABLE memories_fts');
  db.exec(`CREATE VIRTUAL TABLE memories_fts USING fts5(
    content, source, participants, source_channel,
    content='memories', content_rowid='rowid', tokenize='porter unicode61')`);
  db.exec(`INSERT INTO memories_fts(memories_fts) VALUES('rebuild')`);
  _ftsTriggers(db);
  db.exec('COMMIT');
  log('fts: external-content + triggers installed, rebuilt');
}

function _migrateContentRaw(db, log) {
  if (!has(db, 'memories')) return;
  const n = db.prepare("SELECT COUNT(*) c FROM memories WHERE content_raw IS NOT NULL AND content_raw=content").get().c;
  if (n === 0) { log('content_raw: nothing redundant - skip'); return; }
  const hadTrig = !!db.prepare("SELECT 1 FROM sqlite_master WHERE name='memories_fts_update'").get();
  db.exec('BEGIN');
  if (hadTrig) db.exec('DROP TRIGGER IF EXISTS memories_fts_update'); // content unchanged -> FTS unaffected
  const r = db.prepare("UPDATE memories SET content_raw=NULL WHERE content_raw IS NOT NULL AND content_raw=content").run();
  if (hadTrig) db.exec(`CREATE TRIGGER memories_fts_update AFTER UPDATE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, content, source, participants, source_channel)
    VALUES('delete', OLD.rowid, OLD.content, OLD.source, COALESCE(OLD.participants,''), COALESCE(OLD.source_channel,''));
    INSERT INTO memories_fts(rowid, content, source, participants, source_channel)
    VALUES (NEW.rowid, NEW.content, NEW.source, COALESCE(NEW.participants,''), COALESCE(NEW.source_channel,''));
  END`);
  db.exec('COMMIT');
  log(`content_raw: nulled ${r.changes} redundant rows`);
}

function _dropLostAndFound(db, log) {
  if (!has(db, 'lost_and_found')) { log('lost_and_found: absent - skip'); return; }
  db.exec('DROP TABLE lost_and_found');
  log('lost_and_found: dropped');
}

/**
 * Run the space optimization on an open better-sqlite3 `db` that already has sqlite-vec loaded.
 * opts: { vacuum=true, dropLostAndFound=false, log=console.log }
 * Each phase is idempotent and transactional; the float32 vectors are never dropped until the
 * int8 copy is built and verified in the same transaction (a crash rolls back to float32).
 */
function optimizeBrain(db, opts = {}) {
  const { vacuum = true, dropLostAndFound = false, log = () => {} } = opts;
  _migrateEmbeddings(db, log);
  _migrateFts(db, log);
  _migrateContentRaw(db, log);
  if (dropLostAndFound) _dropLostAndFound(db, log);
  if (vacuum) { log('vacuuming...'); db.exec('VACUUM'); try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {} }
}

module.exports = { optimizeBrain, needsOptimization, embeddingCount, declaredVecType };

// ---- CLI ----
if (require.main === module) {
  const path = require('path');
  const WE = path.resolve(__dirname, '..', '..');
  const Database = require(path.join(WE, 'node_modules', 'better-sqlite3'));
  const vec = require(path.join(WE, 'node_modules', 'sqlite-vec'));
  const DB = process.argv[2];
  if (!DB) { console.error('usage: migrate.js <dbPath> [--no-vacuum] [--keep-lost-and-found]'); process.exit(1); }
  const db = new Database(DB);
  vec.load(db);
  db.pragma('busy_timeout = 60000');
  const MB = b => (b / 1048576).toFixed(0);
  const before = db.prepare('SELECT SUM(pgsize) b FROM dbstat').get().b;
  const t0 = Date.now();
  optimizeBrain(db, {
    vacuum: !process.argv.includes('--no-vacuum'),
    dropLostAndFound: !process.argv.includes('--keep-lost-and-found'),
    log: (...a) => console.log('[migrate]', ...a),
  });
  const after = db.prepare('SELECT SUM(pgsize) b FROM dbstat').get().b;
  console.log(`[migrate] logical size ${MB(before)} -> ${MB(after)} MB in ${((Date.now()-t0)/1000).toFixed(1)}s`);
  db.close();
  console.log('[migrate] DONE');
}
