'use strict';

const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { threadId } = require('node:worker_threads');

const WRITE_SQL_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|VACUUM|REINDEX|ANALYZE|ATTACH|DETACH|BEGIN|COMMIT|ROLLBACK)\b/i;
const WRITE_PRAGMA_RE = /^\s*(?:journal_mode|wal_checkpoint|user_version|foreign_keys|synchronous|locking_mode|auto_vacuum|optimize)\b/i;
const DEFAULT_TIMEOUT_MS = 0;
const DEFAULT_STALE_MS = 10 * 60 * 1000;
const DEFAULT_POLL_MS = 25;
const DEFAULT_CLAIM_STALE_MS = 1000;
const DEFAULT_STALE_RECOVERY_TIMEOUT_MS = 5000;
const GLOBAL_STATE_KEY = Symbol.for('ctm.sqliteWriteLockState');

const globalState = globalThis[GLOBAL_STATE_KEY] || (globalThis[GLOBAL_STATE_KEY] = {
  token: `${process.pid}:${threadId}:${crypto.randomBytes(8).toString('hex')}`,
  locks: new Map(),
});

function _sleepSync(ms) {
  const buffer = new SharedArrayBuffer(4);
  Atomics.wait(new Int32Array(buffer), 0, 0, ms);
}

function _pidAlive(pid) {
  const n = Number(pid);
  if (!Number.isFinite(n) || n <= 0) return false;
  try {
    process.kill(n, 0);
    return true;
  } catch {
    return false;
  }
}

function _readLock(lockPath) {
  try {
    return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
  } catch {
    return null;
  }
}

function _inspectLock(lockPath) {
  let stat = null;
  try {
    stat = fs.statSync(lockPath);
  } catch (error) {
    if (error?.code === 'ENOENT') return { exists: false, stat: null, raw: '', info: null, parseError: null };
    return { exists: true, stat: null, raw: '', info: null, parseError: error };
  }

  let raw = '';
  try {
    raw = fs.readFileSync(lockPath, 'utf8');
    try {
      return { exists: true, stat, raw, info: JSON.parse(raw), parseError: null };
    } catch (parseError) {
      return { exists: true, stat, raw, info: null, parseError };
    }
  } catch (error) {
    if (error?.code === 'ENOENT') return { exists: false, stat: null, raw: '', info: null, parseError: null };
    return { exists: true, stat, raw, info: null, parseError: error };
  }
}

// Canonical path is stable for the process lifetime (the parent directory's symlink
// chain doesn't change), so cache it. _acquire runs this on every write — without the
// cache it pays a realpathSync.native syscall per write, which is pure overhead on the
// event loop (and worse when the lock file lives on a sync'd filesystem like Dropbox).
const _canonicalPathCache = new Map();
function _canonicalLockPath(lockPath) {
  const resolved = path.resolve(lockPath);
  const cached = _canonicalPathCache.get(resolved);
  if (cached !== undefined) return cached;
  let result;
  try {
    result = fs.realpathSync.native(resolved);
  } catch {
    try {
      const dir = fs.realpathSync.native(path.dirname(resolved));
      result = path.join(dir, path.basename(resolved));
    } catch {
      result = resolved;
    }
  }
  _canonicalPathCache.set(resolved, result);
  return result;
}

function _releaseLockFile(lockPath, token) {
  try {
    const info = _readLock(lockPath);
    if (info?.token === token) fs.unlinkSync(lockPath);
  } catch {}
}

function _lockAgeMs(inspected, now = Date.now()) {
  const createdAt = Number(inspected?.info?.created_at_ms || 0);
  const basis = createdAt || Number(inspected?.stat?.mtimeMs || 0);
  if (!basis) return Number.POSITIVE_INFINITY;
  return Math.max(0, now - basis);
}

