import { SybaseError, SybaseConnectionError, SybaseQueryError, SybaseTimeoutError, SybasePoolError } from "./errors.js";
/**
 * Sybase connection management with production-grade connection pooling.
 *
 * Features:
 * - Persistent db-lib connections via N-API
 * - Connection pool with max sizing
 * - Health checks (dead connection detection + optional ping)
 * - Idle connection cleanup
 * - Automatic reconnection for dead connections
 * - Acquire timeout with queuing
 * - Retry on transient failures
 * - Graceful drain/shutdown
 * - Observable pool metrics
 * - Query logging/middleware support
 */
import { native } from "./native/index.js";
// ---------------------------------------------------------------------------
// Single connection
// ---------------------------------------------------------------------------
/**
 * A single Sybase connection wrapping the native FreeTDS handle.
 *
 * Normally you won't use this directly — use `SybasePool` instead.
 * This class manages the lifecycle of a single db-lib connection.
 */
export class SybaseConnection {
    config;
    handle = null;
    closed = false;
    inUse = false;
    /** Promise queue to serialize queries on a single DBPROCESS (not thread-safe). */
    queryQueue = Promise.resolve();
    constructor(config) {
        this.config = config;
    }
    async connect() {
        if (this.handle && !this.closed) {
            return;
        }
        this.closed = false;
        try {
            this.handle = await native.connect({
                host: this.config.host,
                port: this.config.port,
                database: this.config.database,
                username: this.config.username,
                password: this.config.password,
                timeout: this.config.timeout ?? 30,
                packetSize: this.config.packetSize ?? 4096
            });
        }
        catch (err) {
            throw new SybaseConnectionError(err.message ?? String(err), {
                host: this.config.host,
                port: this.config.port
            });
        }
    }
    async query(sql, options) {
        // Serialize queries on this connection — db-lib DBPROCESS is not reentrant.
        const task = this.queryQueue.then(() => this.executeQuery(sql, options));
        // Update queue head; swallow rejections so subsequent queries still run.
        this.queryQueue = task.catch(() => { });
        return task;
    }
    async executeQuery(sql, options) {
        if (!this.handle || this.closed) {
            throw new SybaseConnectionError("Connection is closed", {
                host: this.config.host,
                port: this.config.port
            });
        }
        const baseTimeoutS = this.config.timeout ?? 30;
        const nativeHardMs = options?.hardTimeoutMs ?? this.config.hardTimeoutMs;
        // JS-side fallback: even longer than the native watchdog. If THIS fires,
        // the native worker is stuck despite dbcancel — we bail at the JS layer
        // and mark the connection dead so the pool replaces it. The worker may
        // continue running until the OS finally gives up the socket; that's
        // acceptable because libuv's worker pool will reclaim the slot eventually.
        const jsHardMs = (nativeHardMs ?? Math.max(baseTimeoutS * 2 * 1000, 30000)) + 5000;
        let timer = null;
        const timeoutPromise = new Promise((_, reject) => {
            timer = setTimeout(() => {
                // Mark dead so the pool will discard this connection on release.
                this.closed = true;
                reject(new SybaseTimeoutError(`Query exceeded JS-side hard timeout of ${jsHardMs}ms; ` +
                    `native worker did not respond to cancel and connection has been abandoned`, "query", jsHardMs));
            }, jsHardMs);
            // Don't keep the event loop alive just for this timer.
            if (timer && typeof timer.unref === "function") {
                timer.unref();
            }
        });
        const nativeOptions = {};
        if (options?.maxRows !== undefined) {
            nativeOptions.maxRows = options.maxRows;
        }
        if (nativeHardMs !== undefined) {
            nativeOptions.hardTimeoutMs = nativeHardMs;
        }
        try {
            const result = (await Promise.race([
                native.query(this.handle, sql, nativeOptions),
                timeoutPromise
            ]));
            return result;
        }
        catch (err) {
            // Native-side hard timeout — connection is dead.
            if (err && err.hardTimeout) {
                this.closed = true;
                throw new SybaseTimeoutError(err.message ?? "Query hard timeout", "query", baseTimeoutS * 2 * 1000);
            }
            // Cooperative timeout (db-lib's DBSETTIME fired). Connection itself
            // may still be usable, but to be safe we let the pool decide via
            // isConnected().
            if (err && err.timedOut) {
                throw new SybaseTimeoutError(err.message ?? "Query timed out", "query", baseTimeoutS * 1000);
            }
            // Dead connection (e.g. server dropped, network gone)
            if (err && err.connectionDead) {
                this.closed = true;
                throw new SybaseConnectionError(err.message ?? String(err), {
                    host: this.config.host,
                    port: this.config.port
                });
            }
            if (err instanceof SybaseTimeoutError || err instanceof SybaseConnectionError) {
                throw err;
            }
            throw new SybaseQueryError(err.message ?? String(err), {
                sql,
                connectionDead: false
            });
        }
        finally {
            if (timer) {
                clearTimeout(timer);
            }
        }
    }
    close() {
        if (this.handle) {
            try {
                native.close(this.handle);
            }
            catch {
                // Ignore close errors
            }
            this.handle = null;
        }
        this.closed = true;
    }
    get isConnected() {
        if (!this.handle || this.closed) {
            return false;
        }
        return native.isAlive(this.handle);
    }
    /** @internal */
    get _inUse() {
        return this.inUse;
    }
    /** @internal */
    set _inUse(val) {
        this.inUse = val;
    }
}
/**
 * Production-grade connection pool for Sybase ASE.
 *
 * Features:
 * - max pool sizing with automatic scaling
 * - Health checks (dead connection detection + optional ping)
 * - Idle connection cleanup
 * - Acquire timeout with fair queuing
 * - Retry on transient connection failures
 * - Graceful drain/shutdown
 * - Observable metrics for monitoring
 * - Query logging middleware
 *
 * @example
 * ```ts
 * const pool = new SybasePool({
 *   host: "sybase-host",
 *   port: 5000,
 *   database: "mydb",
 *   username: "sa",
 *   password: "secret",
 *   max: 10,
 *   logger: { query(log) { console.log(`[${log.durationMs}ms] ${log.sql}`); } }
 * });
 *
 * const result = await pool.query("SELECT 1 AS val");
 * console.log(pool.metrics);
 * await pool.drain();
 * ```
 */
