'use strict';

// Read/parse pool worker.
//
// One of N identical workers managed by lib/read-pool-client.js. Its job is to
// take the CPU/IO-heavy *read* work off the main event loop: parsing JSONL
// conversation transcripts (often 1–10MB `JSON.parse`) and batch `fs.stat`
// scans. These block the main thread for seconds and stall keystroke echo when
// many sessions are open (see lib/perf-tracker.js + the [perf-lag] log).
//
// Deliberately DB-free: the main thread resolves file paths via fast indexed
// SQLite lookups and passes them in as payload, so a pool worker never opens a
// SQLite connection. That keeps the pool simple (no read-only WAL connection to
// manage) and side-effect-free — it only reads files and returns plain data.
//
// Speaks the same message protocol as workers/db-owner-worker.js (ready /
// request / response / status / close) so it can share the client transport.

const { parentPort } = require('node:worker_threads');
const fs = require('node:fs');

let ready = false;
let closing = false;
let active = null;
let enqueued = 0;
let completed = 0;
let failed = 0;
const queue = [];

// Lazy so the worker boots instantly and a parser require error surfaces per
// request (as a normal op failure) instead of crashing the whole pool at boot.
let parser = null;
function _parser() {
  if (!parser) parser = require('../lib/jsonl-conversation-parser');
  return parser;
}

// Lazy require of the coding-agent model helpers (used by the resolveAgentModels op). DB-free
// except the Codex state DB it opens read-only — fine in a worker thread.
let _codingModels = null;
function _codingModelsMod() {
  if (!_codingModels) _codingModels = require('../lib/coding-agent-models');
  return _codingModels;
}

// Opportunity A follow-up: a READ-ONLY connection used only for the recent-sessions
// rebuild (getAllSessionsData — a ~700ms window-function query). Running it here keeps
// it off the main event loop. Read-only + lazy: opened on first use, cached per dbPath,
// never writes, never changes journal_mode (WAL readers don't need to). This is the one
// DB touch the otherwise DB-free pool makes, scoped to a pure read.
let _roDb = null;
let _roDbPath = null;
function _readonlyDb(dbPath) {
  if (!dbPath) throw new Error('getAllSessionsData requires a dbPath');
  if (_roDb && _roDbPath === dbPath) return _roDb;
  if (_roDb) { try { _roDb.close(); } catch {} _roDb = null; _roDbPath = null; }
  const { loadSqliteDriver } = require('../lib/sqlite-driver');
  const Database = loadSqliteDriver({ source: 'read-pool-worker' });
  const d = new Database(dbPath, { readonly: true, fileMustExist: true });
  d.pragma('busy_timeout = 5000');
  _roDb = d;
  _roDbPath = dbPath;
  return _roDb;
}

function _serializeError(err) {
  return {
    message: err && err.message ? err.message : String(err || 'unknown error'),
    code: err && err.code ? err.code : undefined,
    stack: err && err.stack ? String(err.stack) : undefined,
  };
}

function _status() {
  return { ready, active, running: !!active, pending: queue.length, enqueued, completed, failed };
}

function _postStatus() {
  try { parentPort.postMessage({ type: 'status', status: _status() }); } catch {}
}

