/**
 * LadybugDB connection pool (core). Used by MCP, sync, search, wiki, etc.
 *
 * LadybugDB Adapter (Connection Pool)
 *
 * Manages a pool of LadybugDB databases keyed by repoId, each with
 * multiple Connection objects for safe concurrent query execution.
 *
 * LadybugDB Connections are NOT thread-safe — a single Connection
 * segfaults if concurrent .query() calls hit it simultaneously.
 * This adapter provides a checkout/return connection pool so each
 * concurrent query gets its own Connection from the same Database.
 *
 * @see https://docs.ladybugdb.com/concurrency — multiple Connections
 * from the same Database is the officially supported concurrency pattern.
 */
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import lbug from '@ladybugdb/core';
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
/**
 * Probe whether a Windows FTS extension binary is locally installed under
 * ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
 * version dir whose libfts.lbug_extension exists on disk; false if the
 * extension root is missing or contains no FTS binary.
 *
 * Gates the Windows skip-FTS-load guard below so we only skip the load
 * when no extension binary is present. When at least one binary exists,
 * loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
 * LOAD EXTENSION fts to its version-specific path internally, and the
 * ExtensionManager's tryLoad try/catch handles version-mismatch errors
 * cleanly without ever attempting dlopen of a stale binary. The install
 * path that the #1199/#1217 SIGSEGV documented is never exercised at
 * query time.
 *
 * Exported so unit tests can exercise the probe directly against a
 * temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
 */
export async function hasLocalWinFtsExtension() {
    try {
        const extRoot = path.join(os.homedir(), '.lbdb', 'extension');
        const versions = await fs.readdir(extRoot);
        for (const v of versions) {
            try {
                await fs.stat(path.join(extRoot, v, 'win_amd64', 'fts', 'libfts.lbug_extension'));
                return true;
            }
            catch {
                /* missing for this version, keep looking */
            }
        }
    }
    catch {
        /* no .lbdb/extension dir */
    }
    return false;
}
const pool = new Map();
const poolCloseListeners = new Set();
/**
 * Subscribe to pool-close events. Returns a disposer that removes the
 * listener (handy for tests).
 */
export function addPoolCloseListener(listener) {
    poolCloseListeners.add(listener);
    return () => {
        poolCloseListeners.delete(listener);
    };
}
const dbCache = new Map();
/** Max repos in the pool (LRU eviction) */
const MAX_POOL_SIZE = 5;
/** Idle timeout before closing a repo's connections */
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
/** Max connections per repo (caps concurrent queries per repo) */
const MAX_CONNS_PER_REPO = 8;
let idleTimer = null;
// Stdout-capture state lives in `gitnexus/src/mcp/stdio-capture.ts` — a leaf
// module with zero non-`node:` imports. We re-export the same symbols here
// so the existing test mock seam (`gitnexus/src/mcp/core/lbug-adapter.ts`
// re-exports * from this file, and 8+ test files use that path with
// `vi.mock(...)`) continues to work without churn. The source of truth is
// the leaf module; this re-export is a compatibility shim.
//
// Why the leaf module exists: Codex's adversarial review on PR #1383 found
// that putting this state in pool-adapter.ts pulled `@ladybugdb/core` into
// `cli/mcp.ts`'s static-import closure (via stdio-context → pool-adapter →
// @ladybugdb/core), corrupting stdout in the pre-sentinel window. Routing
// through the leaf breaks that chain.
export { realStdoutWrite, realStderrWrite, setActiveStdoutWrite } from '../../mcp/stdio-capture.js';
import { getActiveStdoutWrite, realStderrWrite } from '../../mcp/stdio-capture.js';
let stdoutSilenceCount = 0;
/** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
let preWarmActive = false;
/**
 * Start the idle cleanup timer (runs every 60s)
 */
function ensureIdleTimer() {
    if (idleTimer)
        return;
    idleTimer = setInterval(() => {
        const now = Date.now();
        for (const [repoId, entry] of pool) {
            if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
                closeOne(repoId);
            }
        }
    }, 60_000);
    if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
        idleTimer.unref();
    }
}
/**
 * Touch a repo to reset its idle timeout.
 * Call this during long-running operations to prevent the connection from being closed.
 */