function _lockUnchanged(a, b) {
  if (!a?.exists || !b?.exists) return false;
  if (a.info?.token || b.info?.token) return a.info?.token && a.info.token === b.info?.token;
  if (!a.stat || !b.stat) return false;
  if (a.raw || b.raw) return a.raw === b.raw && a.stat.size === b.stat.size && Math.abs(a.stat.mtimeMs - b.stat.mtimeMs) < 1;
  return a.stat.dev === b.stat.dev &&
    a.stat.ino === b.stat.ino &&
    a.stat.size === b.stat.size &&
    Math.abs(a.stat.mtimeMs - b.stat.mtimeMs) < 1 &&
    Math.abs(a.stat.ctimeMs - b.stat.ctimeMs) < 1;
}

function _unlinkIfUnchanged(lockPath, observed) {
  const current = _inspectLock(lockPath);
  if (!current.exists) return true;
  if (!_lockUnchanged(observed, current)) return false;
  try {
    fs.unlinkSync(lockPath);
    return true;
  } catch (error) {
    return error?.code === 'ENOENT';
  }
}

function _staleReason(inspected, staleMs, claimStaleMs) {
  if (!inspected?.exists) return 'missing';
  const ageMs = _lockAgeMs(inspected);
  if (!inspected.info) {
    return ageMs > claimStaleMs ? 'orphaned-claim' : '';
  }
  if (ageMs > staleMs) return 'expired';
  return _pidAlive(inspected.info?.pid) ? '' : 'dead-owner';
}

function _writeLockClaim(lockPath, payload) {
  const dir = path.dirname(lockPath);
  const base = path.basename(lockPath);
  const tempPath = path.join(dir, `.${base}.${process.pid}.${threadId}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`);
  const text = JSON.stringify(payload);
  try {
    fs.writeFileSync(tempPath, text, { flag: 'wx', mode: 0o600 });
    fs.linkSync(tempPath, lockPath);
  } catch (error) {
    if (error?.code !== 'EEXIST' && ['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV'].includes(error?.code)) {
      let fd = null;
      try {
        fd = fs.openSync(lockPath, 'wx', 0o600);
        fs.writeFileSync(fd, text);
        return;
      } finally {
        try { if (fd !== null) fs.closeSync(fd); } catch {}
      }
    }
    throw error;
  } finally {
    try { fs.unlinkSync(tempPath); } catch {}
  }
}

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

function _lockBusyError(lockPath, info) {
  const owner = info?.pid ? ` pid=${info.pid}` : '';
  const label = info?.label ? ` label=${info.label}` : '';
  const err = new Error(`SQLite write lock busy${owner}${label}: ${lockPath}`);
  err.code = 'SQLITE_WRITE_LOCK_BUSY';
  err.lockPath = lockPath;
  err.lockInfo = info || null;
  return err;
}

function _stripLeadingSqlComments(sql) {
  let text = String(sql || '');
  while (true) {
    const next = text.replace(/^\s+/, '');
    if (next.startsWith('--')) {
      const idx = next.indexOf('\n');
      text = idx === -1 ? '' : next.slice(idx + 1);
      continue;
    }
    if (next.startsWith('/*')) {
      const idx = next.indexOf('*/');
      text = idx === -1 ? '' : next.slice(idx + 2);
      continue;
    }
    return next;
  }
}