async function _runOp(op, payload = {}) {
  switch (op) {
    case 'ping': {
      // Test hook: an optional bounded delay lets tests hold a worker busy to
      // exercise pool concurrency/admission control deterministically.
      const sleepMs = Math.min(5000, Math.max(0, Number(payload.sleepMs) || 0));
      if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
      return { pong: true, ts: Number(payload.ts) || 0 };
    }

    case 'statFiles': {
      // Batch fs.stat off the main thread. apiSessionMessages and the scan jobs
      // sum per-file sizes with synchronous statSync loops on the hot path.
      const paths = Array.isArray(payload.paths) ? payload.paths : [];
      let totalSize = 0;
      const stats = paths.map((p) => {
        try {
          const st = fs.statSync(p);
          totalSize += st.size;
          return { path: p, exists: true, size: st.size, mtimeMs: st.mtimeMs };
        } catch {
          return { path: p, exists: false, size: 0, mtimeMs: 0 };
        }
      });
      return { stats, totalSize };
    }

    case 'resolveAgentModels': {
      // Resolve agent model ids (+ optional Codex titles) off the main loop. The claude path
      // reads/scans the transcript JSONL (statSync + tail read + JSON.parse); the codex path
      // opens the Codex state DB read-only — both blocked the main event loop in
      // syncCodingSessionModels. Returns RAW model names; the main thread maps them to the model
      // registry (cheap, no fs/DB) so the registry cache stays single-sourced on main.
      const mod = _codingModelsMod();
      const homeDir = payload.homeDir || process.env.HOME;
      const items = Array.isArray(payload.items) ? payload.items : [];
      const results = items.map((it) => {
        const out = { id: it.id };
        try {
          if (it.agentType === 'claude') {
            out.modelName = mod.getClaudeSessionModelName({
              jsonlPath: it.claudeJsonlPath,
              homeDir,
              sessionActivityMs: it.sessionActivityMs || 0,
            }) || null;
          } else if (it.agentType === 'codex') {
            out.modelName = it.codexThreadId
              ? mod.getCodexThreadModel(it.codexThreadId, homeDir)
              : mod.getCodexLatestModel(homeDir);
            if (it.wantCodexTitle && it.codexThreadId) {
              out.codexTitle = mod.getCodexThreadTitleById(it.codexThreadId, homeDir) || null;
            }
          }
        } catch (e) {
          out.error = e && e.message ? e.message : String(e);
        }
        return out;
      });
      return { results };
    }

    case 'parseSessionFiles': {
      // Phase 2b consumer: read + parse a session's JSONL file set into the
      // combined, chronologically-sorted message list the Conversation tab
      // renders. The heavy fs.readFileSync + JSON.parse (up to 10MB) runs here,
      // not on the main event loop. Returns the exact shape parseSessionFiles()
      // returns on the main thread — parity is by construction (same function).
      const files = Array.isArray(payload.files) ? payload.files : [];
      return _parser().parseSessionFiles(files, payload.options || {});
    }

    case 'getAllSessionsData': {
      // Opportunity A: run the recent-sessions rebuild query (~700ms) off the main
      // event loop, on this worker's read-only connection. Same logic as
      // db.getAllSessionsData() (shared lib/all-sessions-query). Returns plain rows.
      const { queryAllSessionsData } = require('../lib/all-sessions-query');
      return { rows: queryAllSessionsData(_readonlyDb(payload.dbPath)) };
    }

    case 'listPromptExecutions': {
      // Move the /api/prompt-executions list query OFF the main event loop. It is
      // `SELECT * FROM prompt_executions ... LIMIT ?` where LIMIT can be up to 10000
      // and each row carries large message_text — a CPU profile of the live primary
      // showed a single .all() blocking ~3.9s on the request path. Same SQL as the
      // main-thread handler (shared lib → parity by construction); the heavy read
      // runs here on this worker's read-only connection.
      const { queryPromptExecutions } = require('../lib/prompt-executions-query');
      return { executions: queryPromptExecutions(_readonlyDb(payload.dbPath), payload) };
    }

    case 'listRichWorktrees': {
      // Move the worktree git fan-out OFF the main event loop. listRichWorktrees spawns
      // ~4 git processes PER worktree (rev-list/status/log/unmerged); on a 37-worktree repo
      // that's ~150 fork/execs whose synchronous spawn setup blocked the main loop for
      // seconds every refresh (the dominant `(unknown)` freeze in the CPU profile). Running
      // it here puts the spawn storm on a worker thread. Read-only fan-out only — the
      // side-effecting autoRecover path stays on main.
      const gitw = require('../git-utils');
      const worktrees = await gitw.listRichWorktrees(payload.cwd, payload.opts || {});
      return { worktrees: Array.isArray(worktrees) ? worktrees : [] };
    }

    case 'loadCursorConversation': {
      // Phase 3: run the cursor blob-graph enumeration + reconstruction OFF the main event
      // loop (the cold cache-miss path; steady state is served from session_conversations).
      // Worker-safe: cursor-conversation-store uses only better-sqlite3 (readonly) + fs/path.
      const cs = require('../lib/cursor-conversation-store');
      const store = cs.loadCursorConversationForSession(payload.options || {});
      if (!store || !Array.isArray(store.messages)) return { store: null };
      return {
        store: {
          messages: store.messages,
          agentId: store.agentId || '',
          name: store.name || '',
          workspacePath: store.workspacePath || '',
          lastUsedModel: store.lastUsedModel || '',
          createdAtMs: store.createdAtMs || 0,
          storeDbPath: store.storeDbPath || '',
        },
      };
    }

    case 'parseConversationBlob': {
      // Move the session_conversations cache-blob parse OFF the main event loop. The
      // JSON.parse (0.5–2MB, deeply nested) + codex-synthetic scan + timeline sort is
      // 150–360ms on main per the [freeze-probe]; growing/active sessions re-parse it on
      // every poll. Same function as the main-thread fallback (shared module) → parity by
      // construction. Clone-in (string) + clone-out (array) is ~3ms even at 1.7MB.
      const { parseConversationBlob } = require('../lib/conversation-blob-parser');
      return { messages: parseConversationBlob(payload.messages) };
    }

    case 'computeUserPromptIndex': {
      // Phase 1: run the Queue Builder prompt-index compute (SELECT all user rows + sort
      // + exclusions + sha1-per-prompt summary, ~460ms on a 328-prompt session) off the
      // main event loop. Same logic as the main-thread fallback (shared module). Inputs
      // (sourceIds, exclusion rows, codex-synthetic flag) are resolved on main.
      const { computeUserPromptIndex } = require('../lib/session-prompt-index');
      return computeUserPromptIndex(_readonlyDb(payload.dbPath), {
        sourceIds: payload.sourceIds || [],
        exclusionRows: payload.exclusionRows || [],
        filterCodexSynthetic: payload.filterCodexSynthetic === true,
      });
    }

    case 'autoTitleCandidates': {
      // Run auto-title's candidate SELECT OFF the main event loop. The query
      // (ORDER BY imported_at DESC LIMIT 20, LEFT JOIN ctm_sessions to find
      // untitled ones) full-scans + sorts the session_conversations table whose
      // rows carry large `messages` blobs — synchronously on the main loop it was
      // measured freezing it up to ~25s on cold Dropbox pages every 5 min (the
      // dominant auto-title freeze). Off-thread the cold cost is invisible to the
      // loop. (An imported_at index was measured: it removes the sort but trades
      // sequential scan for random row fetches that page in MORE on cold Dropbox —
      // net slower cold, negligible warm — so we offload instead of indexing.)
      // Read-only WAL connection → side-effect-free; same SQL as the main fallback.
      const conn = _readonlyDb(payload.dbPath);
      const rows = conn.prepare(`
        SELECT sc.ctm_session_id as session_id, sc.first_message, sc.user_msg_count
        FROM session_conversations sc
        LEFT JOIN ctm_sessions cs ON sc.ctm_session_id = cs.id
        WHERE (cs.title IS NULL OR cs.title = '')
          AND sc.user_msg_count >= 2
          AND sc.first_message != ''
        ORDER BY sc.imported_at DESC
        LIMIT 20
      `).all();
      return { rows };
    }

    case 'sessionIntegrityDetect': {
      // Run session-integrity's READ-ONLY detection (the full-corpus mismatch scan +
      // relink audit) OFF the db-owner worker. On cold Dropbox this scan takes ~60s;
      // run on the single serial db-owner worker it MONOPOLIZES it, starving
      // interactive writes onto the main thread where they stall on Dropbox I/O — the
      // 30-min steady-state freeze. detectMismatches and auditRelink are both
      // read-only, so they run safely on this worker's read-only connection. Only the
      // rare recovery WRITES stay on the db-owner worker (applied by main when issues
      // exist). Mirrors the read-only portions of session-integrity.runIntegrityCheck.
      const roDb = _readonlyDb(payload.dbPath);
      const integ = require('../session-integrity');
      const { getAllSessionFiles } = require('../session-utils');
      const issues = integ.detectMismatches(roDb, getAllSessionFiles);
      let crossLinks = [];
      try {
        const { auditRelink } = require('../lib/relink-audit');
        const audit = auditRelink({ db: roDb });
        crossLinks = (audit.tabs || [])
          .filter((t) => t.verdict === 'cross_linked')
          .map((t) => ({
            ctm_session_id: t.ctm_session_id,
            label: t.label,
            startup_target: t.startup_agent_id,
            reason: t.details?.reason || 'cross_linked',
            suggested_fix: t.details?.suggested_fix || null,
          }));
      } catch (e) {
        // Relink audit is best-effort; detection issues are still returned.
      }
      return { issues, crossLinks };
    }

    case 'auditRelink': {
      // Run the relink audit (full agent_sessions + session_conversations scan + a
      // per-agent cold-JSONL header/first-message read) OFF the main event loop. It runs
      // synchronously at the TOP of restore (repairSafeRelinkSwaps) where, on cold Dropbox,
      // the N×M fs reads froze the loop for tens of seconds. auditRelink is fully read-only,
      // so it runs on this worker's read-only connection; the rare repair WRITES stay on the
      // main thread (repairSafeRelinkSwaps consumes this result). Returns the full audit.
      const { auditRelink } = require('../lib/relink-audit');
      return auditRelink({ db: _readonlyDb(payload.dbPath) });
    }

    case 'ctmSessionMemorySearch':
    case 'ctmSessionMemoryContext':
    case 'ctmSessionMemoryContextPack': {
      // Run the Wall-E session-memory reads OFF the main event loop. These power the
      // /api/ctm/session-memory/* endpoint that Wall-E MCP routing (walle_context_hint /
      // lookup_context / context_pack / search_sessions) calls from EVERY active agent
      // session. The work is brutal on the main thread: searchCtmSessions runs an FTS scan
      // PLUS a `content LIKE '%q%'` scan over session_messages (220k+ rows) PLUS a
      // `messages LIKE '%q%'` scan over the session_conversations blob column (tens of MB of
      // JSON) PLUS a full JSON.parse of every matched conversation. A single search was
      // measured blocking the CTM main loop 7.74s. Same shared wall-e module as the
      // main-thread fallback → result parity by construction; the worker's read-only WAL
      // connection makes it side-effect-free (better-sqlite3 readonly + fs only).
      const ctx = require('../../wall-e/memory/ctm-session-context');
      const args = (payload && payload.args) || {};
      const conn = _readonlyDb(payload.dbPath);
      if (op === 'ctmSessionMemorySearch') return ctx.searchCtmSessions({ ...args, db: conn });
      if (op === 'ctmSessionMemoryContext') return ctx.getCtmSessionContext({ ...args, db: conn });
      return ctx.buildContextPack({ ...args, db: conn });
    }

    case 'drain':
      return { ok: true, ..._status() };

    default: {
      const err = new Error(`Unsupported read-pool op: ${op}`);
      err.code = 'CTM_READ_POOL_UNSUPPORTED_OP';
      throw err;
    }
  }
}

