'use strict';

// Background migration: backfill session_message_rows for EXISTING sessions whose rows are
// not yet complete. Source = the existing (small, ≤24 MB) blob; over-cap sessions ('[]')
// stay on the JSONL render path (the gate keeps them off rows) until re-imported. Idempotent,
// bounded, resumable — and the render gate (db.sessionContentRowsAvailable) keeps a
// half-migrated session on the blob path, so a sweep in progress never breaks reads.
//
// `db` is the better-sqlite3 handle (dbModule.getDb()); `replaceSessionMessageRows` is the
// db.js writer (operates on the same handle). Passed in so this stays unit-testable in
// isolation.

// Sessions with a real blob whose row count is below the conversation's message count.
function findUnbackfilledSessions(db, limit = 50) {
  const lim = Math.max(1, Math.min(100000, Number(limit) || 50));
  return db.prepare(`
    SELECT sc.ctm_session_id AS id
    FROM session_conversations sc
    WHERE sc.extracted_source_len > 0
      AND sc.messages IS NOT NULL AND sc.messages != '[]' AND length(sc.messages) > 2
      AND (SELECT COUNT(*) FROM session_message_rows r WHERE r.ctm_session_id = sc.ctm_session_id) < sc.extracted_source_len
    LIMIT ?
  `).all(lim).map(r => r.id);
}

// Backfill one session's rows from its blob. Returns rows written (0 if no parseable blob).
function backfillSessionContentRows(db, sessionId, replaceSessionMessageRows) {
  const row = db.prepare('SELECT messages FROM session_conversations WHERE ctm_session_id = ?').get(sessionId);
  if (!row || !row.messages) return 0;
  let msgs;
  try { msgs = JSON.parse(row.messages); } catch { return 0; }
  if (!Array.isArray(msgs) || msgs.length === 0) return 0;
  return replaceSessionMessageRows(sessionId, msgs);
}

// Disk reclaim: sessions whose faithful rows are COMPLETE (count >= extracted_source_len > 0 — the
// same gate as sessionContentRowsAvailable) but whose blob is still present. Once rows serve the
// render, the blob is dead weight; nulling it reclaims disk. Reversible: re-import from the JSONL
// rebuilds it. Excludes already-empty/over-cap blobs.
function findReclaimableSessions(db, limit = 50) {
  const lim = Math.max(1, Math.min(100000, Number(limit) || 50));
  return db.prepare(`
    SELECT sc.ctm_session_id AS id
    FROM session_conversations sc
    WHERE sc.messages IS NOT NULL AND sc.messages != '[]' AND length(sc.messages) > 2
      AND sc.extracted_source_len > 0
      AND (SELECT COUNT(*) FROM session_message_rows r WHERE r.ctm_session_id = sc.ctm_session_id) >= sc.extracted_source_len
    LIMIT ?
  `).all(lim).map(r => r.id);
}

// Null the blob (messages='[]') for complete-rows sessions. Bounded + idempotent. The caller only
// runs this when blob retirement is active (CTM_DUAL_WRITE_BLOB=0) — otherwise the next import would
// just rewrite the blob, so reclaiming would be pointless churn.
function reclaimRetiredBlobs(db, { limit = 50 } = {}) {
  const ids = findReclaimableSessions(db, limit);
  let reclaimed = 0;
  const upd = db.prepare("UPDATE session_conversations SET messages = '[]' WHERE ctm_session_id = ? AND messages != '[]'");
  for (const id of ids) {
    try { if (upd.run(id).changes > 0) reclaimed++; }
    catch (e) { console.error('[blob-reclaim] session', String(id).slice(0, 8), 'failed:', e.message); }
  }
  return { swept: ids.length, reclaimed, remaining: findReclaimableSessions(db, 1).length };
}

// One bounded sweep. Safe to call repeatedly (idempotent); converges as sessions complete. When blob
// retirement is active, the same sweep also reclaims disk from complete-rows sessions (one off-thread
// pass does both — no extra scheduler job).
function runContentRowsBackfillSweep(db, replaceSessionMessageRows, { limit = 50 } = {}) {
  const ids = findUnbackfilledSessions(db, limit);
  let migrated = 0, rows = 0;
  for (const id of ids) {
    try {
      const n = backfillSessionContentRows(db, id, replaceSessionMessageRows);
      if (n > 0) { migrated++; rows += n; }
    } catch (e) {
      console.error('[content-backfill] session', String(id).slice(0, 8), 'failed:', e.message);
    }
  }
  let reclaimed = 0;
  if (process.env.CTM_DUAL_WRITE_BLOB === '0') {
    try { reclaimed = reclaimRetiredBlobs(db, { limit }).reclaimed; }
    catch (e) { console.error('[blob-reclaim] sweep failed:', e.message); }
  }
  let droppedLegacy = null;
  if (process.env.CTM_RETIRE_LEGACY_STORES === '1') {
    try { const r = retireLegacyStores(db); if (r.dropped.length) droppedLegacy = r.dropped; }
    catch (e) { console.error('[retire-legacy] sweep failed:', e.message); }
  }
  return { swept: ids.length, migrated, rows, reclaimed, droppedLegacy, remaining: findUnbackfilledSessions(db, 1).length };
}

// Drop the fully-retired legacy message stores once migration is COMPLETE (every eligible session
// has rows, so the search/freshness/reference readers never need the legacy fallback). The readers
// are independently guarded against a missing table, so a drop mid-process can't blank results.
// Gated by the caller on CTM_RETIRE_LEGACY_STORES=1. Reversible: set CTM_DUAL_WRITE_SESSION_MESSAGES=1
// + re-run the legacy backfill, or re-import from the JSONL.
function retireLegacyStores(db) {
  const status = getSessionRowsStatus(db);
  if (!status.complete) return { dropped: [], reason: 'migration_incomplete', remaining: status.remaining };
  const dropped = [];
  for (const t of ['session_messages_fts', 'session_messages']) {
    try {
      if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(t)) {
        db.exec(`DROP TABLE IF EXISTS ${t}`);
        dropped.push(t);
      }
    } catch (e) { console.error('[retire-legacy] drop', t, 'failed:', e.message); }
  }
  return { dropped };
}

// Migration completeness (for /api/ctm/session-rows-status + the cutover gate).
function getSessionRowsStatus(db) {
  const total = Number(db.prepare(
    "SELECT COUNT(*) AS c FROM session_conversations WHERE extracted_source_len > 0 AND messages != '[]' AND length(messages) > 2"
  ).get().c);
  const remaining = findUnbackfilledSessions(db, 100000).length;
  const reclaimable = findReclaimableSessions(db, 100000).length;
  return { total, migrated: total - remaining, remaining, complete: remaining === 0, reclaimable };
}

module.exports = {
  findUnbackfilledSessions,
  backfillSessionContentRows,
  runContentRowsBackfillSweep,
  findReclaimableSessions,
  reclaimRetiredBlobs,
  retireLegacyStores,
  getSessionRowsStatus,
};
