'use strict';

// Client for the read/parse worker pool (workers/read-pool-worker.js).
//
// A small fixed pool of DB-free workers that absorb the CPU/IO-heavy *read*
// work (JSONL parse, batch fs.stat) that otherwise blocks the main event loop.
// Unlike the serial db-owner worker, this is a POOL: requests dispatch to the
// least-busy worker so concurrent Conversation-tab loads + scan jobs run in
// parallel instead of head-of-line blocking each other.
//
// Each worker processes its own queue serially (one op at a time), which is why
// "least in-flight" is the right load metric. Mirrors the message protocol and
// robustness (timeouts, fatal/exit handling, reject-all) of
// lib/db-owner-worker-client.js.

const os = require('node:os');
const path = require('node:path');
const { Worker } = require('node:worker_threads');

const DEFAULT_REQUEST_TIMEOUT_MS = 30 * 1000;
const DEFAULT_CLOSE_TIMEOUT_MS = 5 * 1000;
const MIN_POOL_SIZE = 3;
// Raised 8→12: the cores-2 default wants 14 on a 16-core box but was capped at 8, needlessly
// throttling heavy-op concurrency (parse / prompt-index / getAllSessionsData all contend). 12
// leaves ~4 cores for the main loop + db-owner worker. Each worker is a thread + a read-only WAL
// connection (WAL allows many readers); smaller boxes still clamp to cores-2. CTM_READ_POOL_SIZE
// overrides up to this cap.
const MAX_POOL_SIZE = 12;

// Ops whose cost scales with transcript/blob size (seconds, not bounded ms) and
// can monopolize a worker during the cold-boot restore storm. They share the pool
// with latency-sensitive bounded reads (getAllSessionsData, listRichWorktrees,
// autoTitleCandidates, statFiles, …). If a heavy op occupies EVERY worker, those
// small reads queue behind a 10–30s parse, hit their request timeout, and fall
// back to the MAIN THREAD — which is exactly the [offthread-read:*] "falling back
// to main" cascade + 28s event-loop freeze seen at boot. We cap concurrent heavy
// ops below the pool size so ≥1 worker is always free for the light reads.
const HEAVY_OPS = new Set([
  'parseSessionFiles',
  'parseConversationBlob',
  'computeUserPromptIndex',
  'loadCursorConversation',
  'sessionIntegrityDetect', // ~60s full-corpus scan on cold Dropbox
  'auditRelink',            // restore-path relink audit: full scan + per-agent cold-JSONL reads
]);
// Keep this many workers out of heavy reach so light reads never head-of-line
// block behind a heavy parse. Clamped so tiny pools still allow ≥1 heavy op.
const RESERVED_LIGHT_WORKERS = 2;

function _durationMs(value, fallback) {
  const n = Number(value);
  return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : fallback;
}

// Core-aware default. A fixed 3 starved the pool during the 20-session restore storm:
// getAllSessionsData + the prompt-index compute + parseSessionFiles all contend on the same
// workers, so the small latency-sensitive reads timed out (5s) and fell back to a main-thread
// build/compute — the early-boot freezes the event-loop budget surfaced. On a 16-core box
// 3 of 16 cores is a needless bottleneck. Scale to cores-2 (capped at MAX) so the boot
// concurrency has room; CTM_READ_POOL_SIZE still overrides.
function _coreAwareDefaultPoolSize() {
  let cores = 0;
  try { cores = (typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length) || 0; } catch {}
  return Math.min(MAX_POOL_SIZE, Math.max(MIN_POOL_SIZE, cores - 2));
}

function _resolvePoolSize(requested) {
  const raw = requested ?? process.env.CTM_READ_POOL_SIZE;
  const n = Number(raw);
  if (Number.isFinite(n) && n >= 1) return Math.min(MAX_POOL_SIZE, Math.trunc(n));
  return _coreAwareDefaultPoolSize();
}

// One worker_thread + its in-flight bookkeeping. Lazily (re)spawned on first use
// and respawned transparently after a crash so a single bad parse can't take the
// pool down permanently.
class _PoolWorker {
  constructor(opts) {
    this.workerPath = opts.workerPath;
    this.logger = opts.logger || console;
    this.worker = null;
    this.ready = false;
    this.startPromise = null;
    this.pending = new Map(); // requestId -> { resolve, reject, timer }
    this.nextRequestId = 1;
    this.heavyInFlight = 0; // heavy ops currently running on THIS worker
  }