function _pump() {
  if (active || closing) return;
  const task = queue.shift();
  if (!task) {
    _postStatus();
    return;
  }
  active = task.op;
  _postStatus();
  Promise.resolve()
    .then(() => _runOp(task.op, task.payload))
    .then((result) => {
      completed += 1;
      parentPort.postMessage({ type: 'response', requestId: task.requestId, result });
    }, (err) => {
      failed += 1;
      parentPort.postMessage({ type: 'response', requestId: task.requestId, error: _serializeError(err) });
    })
    .finally(() => {
      active = null;
      _pump();
    });
}

function _enqueue(msg) {
  enqueued += 1;
  queue.push({ requestId: msg.requestId, op: msg.op, payload: msg.payload || {} });
  _pump();
}

function _close() {
  closing = true;
  queue.length = 0;
  ready = false;
  try { parentPort.postMessage({ type: 'closed' }); } catch {}
  setImmediate(() => {
    try { parentPort.close(); } catch {}
  });
}

parentPort.on('message', (msg) => {
  if (!msg || typeof msg !== 'object') return;
  if (msg.type === 'request') { _enqueue(msg); return; }
  if (msg.type === 'status') { _postStatus(); return; }
  if (msg.type === 'close') {
    if (active) {
      const interval = setInterval(() => {
        if (active) return;
        clearInterval(interval);
        _close();
      }, 10);
      if (typeof interval.unref === 'function') interval.unref();
    } else {
      _close();
    }
  }
});

// No DB to open — signal ready immediately.
ready = true;
try { parentPort.postMessage({ type: 'ready', status: _status() }); } catch {}