function _acquire(lockPath, opts = {}) {
  const timeoutMs = _durationMs(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 0);
  const staleMs = _durationMs(opts.staleMs ?? DEFAULT_STALE_MS, DEFAULT_STALE_MS, 1000);
  // Floor of 1ms (not 5) so a background writer can poll tightly and reliably win
  // the lock in the brief window after the main thread releases — the main thread
  // can never wait, so the worker winning fast is how we avoid starving it.
  const pollMs = _durationMs(opts.pollMs ?? DEFAULT_POLL_MS, DEFAULT_POLL_MS, 1);
  const claimStaleMs = _durationMs(opts.claimStaleMs ?? DEFAULT_CLAIM_STALE_MS, DEFAULT_CLAIM_STALE_MS, 1);
  const staleRecoveryTimeoutMs = _durationMs(
    opts.staleRecoveryTimeoutMs ?? DEFAULT_STALE_RECOVERY_TIMEOUT_MS,
    DEFAULT_STALE_RECOVERY_TIMEOUT_MS,
    0
  );
  const startedAt = Date.now();
  const deadline = startedAt + timeoutMs;
  const dir = path.dirname(lockPath);
  const canonical = _canonicalLockPath(lockPath);
  let staleRecoveryDeadline = 0;
  let contended = false;
  const held = globalState.locks.get(canonical);
  if (held) {
    held.depth += 1;
    return {
      waitedMs: 0,
      contended: false,
      release: () => {
        held.depth -= 1;
        if (held.depth <= 0) {
          globalState.locks.delete(canonical);
          _releaseLockFile(held.lockPath, held.token);
        }
      },
    };
  }

  try { fs.mkdirSync(dir, { recursive: true }); } catch {}

  while (true) {
    try {
      const token = globalState.token;
      _writeLockClaim(lockPath, {
        pid: process.pid,
        thread_id: threadId,
        token,
        created_at_ms: Date.now(),
        label: opts.label || '',
      });
      const entry = { depth: 1, lockPath, token };
      globalState.locks.set(canonical, entry);
      return {
        waitedMs: Date.now() - startedAt,
        contended,
        release: () => {
          entry.depth -= 1;
          if (entry.depth > 0) return;
          globalState.locks.delete(canonical);
          _releaseLockFile(lockPath, token);
        },
      };
    } catch (err) {
      if (err?.code !== 'EEXIST') throw err;
      contended = true;
      const observed = _inspectLock(lockPath);
      const staleReason = _staleReason(observed, staleMs, claimStaleMs);
      if (staleReason) {
        if (_unlinkIfUnchanged(lockPath, observed)) continue;
        if (!staleRecoveryDeadline) staleRecoveryDeadline = Date.now() + staleRecoveryTimeoutMs;
        if (Date.now() < staleRecoveryDeadline) {
          _sleepSync(pollMs);
          continue;
        }
      } else if (!observed.info) {
        const waitMs = Math.min(pollMs, Math.max(1, claimStaleMs - _lockAgeMs(observed)));
        _sleepSync(waitMs);
        continue;
      } else {
        staleRecoveryDeadline = 0;
      }
      if (Date.now() >= deadline) {
        throw _lockBusyError(lockPath, observed.info);
      }
      _sleepSync(Math.min(pollMs, Math.max(1, deadline - Date.now())));
    }
  }
}

function createSqliteWriteLock(lockPath, opts = {}) {
  let depth = 0;
  const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null;
  return function withSqliteWriteLock(fn, label = opts.label || '') {
    // Reentrant within the same connection: already inside a held write lock, so
    // no re-acquire and no event (the outer call owns the timing).
    if (depth > 0) return fn();
    depth += 1;
    let acquired = null;
    const waitStart = onEvent ? Date.now() : 0;
    try {
      try {
        acquired = _acquire(lockPath, { ...opts, label });
      } catch (err) {
        // Surface contention to instrumentation even when the acquire fails fast
        // (e.g. main-thread 0ms timeout) so callers can see busy/starvation.
        if (onEvent) {
          try {
            // Pass the busy error so instrumentation can attribute the lock HOLDER
            // (the error message carries the holder pid). Extra field; existing
            // consumers that ignore it are unaffected.
            onEvent({ label, waitedMs: Date.now() - waitStart, heldMs: 0, contended: true, acquired: false, error: err });
          } catch {}
        }
        throw err;
      }
      const heldStart = onEvent ? Date.now() : 0;
      try {
        return fn();
      } finally {
        try { acquired.release(); } finally {
          if (onEvent) {
            try {
              onEvent({
                label,
                waitedMs: acquired.waitedMs,
                heldMs: Date.now() - heldStart,
                contended: acquired.contended,
                acquired: true,
              });
            } catch {}
          }
        }
      }
    } finally {
      depth -= 1;
    }
  };
}