export const touchRepo = (repoId) => {
    const entry = pool.get(repoId);
    if (entry) {
        entry.lastUsed = Date.now();
    }
};
/**
 * Evict the least-recently-used repo if pool is at capacity
 */
function evictLRU() {
    if (pool.size < MAX_POOL_SIZE)
        return;
    let oldestId = null;
    let oldestTime = Infinity;
    for (const [id, entry] of pool) {
        if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
            oldestTime = entry.lastUsed;
            oldestId = id;
        }
    }
    if (oldestId) {
        closeOne(oldestId);
    }
}
/**
 * Remove a repo from the pool, close its connections, and release its
 * shared Database ref.  Only closes the Database when no other repoIds
 * reference it (refCount === 0).
 */
function closeOne(repoId) {
    const entry = pool.get(repoId);
    if (!entry)
        return;
    entry.closed = true;
    // Close available connections — fire-and-forget with .catch() to prevent
    // unhandled rejections.  Native close() returns Promise<void> but can crash
    // the N-API destructor on macOS/Windows; deferring to process exit lets
    // dangerouslyIgnoreUnhandledErrors absorb the crash.
    for (const conn of entry.available) {
        conn.close().catch(() => { });
    }
    entry.available.length = 0;
    // Checked-out connections can't be closed here — they're in-flight.
    // The checkin() function detects entry.closed and closes them on return.
    // Only close the Database when no other repoIds reference it.
    // External databases (injected via initLbugWithDb) are never closed here —
    // the core adapter owns them and handles their lifecycle.
    const shared = dbCache.get(entry.dbPath);
    if (shared) {
        shared.refCount--;
        if (shared.refCount === 0) {
            if (shared.external) {
                // External databases are owned by the core adapter — don't close
                // or remove from cache.  Keep the entry so future initLbug() calls
                // for the same dbPath reuse it instead of hitting a file lock.
                shared.refCount = 0;
                shared.ftsLoaded = false;
            }
            else {
                shared.db.close().catch(() => { });
                dbCache.delete(entry.dbPath);
            }
        }
    }
    pool.delete(repoId);
    // Notify listeners AFTER the pool entry is gone so any cache-invalidation
    // they perform is consistent with `isLbugReady(repoId) === false`.
    for (const listener of poolCloseListeners) {
        try {
            listener(repoId);
        }
        catch {
            // Isolate listener failures — teardown must complete.
        }
    }
}
/**
 * Create a new Connection from a repo's Database.
 * Silences stdout to prevent native module output from corrupting MCP stdio.
 */
let activeQueryCount = 0;
/**
 * Silence stdout by replacing process.stdout.write with a no-op.
 * Uses a reference counter so nested silence/restore pairs are safe.
 * Exported so other modules (e.g. embedder) use the same mechanism instead
 * of independently patching stdout, which causes restore-order conflicts.
 */
export function silenceStdout() {
    if (stdoutSilenceCount++ === 0) {
        // eslint-disable-next-line no-restricted-syntax -- silencing infrastructure; replacement is a no-op
        process.stdout.write = (() => true);
    }
}
export function restoreStdout() {
    if (--stdoutSilenceCount <= 0) {
        stdoutSilenceCount = 0;
        // eslint-disable-next-line no-restricted-syntax -- restoring the active stdout-write handler is the silencing API contract
        process.stdout.write = getActiveStdoutWrite();
    }
}
// Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
// inside createConnection before restoreStdout runs).
// Exempts active queries and pre-warm — these legitimately hold silence for
// longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
setInterval(() => {
    if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
        stdoutSilenceCount = 0;
        // eslint-disable-next-line no-restricted-syntax -- watchdog recovery for stuck silencing
        process.stdout.write = getActiveStdoutWrite();
    }
}, 1000).unref();
function createConnection(db) {
    silenceStdout();
    try {
        return new lbug.Connection(db);
    }
    finally {
        restoreStdout();
    }
}
/** Query timeout in milliseconds */
const QUERY_TIMEOUT_MS = 30_000;
/** Waiter queue timeout in milliseconds */
const WAITER_TIMEOUT_MS = 15_000;
const LOCK_RETRY_ATTEMPTS = 3;
const LOCK_RETRY_DELAY_MS = 2000;
const SHADOW_REPLAY_PROBE_QUERY = 'MATCH (n) RETURN n LIMIT 1';
const poolSidecarLogger = {
    warn: (message) => {
        realStderrWrite(`${message}\n`);
    },
    debug: (_message) => { },
    info: (message) => {
        realStderrWrite(`${message}\n`);
    },
};
/**
 * Pool-local quarantine guard that tolerates the concurrent-peer race the
 * direct adapter does NOT face (the direct adapter holds `acquireInitLock`,
 * a cross-process file lock, around its quarantine calls — so any ENOENT
 * there is a real bug, not a benign race).
 *
 * On ENOENT from `fs.rename`, re-inspects via `statIfExists` to confirm the
 * WAL really is gone. If gone, returns `{ kind: 'peer-handled' }`. If the
 * WAL is somehow still present after the ENOENT (filesystem race we don't
 * fully model), re-throws as a classified error rather than silently
 * returning success — preserves the lock-invariant principle at the pool
 * sites too.
 *
 * On any non-ENOENT failure, classifies through `renameFailureMessage`:
 * EACCES/EPERM/EBUSY → permission-specific message; everything else
 * (including the LadybugDB missing-shadow error if it ever propagates here)
 * → `shadowSidecarRecoveryMessage`.
 *
 * See plan: docs/plans/2026-05-21-001-fix-pr-1747-quarantine-enoent-and-large-wal-plan.md (U2)
 */