export class SybasePool {
    pool = [];
    waitQueue = [];
    closed = false;
    draining = false;
    cleanupTimer = null;
    cfg;
    // Metrics tracking
    _totalConnectionsCreated = 0;
    _totalConnectionsDestroyed = 0;
    _totalQueriesExecuted = 0;
    _totalQueryErrors = 0;
    _totalAcquireTimeouts = 0;
    _queryDurationSum = 0;
    _queryDurationCount = 0;
    _activeCount = 0;
    /** Number of connections currently being created (prevents exceeding max). */
    _pendingCreates = 0;
    constructor(config) {
        const max = config.max ?? 5;
        if (max < 1) {
            throw new SybasePoolError("Pool max must be at least 1", "closed");
        }
        this.cfg = {
            ...config,
            timeout: config.timeout ?? 30,
            max,
            idleTimeoutMs: config.idleTimeoutMs ?? 60000,
            acquireTimeoutMs: config.acquireTimeoutMs ?? 10000,
            cleanupIntervalMs: config.cleanupIntervalMs ?? 30000,
            healthCheck: config.healthCheck ?? false,
            retries: config.retries ?? 2,
            retryDelayMs: config.retryDelayMs ?? 500,
            logger: config.logger
        };
        // Start idle cleanup timer
        this.cleanupTimer = setInterval(() => {
            this.cleanupIdle();
        }, this.cfg.cleanupIntervalMs);
        // Unref so it doesn't keep the process alive
        if (this.cleanupTimer && typeof this.cleanupTimer.unref === "function") {
            this.cleanupTimer.unref();
        }
    }
    /**
     * Acquire a connection from the pool.
     * - Validates existing connections (health check)
     * - Creates new connections up to `max`
     * - Queues if pool is full
     */
    async acquire() {
        if (this.closed) {
            throw new SybasePoolError("Pool is closed", "closed");
        }
        if (this.draining) {
            throw new SybasePoolError("Pool is draining — no new connections allowed", "draining");
        }
        // Try to find an idle, healthy connection
        for (let i = 0; i < this.pool.length; i++) {
            const pooled = this.pool[i];
            if (!pooled.inUse) {
                // Check if connection is still alive
                if (!pooled.conn.isConnected) {
                    // Dead connection — remove and continue
                    this.removeFromPool(i);
                    i--;
                    continue;
                }
                // Optional health check (lightweight query)
                if (this.cfg.healthCheck) {
                    try {
                        await pooled.conn.query("SELECT 1");
                    }
                    catch {
                        // Health check failed — remove dead connection
                        this.removeFromPool(i);
                        i--;
                        continue;
                    }
                }
                pooled.inUse = true;
                pooled.conn._inUse = true;
                pooled.lastUsed = Date.now();
                this._activeCount++;
                return pooled.conn;
            }
        }
        // Can we create a new connection?
        if (this.pool.length + this._pendingCreates < this.cfg.max) {
            this._pendingCreates++;
            let conn;
            try {
                conn = await this.createConnection();
            }
            catch (err) {
                this._pendingCreates--;
                throw err;
            }
            this._pendingCreates--;
            // Re-check after async gap: another acquire may have filled pool
            if (this.pool.length >= this.cfg.max) {
                // Pool is full now — close the extra connection
                conn.close();
                this._totalConnectionsDestroyed++;
                // Fall through to wait queue below
            }
            else {
                const pooled = {
                    conn,
                    lastUsed: Date.now(),
                    createdAt: Date.now(),
                    inUse: true
                };
                conn._inUse = true;
                this._activeCount++;
                this.pool.push(pooled);
                return conn;
            }
        }
        // Pool full — wait for one to become available
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                const idx = this.waitQueue.findIndex(w => w.resolve === resolve);
                if (idx !== -1) {
                    this.waitQueue.splice(idx, 1);
                }
                this._totalAcquireTimeouts++;
                reject(new SybaseTimeoutError(`Connection acquire timeout after ${this.cfg.acquireTimeoutMs}ms`, "acquire", this.cfg.acquireTimeoutMs));
            }, this.cfg.acquireTimeoutMs);
            this.waitQueue.push({ resolve, reject, timer });
        });
    }
    /**
     * Release a connection back to the pool.
     */
    release(conn) {
        const pooled = this.pool.find(p => p.conn === conn);
        if (!pooled || !pooled.inUse) {
            // Already released or not in pool — ignore
            return;
        }
        pooled.inUse = false;
        conn._inUse = false;
        pooled.lastUsed = Date.now();
        this._activeCount--;
        // Note: SET ROWCOUNT is already reset to 0 within query batches by
        // SybaseSelectBuilder (appends "SET ROWCOUNT 0" after every paginated query).
        // We do NOT issue an async reset here to avoid race conditions where the
        // reset command would execute concurrently with the next user's query.
        // If connection died during use, remove it
        if (!conn.isConnected) {
            const idx = this.pool.indexOf(pooled);
            if (idx !== -1) {
                this.pool.splice(idx, 1);
                conn.close();
                this._totalConnectionsDestroyed++;
            }
            // Try to serve a waiter with a new connection (only if not draining)
            if (!this.draining) {
                void this.serveWaiter();
            }
            return;
        }
        // If draining and no in-use connections, we're done
        if (this.draining) {
            this.checkDrainComplete();
            return;
        }
        // If someone is waiting, give them this connection
        if (this.waitQueue.length > 0) {
            const waiter = this.waitQueue.shift();
            clearTimeout(waiter.timer);
            pooled.inUse = true;
            conn._inUse = true;
            pooled.lastUsed = Date.now();
            this._activeCount++;
            waiter.resolve(conn);
        }
    }
    /**
     * Execute a query using a pooled connection (auto acquire/release).
     * Retries on transient connection failures.
     */
    async query(sql, options) {
        let lastError = null;
        for (let attempt = 0; attempt <= this.cfg.retries; attempt++) {
            const conn = await this.acquire();
            const startTime = Date.now();
            try {
                const result = await conn.query(sql, options);
                const durationMs = Date.now() - startTime;
                this._totalQueriesExecuted++;
                this._queryDurationSum += durationMs;
                this._queryDurationCount++;
                // Log successful query
                if (this.cfg.logger) {
                    this.cfg.logger.query({
                        sql,
                        durationMs,
                        rowCount: result.rowCount,
                        timestamp: new Date(startTime)
                    });
                }
                this.release(conn);
                return result;
            }
            catch (err) {
                const durationMs = Date.now() - startTime;
                this._totalQueryErrors++;
                // Log failed query
                if (this.cfg.logger) {
                    this.cfg.logger.query({
                        sql,
                        durationMs,
                        rowCount: 0,
                        error: err instanceof Error ? err : new Error(String(err)),
                        timestamp: new Date(startTime)
                    });
                }
                // Reset session state (SET ROWCOUNT) if connection is still alive
                // to prevent state leaking to the next query on this pooled connection.
                if (conn.isConnected) {
                    try {
                        await conn.query("SET ROWCOUNT 0");
                    }
                    catch {
                        // If reset fails, connection will be detected dead on release
                    }
                }
                this.release(conn);
                if (err instanceof SybaseConnectionError && attempt < this.cfg.retries) {
                    // Transient failure — retry after delay
                    lastError = err;
                    await this.delay(this.cfg.retryDelayMs);
                    continue;
                }
                throw err;
            }
        }
        throw lastError ?? new SybaseError("Query failed after retries");
    }
    /**
     * Gracefully drain the pool — finish all in-flight operations, then close.
     *
     * New acquire() calls will be rejected immediately.
     * Returns a promise that resolves when all active connections are released.
     *
     * @param timeoutMs - Maximum time to wait for drain. Default: 30000ms
     */
    async drain(timeoutMs = 30000) {
        if (this.closed) {
            return;
        }
        this.draining = true;
        // Reject all waiters immediately
        for (const waiter of this.waitQueue) {
            clearTimeout(waiter.timer);
            waiter.reject(new SybasePoolError("Pool is draining", "draining"));
        }
        this.waitQueue = [];
        // If no active connections, close immediately
        if (this.active === 0) {
            return this.close();
        }
        // Wait for all active connections to be released
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                // Force close after timeout
                this.close().then(resolve, reject);
            }, timeoutMs);
            if (typeof timer.unref === "function") {
                timer.unref();
            }
            this._drainResolve = () => {
                clearTimeout(timer);
                this.close().then(resolve, reject);
            };
        });
    }
    _drainResolve;
    checkDrainComplete() {
        if (this.draining && this.active === 0 && this._drainResolve) {
            this._drainResolve();
            this._drainResolve = undefined;
        }
    }
    /**
     * Close all connections and shut down the pool.
     */
    async close() {
        this.closed = true;
        this.draining = false;
        // Stop cleanup timer
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
        // Reject all waiters
        for (const waiter of this.waitQueue) {
            clearTimeout(waiter.timer);
            waiter.reject(new SybasePoolError("Pool is closing", "closed"));
        }
        this.waitQueue = [];
        // Close all connections
        for (const pooled of this.pool) {
            pooled.conn.close();
            this._totalConnectionsDestroyed++;
        }
        this.pool = [];
        this._activeCount = 0;
    }
    // ---------------------------------------------------------------------------
    // Pool state accessors
    // ---------------------------------------------------------------------------
    /** Number of total connections in the pool. */
    get size() {
        return this.pool.length;
    }
    /** Number of idle (available) connections. */
    get idle() {
        return this.pool.length - this._activeCount;
    }
    /** Number of active (in-use) connections. */
    get active() {
        return this._activeCount;
    }
    /** Number of waiters in queue. */
    get waiting() {
        return this.waitQueue.length;
    }
    /** Whether the pool is currently draining. */
    get isDraining() {
        return this.draining;
    }
    /** Whether the pool is closed. */
    get isClosed() {
        return this.closed;
    }
    /** @internal Get the configured logger (if any). */
    get logger() {
        return this.cfg.logger;
    }
    /**
     * Get a snapshot of pool metrics for monitoring/observability.
     */
    get metrics() {
        return {
            totalConnectionsCreated: this._totalConnectionsCreated,
            totalConnectionsDestroyed: this._totalConnectionsDestroyed,
            totalQueriesExecuted: this._totalQueriesExecuted,
            totalQueryErrors: this._totalQueryErrors,
            totalAcquireTimeouts: this._totalAcquireTimeouts,
            avgQueryDurationMs: this._queryDurationCount > 0
                ? Math.round(this._queryDurationSum / this._queryDurationCount)
                : 0,
            currentSize: this.size,
            currentActive: this.active,
            currentIdle: this.idle,
            currentWaiting: this.waiting
        };
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    async createConnection() {
        const conn = new SybaseConnection(this.cfg);
        await conn.connect();
        this._totalConnectionsCreated++;
        return conn;
    }
    removeFromPool(index) {
        const removed = this.pool.splice(index, 1)[0];
        if (removed) {
            removed.conn.close();
            this._totalConnectionsDestroyed++;
        }
    }
    async serveWaiter() {
        if (this.waitQueue.length === 0) {
            return;
        }
        this._pendingCreates++;
        try {
            const conn = await this.createConnection();
            this._pendingCreates--;
            const pooled = {
                conn,
                lastUsed: Date.now(),
                createdAt: Date.now(),
                inUse: true
            };
            conn._inUse = true;
            this._activeCount++;
            this.pool.push(pooled);
            // Re-check queue after async operation — waiter may have timed out
            const waiter = this.waitQueue.shift();
            if (waiter) {
                clearTimeout(waiter.timer);
                waiter.resolve(conn);
            }
            else {
                // No waiter left — release the connection back to idle
                pooled.inUse = false;
                conn._inUse = false;
                this._activeCount--;
            }
        }
        catch (err) {
            this._pendingCreates--;
            // Can't create connection — reject the first waiter if any
            const waiter = this.waitQueue.shift();
            if (waiter) {
                clearTimeout(waiter.timer);
                waiter.reject(err instanceof Error ? err : new SybaseError(String(err)));
            }
        }
    }
    /**
     * Remove idle connections that have exceeded the idle timeout.
     */
    cleanupIdle() {
        const now = Date.now();
        let i = 0;
        while (i < this.pool.length) {
            const pooled = this.pool[i];
            const isIdle = !pooled.inUse;
            const isExpired = now - pooled.lastUsed > this.cfg.idleTimeoutMs;
            if (isIdle && isExpired) {
                this.removeFromPool(i);
                // Don't increment i — the array shifted
            }
            else {
                i++;
            }
        }
    }
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}
//# sourceMappingURL=connection.js.map