  get inFlight() { return this.pending.size; }

  start() {
    if (this.ready) return Promise.resolve(this);
    if (this.startPromise) return this.startPromise;
    this.startPromise = new Promise((resolve, reject) => {
      const worker = new Worker(this.workerPath, { env: { ...process.env } });
      this.worker = worker;

      const failStart = (err) => {
        if (this.ready) return;
        this.startPromise = null;
        this.worker = null;
        try { worker.terminate(); } catch {}
        reject(err instanceof Error ? err : new Error(String(err || 'read-pool worker failed to start')));
      };

      worker.on('message', (msg) => {
        if (!msg || typeof msg !== 'object') return;
        if (msg.type === 'ready') { this.ready = true; resolve(this); return; }
        if (msg.type === 'response') { this._finish(msg.requestId, msg.error, msg.result); return; }
        if (msg.type === 'fatal') {
          const err = new Error(msg.message || 'read-pool worker fatal error');
          if (!this.ready) failStart(err);
          this._rejectAll(err);
          return;
        }
        // 'status' / 'closed' are advisory for the pool — nothing to track.
      });
      worker.on('error', (err) => {
        if (!this.ready) failStart(err);
        this._rejectAll(err);
      });
      worker.on('exit', (code) => {
        const wasReady = this.ready;
        this.ready = false;
        this.worker = null;
        this.startPromise = null;
        const err = new Error(`read-pool worker exited with code ${code}`);
        if (!wasReady) failStart(err);
        // Reject in-flight so callers fail fast and fall back to the main thread;
        // a fresh worker is spawned on the next request.
        if (code !== 0) this._rejectAll(err);
        else this._rejectAll(new Error('read-pool worker closed'));
      });
    });
    return this.startPromise;
  }

  async request(op, payload, timeoutMs) {
    await this.start();
    if (!this.worker) throw new Error('read-pool worker is not running');
    const requestId = this.nextRequestId++;
    return await new Promise((resolve, reject) => {
      let timer = null;
      if (timeoutMs > 0) {
        timer = setTimeout(() => {
          this.pending.delete(requestId);
          reject(new Error(`read-pool request timed out: ${op}`));
        }, timeoutMs);
        if (typeof timer.unref === 'function') timer.unref();
      }
      this.pending.set(requestId, { resolve, reject, timer });
      try {
        this.worker.postMessage({ type: 'request', requestId, op, payload });
      } catch (err) {
        this._finish(requestId, err);
      }
    });
  }

  _finish(requestId, error, result) {
    const p = this.pending.get(requestId);
    if (!p) return;
    this.pending.delete(requestId);
    if (p.timer) clearTimeout(p.timer);
    if (error) {
      const err = error instanceof Error ? error : new Error(String(error.message || error || 'read-pool request failed'));
      if (error.code) err.code = error.code;
      p.reject(err);
    } else {
      p.resolve(result);
    }
  }

  _rejectAll(err) {
    const all = Array.from(this.pending.values());
    this.pending.clear();
    for (const p of all) { if (p.timer) clearTimeout(p.timer); p.reject(err); }
  }

  async close(timeoutMs) {
    const worker = this.worker;
    if (!worker) { this._rejectAll(new Error('read-pool worker closed')); return; }
    await new Promise((resolve) => {
      let done = false;
      const finish = () => { if (done) return; done = true; resolve(); };
      let timer = null;
      if (timeoutMs > 0) {
        timer = setTimeout(() => { worker.terminate().finally(finish); }, timeoutMs);
        if (typeof timer.unref === 'function') timer.unref();
      }
      const onMessage = (msg) => {
        if (msg?.type !== 'closed') return;
        if (timer) clearTimeout(timer);
        worker.off('message', onMessage);
        finish();
      };
      worker.on('message', onMessage);
      try { worker.postMessage({ type: 'close' }); }
      catch { if (timer) clearTimeout(timer); worker.off('message', onMessage); worker.terminate().finally(finish); }
    });
    this.ready = false;
    this.worker = null;
    this._rejectAll(new Error('read-pool worker closed'));
  }
}