async function tryQuarantineForMissingShadow(dbPath, opts) {
    try {
        const quarantinePath = await quarantineWalForMissingShadow(dbPath, {
            logger: poolSidecarLogger,
            level: 'warn',
            reason: opts.reason,
        });
        return { kind: 'quarantined', path: quarantinePath };
    }
    catch (err) {
        if (isMissingFsError(err)) {
            const walStat = await statIfExists(`${dbPath}.wal`);
            if (walStat === null) {
                return { kind: 'peer-handled' };
            }
            // Defensive: ENOENT during rename but WAL still present afterwards.
            // Don't silently swallow — surface a classified error. ENOENT falls
            // through to shadowSidecarRecoveryMessage in renameFailureMessage.
            throw new Error(renameFailureMessage(dbPath, err));
        }
        // Classify the rename failure itself — EACCES/EPERM/EBUSY get the
        // permission-specific message; everything else falls through.
        throw new Error(renameFailureMessage(dbPath, err));
    }
}
async function probeDatabaseForShadowReplay(db) {
    const conn = createConnection(db);
    try {
        const queryResult = await conn.query(SHADOW_REPLAY_PROBE_QUERY);
        const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
        await result.getAll();
        result.close?.();
    }
    finally {
        await conn.close().catch(() => { });
    }
}
async function replayShadowPagesWithWritableOpen(dbPath) {
    let db;
    try {
        db = createLbugDatabase(lbug, toNativeSafePath(dbPath), { throwOnWalReplayFailure: false });
        await db.init();
        await probeDatabaseForShadowReplay(db);
    }
    catch (err) {
        if (isMissingShadowSidecarError(err)) {
            await tryQuarantineForMissingShadow(dbPath, {
                reason: 'pool writable replay recovery',
            });
            return;
        }
        throw err;
    }
    finally {
        if (db)
            await db.close().catch(() => { });
    }
}
async function openReadOnlyDatabase(dbPath) {
    let db;
    silenceStdout();
    try {
        await preflightLbugSidecars(dbPath, {
            mode: 'read-only',
            logger: poolSidecarLogger,
            allowQuarantine: true,
        });
        db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
            readOnly: true,
            throwOnWalReplayFailure: false,
        });
        await db.init();
        try {
            await probeDatabaseForShadowReplay(db);
        }
        catch (err) {
            if (isMissingShadowSidecarError(err)) {
                await db.close().catch(() => { });
                db = undefined;
                await tryQuarantineForMissingShadow(dbPath, {
                    reason: 'pool read-only recovery',
                });
                await preflightLbugSidecars(dbPath, {
                    mode: 'read-only',
                    logger: poolSidecarLogger,
                    allowQuarantine: true,
                });
                db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
                    readOnly: true,
                    throwOnWalReplayFailure: false,
                });
                await db.init();
                await probeDatabaseForShadowReplay(db);
                return db;
            }
            if (!isReadOnlyShadowReplayError(err)) {
                throw err;
            }
            await db.close().catch(() => { });
            db = undefined;
            await replayShadowPagesWithWritableOpen(dbPath);
            db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
                readOnly: true,
                throwOnWalReplayFailure: false,
            });
            await db.init();
            await probeDatabaseForShadowReplay(db);
        }
        return db;
    }
    catch (err) {
        if (db)
            await db.close().catch(() => { });
        throw err;
    }
    finally {
        restoreStdout();
    }
}
/**
 * Quarantine the .wal file and retry opening the database.
 * Used when the initial open fails with a WAL corruption error.
 */
