'use strict';

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 DEFAULT_WORKER_WRITE_LOCK_TIMEOUT_MS = 5000;
// Circuit breaker: after this many consecutive request timeouts the worker is
// treated as "degraded" (busy/wedged, not merely a one-off slow op), and
// best-effort ops fail fast WITHOUT being enqueued — so a slow worker stops
// accumulating a backlog of stale work that re-holds the SQLite write lock.
const DEFAULT_DEGRADE_AFTER_TIMEOUTS = 3;
// While degraded, best-effort ops are skipped for this long; after it elapses one
// is allowed through (half-open probe) — it resets the breaker if it succeeds, or
// re-arms the cooldown if it times out again.
const DEFAULT_DEGRADE_COOLDOWN_MS = 5000;

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

class CtmDbOwnerWorkerClient {
  constructor(options = {}) {
    if (!options.dbPath) throw new Error('CtmDbOwnerWorkerClient requires dbPath');
    this.dbPath = options.dbPath;
    this.workerPath = options.workerPath || path.resolve(__dirname, '..', 'workers', 'db-owner-worker.js');
    this.initDb = !!options.initDb;
    this.requestTimeoutMs = _durationMs(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
    this.closeTimeoutMs = _durationMs(options.closeTimeoutMs, DEFAULT_CLOSE_TIMEOUT_MS);
    this.workerWriteLockTimeoutMs = _durationMs(
      options.workerWriteLockTimeoutMs ??
        process.env.CTM_DB_OWNER_WORKER_WRITE_LOCK_TIMEOUT_MS,
      DEFAULT_WORKER_WRITE_LOCK_TIMEOUT_MS
    );
    this.logger = options.logger || console;
    this._degradeAfter = Math.max(1, _durationMs(
      options.degradeAfterTimeouts ?? process.env.CTM_DB_OWNER_WORKER_DEGRADE_AFTER,
      DEFAULT_DEGRADE_AFTER_TIMEOUTS
    ));
    this._degradeCooldownMs = _durationMs(
      options.degradeCooldownMs ?? process.env.CTM_DB_OWNER_WORKER_DEGRADE_COOLDOWN_MS,
      DEFAULT_DEGRADE_COOLDOWN_MS
    );

    this._worker = null;
    this._startPromise = null;
    this._ready = false;
    this._closed = false;
    this._nextRequestId = 1;
    this._pending = new Map();
    // Circuit-breaker state. _consecutiveTimeouts resets on any in-time success.
    // _degradedUntil is the timestamp through which best-effort ops are skipped.
    this._consecutiveTimeouts = 0;
    this._degradedUntil = 0;
    this._status = {
      ready: false,
      pendingClientRequests: 0,
      workerPending: 0,
      workerActive: null,
      completed: 0,
      failed: 0,
    };
  }

  start() {
    if (this._closed) return Promise.reject(new Error('DB owner worker client is closed'));
    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, {
        workerData: {
          dbPath: this.dbPath,
          initDb: this.initDb,
        },
        env: {
          ...process.env,
          CTM_DB_OWNER_WORKER: '1',
          CTM_SQLITE_WRITE_LOCK_TIMEOUT_MS: String(this.workerWriteLockTimeoutMs),
        },
      });
      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 || 'DB owner worker failed to start')));
      };

      worker.on('message', (msg) => {
        if (!msg || typeof msg !== 'object') return;
        if (msg.type === 'ready') {
          this._ready = true;
          this._status.ready = true;
          this._status.workerPending = 0;
          this._status.workerActive = null;
          resolve(this);
          return;
        }
        if (msg.type === 'status') {
          this._status = {
            ...this._status,
            ...msg.status,
            ready: this._ready,
            pendingClientRequests: this._pending.size,
          };
          return;
        }
        if (msg.type === 'response') {
          this._finishRequest(msg.requestId, msg.error, msg.result);
          return;
        }
        if (msg.type === 'closed') {
          this._ready = false;
          this._closed = true;
          this._status.ready = false;
          this._rejectAllPending(new Error('DB owner worker closed'));
          return;
        }
        if (msg.type === 'fatal') {
          const err = new Error(msg.error || 'DB owner worker fatal error');
          err.code = msg.code || 'CTM_DB_OWNER_WORKER_FATAL';
          if (!this._ready) failStart(err);
          this._rejectAllPending(err);
          return;
        }
        if (msg.type === 'log' && this.logger && typeof this.logger[msg.level || 'log'] === 'function') {
          this.logger[msg.level || 'log'](`[ctm-db-worker] ${msg.message || ''}`);
        }
      });

      worker.on('error', (err) => {
        if (!this._ready) failStart(err);
        this._rejectAllPending(err);
      });
      worker.on('exit', (code) => {
        const wasReady = this._ready;
        this._ready = false;
        this._worker = null;
        this._startPromise = null;
        this._status.ready = false;
        if (!this._closed && code !== 0) {
          const err = new Error(`DB owner worker exited with code ${code}`);
          err.code = 'CTM_DB_OWNER_WORKER_EXITED';
          if (!wasReady) failStart(err);
          this._rejectAllPending(err);
        }
      });
    });

    return this._startPromise;
  }

  isDegraded() {
    return Date.now() < this._degradedUntil;
  }

  _recordTimeout(op) {
    this._consecutiveTimeouts += 1;
    if (this._consecutiveTimeouts >= this._degradeAfter) {
      const wasDegraded = this.isDegraded();
      this._degradedUntil = Date.now() + this._degradeCooldownMs;
      if (!wasDegraded && this.logger && typeof this.logger.warn === 'function') {
        this.logger.warn(
          `[ctm-db-worker] degraded after ${this._consecutiveTimeouts} consecutive timeouts ` +
          `(last op: ${op}); best-effort ops will be skipped for ${this._degradeCooldownMs}ms`
        );
      }
    }
  }

  _recordSuccess() {
    if (this._consecutiveTimeouts === 0 && this._degradedUntil === 0) return;
    const wasDegraded = this.isDegraded();
    this._consecutiveTimeouts = 0;
    this._degradedUntil = 0;
    if (wasDegraded && this.logger && typeof this.logger.log === 'function') {
      this.logger.log('[ctm-db-worker] recovered; resuming best-effort ops');
    }
  }

  async request(op, payload = {}, options = {}) {
    if (!op) throw new Error('DB owner worker request requires op');
    // Circuit breaker: when the worker is degraded, fail best-effort ops fast
    // WITHOUT enqueuing — a wedged worker must not keep accumulating stale
    // best-effort work (e.g. scrollback) that re-holds the write lock later.
    // After the cooldown one best-effort op is allowed through as a half-open probe.
    if (options.bestEffort && this.isDegraded()) {
      const err = new Error(`DB owner worker degraded; skipped best-effort op: ${op}`);
      err.code = 'CTM_DB_OWNER_WORKER_DEGRADED';
      throw err;
    }
    await this.start();
    if (!this._worker) throw new Error('DB owner worker is not running');
    const requestId = this._nextRequestId++;
    const timeoutMs = _durationMs(options.timeoutMs, this.requestTimeoutMs);
    return await new Promise((resolve, reject) => {
      let timer = null;
      if (timeoutMs > 0) {
        timer = setTimeout(() => {
          this._pending.delete(requestId);
          this._recordTimeout(op);
          reject(new Error(`DB owner worker request timed out: ${op}`));
        }, timeoutMs);
        if (typeof timer.unref === 'function') timer.unref();
      }
      this._pending.set(requestId, { op, resolve, reject, timer });
      this._status.pendingClientRequests = this._pending.size;
      try {
        this._worker.postMessage({ type: 'request', requestId, op, payload });
      } catch (err) {
        this._finishRequest(requestId, err);
      }
    });
  }

  async drain(options = {}) {
    return await this.request('drain', {}, options);
  }

  getStatus() {
    return {
      ...this._status,
      ready: this._ready,
      closed: this._closed,
      pendingClientRequests: this._pending.size,
      degraded: this.isDegraded(),
      consecutiveTimeouts: this._consecutiveTimeouts,
      dbPath: this.dbPath,
    };
  }

  async close(options = {}) {
    if (this._closed) return { ok: true, alreadyClosed: true };
    const worker = this._worker;
    this._closed = true;
    if (!worker) {
      this._rejectAllPending(new Error('DB owner worker closed'));
      return { ok: true, alreadyClosed: true };
    }
    const timeoutMs = _durationMs(options.timeoutMs, this.closeTimeoutMs);
    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._rejectAllPending(new Error('DB owner worker closed'));
    return { ok: true };
  }

  _finishRequest(requestId, error, result) {
    const pending = this._pending.get(requestId);
    if (!pending) return;
    this._pending.delete(requestId);
    this._status.pendingClientRequests = this._pending.size;
    if (pending.timer) clearTimeout(pending.timer);
    if (error) {
      const err = error instanceof Error ? error : new Error(String(error.message || error || 'DB owner worker request failed'));
      if (error.code) err.code = error.code;
      pending.reject(err);
    } else {
      // An in-time reply proves the worker is draining — clear the breaker.
      this._recordSuccess();
      pending.resolve(result);
    }
  }

  _rejectAllPending(err) {
    const pending = Array.from(this._pending.values());
    this._pending.clear();
    this._status.pendingClientRequests = 0;
    for (const req of pending) {
      if (req.timer) clearTimeout(req.timer);
      req.reject(err);
    }
  }
}

function createDbOwnerWorkerClient(options = {}) {
  return new CtmDbOwnerWorkerClient(options);
}

module.exports = {
  CtmDbOwnerWorkerClient,
  createDbOwnerWorkerClient,
};