class CtmReadPoolClient {
  constructor(options = {}) {
    this.workerPath = options.workerPath || path.resolve(__dirname, '..', 'workers', 'read-pool-worker.js');
    this.size = _resolvePoolSize(options.size);
    this.requestTimeoutMs = _durationMs(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
    this.closeTimeoutMs = _durationMs(options.closeTimeoutMs, DEFAULT_CLOSE_TIMEOUT_MS);
    this.logger = options.logger || console;
    this._closed = false;
    this._workers = [];
    for (let i = 0; i < this.size; i++) {
      this._workers.push(new _PoolWorker({ workerPath: this.workerPath, logger: this.logger }));
    }
    // Heavy-op admission control: reserve a couple of workers for latency-sensitive
    // light reads so a heavy parse can't occupy the whole pool (see HEAVY_OPS note).
    this._maxHeavy = Math.max(1, this.size - Math.min(RESERVED_LIGHT_WORKERS, this.size - 1));
    this._heavyInFlight = 0;        // heavy ops running across the whole pool
    this._heavyWaiters = [];        // FIFO of resolvers waiting for a heavy slot
  }

  _acquireHeavySlot() {
    if (this._heavyInFlight < this._maxHeavy) {
      this._heavyInFlight++;
      return Promise.resolve();
    }
    return new Promise((resolve) => this._heavyWaiters.push(resolve));
  }

  _releaseHeavySlot() {
    const next = this._heavyWaiters.shift();
    // Hand the slot directly to the next waiter (count is conserved); otherwise free it.
    if (next) next();
    else this._heavyInFlight = Math.max(0, this._heavyInFlight - 1);
  }

  async start() {
    if (this._closed) throw new Error('read pool is closed');
    await Promise.all(this._workers.map((w) => w.start()));
    return this;
  }

  _leastBusy() {
    let best = this._workers[0];
    for (const w of this._workers) {
      if (w.inFlight < best.inFlight) best = w;
    }
    return best;
  }

  // Pick a worker for a request. Light reads prefer a worker with NO heavy op in
  // flight (the reserved capacity), so they never wait behind a multi-second parse;
  // among eligible workers, least-in-flight wins. Heavy ops just take least-busy.
  _pickWorker(heavy) {
    if (!heavy) {
      let best = null;
      for (const w of this._workers) {
        if (w.heavyInFlight > 0) continue;
        if (!best || w.inFlight < best.inFlight) best = w;
      }
      if (best) return best;
    }
    return this._leastBusy();
  }

  async request(op, payload = {}, options = {}) {
    if (this._closed) throw new Error('read pool is closed');
    if (!op) throw new Error('read pool request requires op');
    const timeoutMs = _durationMs(options.timeoutMs, this.requestTimeoutMs);
    const heavy = options.heavy ?? HEAVY_OPS.has(op);
    if (!heavy) return await this._pickWorker(false).request(op, payload, timeoutMs);

    // Heavy op: wait for an admission slot, then dispatch to the least-busy worker.
    await this._acquireHeavySlot();
    const worker = this._pickWorker(true);
    worker.heavyInFlight++;
    try {
      return await worker.request(op, payload, timeoutMs);
    } finally {
      worker.heavyInFlight = Math.max(0, worker.heavyInFlight - 1);
      this._releaseHeavySlot();
    }
  }

  getStatus() {
    return {
      size: this.size,
      closed: this._closed,
      maxHeavy: this._maxHeavy,
      heavyInFlight: this._heavyInFlight,
      heavyWaiting: this._heavyWaiters.length,
      workers: this._workers.map((w) => ({ ready: w.ready, inFlight: w.inFlight, heavyInFlight: w.heavyInFlight })),
      inFlight: this._workers.reduce((s, w) => s + w.inFlight, 0),
    };
  }

  async close() {
    if (this._closed) return { ok: true, alreadyClosed: true };
    this._closed = true;
    await Promise.all(this._workers.map((w) => w.close(this.closeTimeoutMs).catch(() => {})));
    return { ok: true };
  }
}

function createReadPoolClient(options = {}) {
  return new CtmReadPoolClient(options);
}

module.exports = { CtmReadPoolClient, createReadPoolClient };