async function tryQuarantineAndReopen(dbPath, repoId) {
    const walPath = dbPath + '.wal';
    const quarantineName = `${walPath}.corrupt.${Date.now()}-${Math.random().toString(36).slice(2)}`;
    try {
        await fs.rename(walPath, quarantineName);
    }
    catch {
        throw new Error(`LadybugDB WAL corruption detected for ${repoId}. ` +
            `Run \`gitnexus analyze\` to rebuild the index. (quarantine failed)`);
    }
    realStderrWrite(`GitNexus: LadybugDB WAL quarantined for ${repoId}; graph may be stale. ` +
        `Run \`gitnexus analyze\` to rebuild the index.\n`);
    return await openReadOnlyDatabase(dbPath);
}
/** Deduplicates concurrent initLbug calls for the same repoId */
const initPromises = new Map();
/**
 * Initialize (or reuse) a Database + connection pool for a specific repo.
 * Retries on lock errors (e.g., when `gitnexus analyze` is running).
 *
 * Concurrent calls for the same repoId are deduplicated — the second caller
 * awaits the first's in-progress init rather than starting a redundant one.
 */
export const initLbug = async (repoId, dbPath) => {
    const existing = pool.get(repoId);
    if (existing) {
        existing.lastUsed = Date.now();
        return;
    }
    // Deduplicate concurrent init calls for the same repoId —
    // prevents double-init race when multiple parallel tool calls
    // trigger initialization for the same repo simultaneously.
    const pending = initPromises.get(repoId);
    if (pending)
        return pending;
    const promise = doInitLbug(repoId, dbPath);
    initPromises.set(repoId, promise);
    try {
        await promise;
    }
    finally {
        initPromises.delete(repoId);
    }
};
/**
 * Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
 * Pool entry is registered LAST so concurrent executeQuery calls see either
 * "not initialized" (and throw) or a fully ready pool — never a half-built one.
 */