// Async, bounded retry for SQLITE_WRITE_LOCK_BUSY. Safe because the busy error is
// always thrown while ACQUIRING the lock — before the wrapped write runs — so the
// failed write never partially applied. Used by background (worker) write paths so
// transient contention retries instead of surfacing to the user. Never use this on
// the main thread's hot path with a blocking sleep; the delay here is async.
async function retryOnWriteLockBusy(fn, opts = {}) {
  const retries = Number.isInteger(opts.retries) ? Math.max(0, opts.retries) : 2;
  const backoffMs = Number.isFinite(opts.backoffMs) ? Math.max(1, opts.backoffMs) : 50;
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (err?.code !== 'SQLITE_WRITE_LOCK_BUSY' || attempt >= retries) throw err;
      attempt += 1;
      if (typeof opts.onRetry === 'function') {
        try { opts.onRetry({ attempt, retries, error: err }); } catch {}
      }
      const jitter = Math.floor(Math.random() * backoffMs);
      await new Promise((resolve) => setTimeout(resolve, backoffMs * attempt + jitter));
    }
  }
}

function isLikelyWriteSql(sql) {
  return WRITE_SQL_RE.test(_stripLeadingSqlComments(sql));
}

function isLikelyWritePragma(pragma) {
  const text = _stripLeadingSqlComments(pragma);
  if (/^\s*PRAGMA\b/i.test(text)) return WRITE_PRAGMA_RE.test(text.replace(/^\s*PRAGMA\b/i, ''));
  return WRITE_PRAGMA_RE.test(text);
}

function containsLikelyWriteSql(sql) {
  return String(sql || '')
    .split(';')
    .some((statement) => {
      const text = _stripLeadingSqlComments(statement);
      if (!text) return false;
      return isLikelyWriteSql(text) || isLikelyWritePragma(text);
    });
}

function installSqliteWriteLock(db, dbPath, opts = {}) {
  if (!db || db.__sqliteWriteLockInstalled) return db;
  const lockPath = opts.lockPath || `${dbPath}.write-lock`;
  const withLock = createSqliteWriteLock(lockPath, opts);

  const originalPrepare = db.prepare.bind(db);
  db.prepare = function prepareWithWriteLock(sql, ...args) {
    const stmt = originalPrepare(sql, ...args);
    if (!isLikelyWriteSql(sql) || typeof stmt.run !== 'function') return stmt;
    const originalRun = stmt.run.bind(stmt);
    stmt.run = (...runArgs) => withLock(() => originalRun(...runArgs), 'statement');
    return stmt;
  };

  const originalExec = db.exec.bind(db);
  db.exec = function execWithWriteLock(sql, ...args) {
    if (!containsLikelyWriteSql(sql)) return originalExec(sql, ...args);
    return withLock(() => originalExec(sql, ...args), 'exec');
  };

  const originalPragma = db.pragma.bind(db);
  db.pragma = function pragmaWithWriteLock(pragma, ...args) {
    if (!isLikelyWritePragma(pragma)) return originalPragma(pragma, ...args);
    return withLock(() => originalPragma(pragma, ...args), 'pragma');
  };

  const originalTransaction = db.transaction.bind(db);
  db.transaction = function transactionWithWriteLock(fn) {
    const txn = originalTransaction(fn);
    const wrap = (call, label) => (...args) => withLock(() => call(...args), label);
    const wrapped = wrap(txn, 'transaction');
    for (const mode of ['default', 'deferred', 'immediate', 'exclusive']) {
      if (typeof txn[mode] === 'function') wrapped[mode] = wrap(txn[mode].bind(txn), `transaction:${mode}`);
    }
    return wrapped;
  };

  Object.defineProperty(db, '__sqliteWriteLockInstalled', { value: true });
  Object.defineProperty(db, '__sqliteWriteLockPath', { value: lockPath });
  return db;
}

module.exports = {
  createSqliteWriteLock,
  containsLikelyWriteSql,
  installSqliteWriteLock,
  isLikelyWritePragma,
  isLikelyWriteSql,
  retryOnWriteLockBusy,
};
