'use strict';

const DEFAULT_MAX_DEPTH = 1000;
const DEFAULT_DRAIN_TIMEOUT_MS = 5000;

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

function _queueError(code, message, extra = {}) {
  const err = new Error(message);
  err.code = code;
  for (const [key, value] of Object.entries(extra)) err[key] = value;
  return err;
}

class SqliteOwnerWriteQueue {
  constructor(options = {}) {
    this.name = options.name || options.label || 'sqlite-owner-write-queue';
    this.maxDepth = Math.max(1, Math.trunc(Number(options.maxDepth) || DEFAULT_MAX_DEPTH));
    this._queue = [];
    this._running = false;
    this._active = null;
    this._accepting = true;
    this._closed = false;
    this._enqueued = 0;
    this._completed = 0;
    this._failed = 0;
    this._idleWaiters = [];
  }

  enqueue(fn, options = {}) {
    if (typeof fn !== 'function') {
      throw new TypeError('SqliteOwnerWriteQueue.enqueue requires a function');
    }
    if (!this._accepting || this._closed) {
      return Promise.reject(_queueError(
        'SQLITE_OWNER_WRITE_QUEUE_CLOSED',
        `${this.name} is closed`
      ));
    }
    if (this._queue.length >= this.maxDepth) {
      return Promise.reject(_queueError(
        'SQLITE_OWNER_WRITE_QUEUE_FULL',
        `${this.name} is full`,
        { pending: this._queue.length, maxDepth: this.maxDepth }
      ));
    }

    const signal = options.signal || null;
    if (signal?.aborted) {
      return Promise.reject(_queueError(
        'SQLITE_OWNER_WRITE_QUEUE_ABORTED',
        `${this.name} task aborted before enqueue`
      ));
    }

    const label = options.label || 'write';
    let task;
    const promise = new Promise((resolve, reject) => {
      task = {
        fn,
        label,
        resolve,
        reject,
        enqueuedAt: Date.now(),
        started: false,
        signal,
        abortListener: null,
      };
    });

    if (signal) {
      task.abortListener = () => {
        if (task.started) return;
        const idx = this._queue.indexOf(task);
        if (idx >= 0) this._queue.splice(idx, 1);
        task.reject(_queueError(
          'SQLITE_OWNER_WRITE_QUEUE_ABORTED',
          `${this.name} task aborted while queued`,
          { label }
        ));
        this._notifyIdleIfNeeded();
      };
      signal.addEventListener('abort', task.abortListener, { once: true });
    }

    this._enqueued += 1;
    this._queue.push(task);
    this._pump();
    return promise;
  }

  getStatus() {
    return {
      name: this.name,
      active: this._active,
      running: this._running,
      pending: this._queue.length,
      accepting: this._accepting,
      closed: this._closed,
      maxDepth: this.maxDepth,
      enqueued: this._enqueued,
      completed: this._completed,
      failed: this._failed,
      idle: !this._running && this._queue.length === 0,
    };
  }

  async drain(options = {}) {
    const timeoutMs = _durationMs(options.timeoutMs, DEFAULT_DRAIN_TIMEOUT_MS);
    if (!this._running && this._queue.length === 0) {
      return { ok: true, timedOut: false, ...this.getStatus() };
    }

    if (timeoutMs === 0) {
      await new Promise((resolve) => this._idleWaiters.push(resolve));
      return { ok: true, timedOut: false, ...this.getStatus() };
    }

    let timer = null;
    let idleWaiter = null;
    try {
      return await Promise.race([
        new Promise((resolve) => {
          idleWaiter = () => resolve({
            ok: true,
            timedOut: false,
            ...this.getStatus(),
          });
          this._idleWaiters.push(idleWaiter);
        }),
        new Promise((resolve) => {
          timer = setTimeout(() => resolve({
            ok: false,
            timedOut: true,
            ...this.getStatus(),
          }), timeoutMs);
          if (typeof timer.unref === 'function') timer.unref();
        }),
      ]);
    } finally {
      if (timer) clearTimeout(timer);
      if (idleWaiter) {
        const idx = this._idleWaiters.indexOf(idleWaiter);
        if (idx >= 0) this._idleWaiters.splice(idx, 1);
      }
    }
  }

  async close(options = {}) {
    const drain = !!options.drain;
    this._accepting = false;
    this._closed = true;
    if (!drain) {
      this._rejectPending(_queueError(
        'SQLITE_OWNER_WRITE_QUEUE_CLOSED',
        `${this.name} closed with pending writes rejected`
      ));
      return { ok: true, drained: false, ...this.getStatus() };
    }
    const result = await this.drain({ timeoutMs: options.timeoutMs });
    return { ...result, drained: !result.timedOut };
  }

  _pump() {
    if (this._running) return;
    const task = this._queue.shift();
    if (!task) {
      this._notifyIdleIfNeeded();
      return;
    }

    this._running = true;
    this._active = task.label;
    task.started = true;
    if (task.signal && task.abortListener) {
      task.signal.removeEventListener('abort', task.abortListener);
      task.abortListener = null;
    }

    Promise.resolve()
      .then(() => task.fn())
      .then((value) => {
        this._completed += 1;
        task.resolve(value);
      }, (err) => {
        this._failed += 1;
        task.reject(err);
      })
      .finally(() => {
        this._running = false;
        this._active = null;
        this._pump();
      });
  }

  _rejectPending(err) {
    const pending = this._queue.splice(0);
    for (const task of pending) {
      if (task.signal && task.abortListener) {
        task.signal.removeEventListener('abort', task.abortListener);
        task.abortListener = null;
      }
      task.reject(err);
    }
    this._notifyIdleIfNeeded();
  }

  _notifyIdleIfNeeded() {
    if (this._running || this._queue.length > 0) return;
    const waiters = this._idleWaiters.splice(0);
    for (const resolve of waiters) resolve();
  }
}

function createSqliteOwnerWriteQueue(options = {}) {
  return new SqliteOwnerWriteQueue(options);
}

module.exports = {
  SqliteOwnerWriteQueue,
  createSqliteOwnerWriteQueue,
};