async function doInitLbug(repoId, dbPath) {
    // Check if database exists
    try {
        await fs.stat(dbPath);
    }
    catch {
        throw new Error(`LadybugDB not found at ${dbPath}. Run: gitnexus analyze`);
    }
    evictLRU();
    // Reuse an existing native Database if another repoId already opened this path.
    // This prevents buffer manager exhaustion from multiple mmap regions on the same file.
    let shared = dbCache.get(dbPath);
    if (!shared) {
        // Open in read-only mode — MCP server never writes to the database.
        // This allows multiple MCP server instances to read concurrently, and
        // avoids lock conflicts when `gitnexus analyze` is writing.
        let lastError = null;
        for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
            try {
                const db = await openReadOnlyDatabase(dbPath);
                shared = { db, refCount: 0, ftsLoaded: false };
                dbCache.set(dbPath, shared);
                break;
            }
            catch (err) {
                lastError = err instanceof Error ? err : new Error(String(err));
                if (isWalCorruptionError(lastError)) {
                    try {
                        const db = await tryQuarantineAndReopen(dbPath, repoId);
                        shared = { db, refCount: 0, ftsLoaded: false };
                        dbCache.set(dbPath, shared);
                        break;
                    }
                    catch (retryErr) {
                        throw new Error(`LadybugDB WAL corruption detected for ${repoId}. ${WAL_RECOVERY_SUGGESTION} ` +
                            `(${retryErr instanceof Error ? retryErr.message : String(retryErr)})`);
                    }
                }
                if (lastError.message.startsWith('LadybugDB checkpoint sidecar is missing') ||
                    lastError.message.startsWith('GitNexus could not move the LadybugDB WAL sidecar') ||
                    isMissingShadowSidecarError(lastError)) {
                    throw lastError;
                }
                const isLockError = lastError.message.includes('Could not set lock') ||
                    /\block(\b|ed|ing)/i.test(lastError.message);
                if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
                    break;
                await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
            }
        }
        if (!shared) {
            throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
                `Retry later. (${lastError?.message || 'unknown error'})`);
        }
    }
    shared.refCount++;
    const db = shared.db;
    // Pre-create the full pool upfront so createConnection() (which silences
    // stdout) is never called lazily during active query execution.
    // Mark preWarmActive so the watchdog timer doesn't interfere.
    preWarmActive = true;
    const available = [];
    try {
        for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
            available.push(createConnection(db));
        }
    }
    finally {
        preWarmActive = false;
    }
    // Load FTS extension once per shared Database.
    // Done BEFORE pool registration so no concurrent checkout can grab
    // the connection while the async FTS load is in progress.
    // policy: 'load-only' — the read pool must never trigger a network
    // install; analyze owns extension installation. If LOAD fails, search
    // features degrade gracefully and the user-facing query path proceeds.
    if (!shared.ftsLoaded) {
        // Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows during
        // *install* — the @ladybugdb/core out-of-process installer hits an unhandled
        // error path that signals SIGSEGV instead of throwing (see #1199, #1217).
        // The previous unconditional skip was over-broad: it also disabled FTS on
        // hosts where the binary was already on disk and only needed LOAD, leaving
        // BM25 silently degraded with no error path (see #1690).
        //
        // Probe ~/.lbdb/extension/*/win_amd64/fts/ first. If any binary is on disk
        // we run loadFTSExtension(..., 'load-only'); the install path is never
        // exercised, and LadybugDB's version-specific resolution + ExtensionManager
        // try/catch handle stale/zero-byte siblings cleanly (verified empirically
        // on Win10 + Node 22.19 + gitnexus 1.6.5 + @ladybugdb/core 0.16.1). With
        // no binary at all, we fall back to the upstream skip so install-time
        // SIGSEGV continues to be avoided.
        if (process.platform === 'win32') {
            shared.ftsLoaded = (await hasLocalWinFtsExtension())
                ? await loadFTSExtension(available[0], { policy: 'load-only' })
                : true;
        }
        else {
            shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
        }
    }
    // Register pool entry only after all connections are pre-warmed and FTS is
    // loaded.  Concurrent executeQuery calls see either "not initialized"
    // (and throw cleanly) or a fully ready pool — never a half-built one.
    pool.set(repoId, {
        db,
        available,
        checkedOut: 0,
        waiters: [],
        lastUsed: Date.now(),
        dbPath,
        closed: false,
    });
    ensureIdleTimer();
}
/**
 * Initialize a pool entry from a pre-existing Database object.
 *
 * Used in tests to avoid the writable→close→read-only cycle that crashes
 * on macOS due to N-API destructor segfaults.  The pool adapter reuses
 * the core adapter's writable Database instead of opening a new read-only one.
 *
 * The Database is registered in the shared dbCache so closeOne() decrements
 * the refCount correctly.  If the Database is already cached (e.g. another
 * repoId already injected it), the existing entry is reused.
 */
export async function initLbugWithDb(repoId, existingDb, dbPath) {
    const existing = pool.get(repoId);
    if (existing) {
        existing.lastUsed = Date.now();
        return;
    }
    // Register in dbCache with external: true so other initLbug() calls
    // for the same dbPath reuse this Database instead of trying to open
    // a new one (which would fail with a file lock error).
    // closeOne() respects the external flag and skips db.close().
    let shared = dbCache.get(dbPath);
    if (!shared) {
        shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
        dbCache.set(dbPath, shared);
    }
    shared.refCount++;
    const available = [];
    preWarmActive = true;
    try {
        for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
            available.push(createConnection(existingDb));
        }
    }
    finally {
        preWarmActive = false;
    }
    // Load FTS extension if not already loaded on this Database.
    // policy: 'load-only' — same contract as initLbug above; the read pool
    // must not block on a network install during query execution.
    // Windows guard: same probe-then-load policy as doInitLbug above.
    if (!shared.ftsLoaded) {
        if (process.platform === 'win32') {
            shared.ftsLoaded = (await hasLocalWinFtsExtension())
                ? await loadFTSExtension(available[0], { policy: 'load-only' })
                : true;
        }
        else {
            shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
        }
    }
    pool.set(repoId, {
        db: existingDb,
        available,
        checkedOut: 0,
        waiters: [],
        lastUsed: Date.now(),
        dbPath,
        closed: false,
    });
    ensureIdleTimer();
}
/**
 * Checkout a connection from the pool.
 * Returns an available connection, or creates a new one if under the cap.
 * If all connections are busy and at cap, queues the caller until one is returned.
 */
function checkout(entry) {
    // Fast path: grab an available connection
    if (entry.available.length > 0) {
        entry.checkedOut++;
        return Promise.resolve(entry.available.pop());
    }
    // Pool was pre-warmed to MAX_CONNS_PER_REPO during init.  If we're here
    // with fewer total connections, something leaked — surface the bug rather
    // than silently creating a connection (which would silence stdout mid-query).
    const totalConns = entry.available.length + entry.checkedOut;
    if (totalConns < MAX_CONNS_PER_REPO) {
        throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
            `connections but found ${totalConns} (${entry.available.length} available, ` +
            `${entry.checkedOut} checked out)`);
    }
    // At capacity — queue the caller with a timeout.
    return new Promise((resolve, reject) => {
        const waiter = (conn) => {
            clearTimeout(timer);
            resolve(conn);
        };
        const timer = setTimeout(() => {
            const idx = entry.waiters.indexOf(waiter);
            if (idx !== -1)
                entry.waiters.splice(idx, 1);
            reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
        }, WAITER_TIMEOUT_MS);
        entry.waiters.push(waiter);
    });
}
/**
 * Return a connection to the pool after use.
 * If the pool entry was closed while the connection was checked out (e.g.
 * LRU eviction), close the orphaned connection instead of returning it.
 * If there are queued waiters, hand the connection directly to the next one
 * instead of putting it back in the available array (avoids race conditions).
 */
function checkin(entry, conn) {
    if (entry.closed) {
        // Pool entry was deleted during checkout — close the orphaned connection
        conn.close().catch(() => { });
        return;
    }
    if (entry.waiters.length > 0) {
        // Hand directly to the next waiter — no intermediate available state
        const waiter = entry.waiters.shift();
        waiter(conn);
    }
    else {
        entry.checkedOut--;
        entry.available.push(conn);
    }
}
/**
 * Execute a query on a specific repo's connection pool.
 * Automatically checks out a connection, runs the query, and returns it.
 */
/** Race a promise against a timeout */
function withTimeout(promise, ms, label) {
    let timer;
    const timeout = new Promise((_, reject) => {
        timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
    });
    return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
export const executeQuery = async (repoId, cypher) => {
    return await executeParameterized(repoId, cypher, {});
};
/**
 * Execute a parameterized query on a specific repo's connection pool.
 * Uses prepare/execute pattern to prevent Cypher injection.
 */
export const executeParameterized = async (repoId, cypher, params) => {
    const entry = pool.get(repoId);
    if (!entry) {
        throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
    }
    entry.lastUsed = Date.now();
    const conn = await checkout(entry);
    silenceStdout();
    activeQueryCount++;
    try {
        const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
        if (!stmt.isSuccess()) {
            const errMsg = await stmt.getErrorMessage();
            throw new Error(`Prepare failed: ${errMsg}`);
        }
        const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
        const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
        const rows = await result.getAll();
        return rows;
    }
    catch (err) {
        if (isReadOnlyDbError(err)) {
            throw new Error('Write operations are not allowed. The pool adapter is read-only.');
        }
        throw err;
    }
    finally {
        activeQueryCount--;
        restoreStdout();
        checkin(entry, conn);
    }
};
/**
 * Close one or all repo pools.
 * If repoId is provided, close only that repo's connections.
 * If omitted, close all repos.
 */
export const closeLbug = async (repoId) => {
    if (repoId) {
        closeOne(repoId);
        return;
    }
    for (const id of [...pool.keys()]) {
        closeOne(id);
    }
    if (idleTimer) {
        clearInterval(idleTimer);
        idleTimer = null;
    }
};
/**
 * Check if a specific repo's pool is active
 */
export const isLbugReady = (repoId) => pool.has(repoId);
