import { EventEmitter } from 'node:events';
import pg from 'pg';
import { ConnectionClosedError, QueryTimeoutError, isRetryableError } from "./errors.js";
import { registerClient, unregisterClient } from "./registry.js";
import { computePoolHotReloadKey, getSharedHotReloadPool, isHotReloadEnabled, } from "./hot-reload.js";
/**
 * A resilient PostgreSQL connection pool.
 *
 * Wraps the `pg` package's Pool and adds:
 * - Safe idle-client error handling (absorbed; pg.Pool self-heals)
 * - Query/connect retry on transient connection errors
 * - Connection state tracking
 * - Pool statistics
 *
 * @example
 * ```typescript
 * const pool = new PostgresPool({
 *   connectionString: process.env.DATABASE_URL,
 *   max: 20,
 *   reconnect: { maxAttempts: 5 }
 * });
 *
 * // Execute queries
 * const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
 *
 * // Close when done
 * await pool.close();
 * ```
 */
export class PostgresPool extends EventEmitter {
    _pool = null;
    _config;
    _poolConfig = {};
    _connected = false;
    _reconnecting = false;
    _closed = false;
    _shuttingDown = false;
    _signalHandlers = [];
    _reconnectPromise = null;
    _connectPromise = null;
    _stats = {
        totalConnections: 0,
        reconnectAttempts: 0,
        failedReconnects: 0,
        lastConnectedAt: null,
        lastDisconnectedAt: null,
        lastReconnectAttemptAt: null,
    };
    /**
     * Creates a new PostgresPool.
     *
     * Note: By default, the actual connection is established lazily on first query.
     * Set `preconnect: true` in config to verify connectivity immediately.
     *
     * @param config - Connection configuration. Can be a connection URL string or a config object.
     *                 If not provided, uses `process.env.DATABASE_URL`.
     */
    constructor(config) {
        super();
        if (typeof config === 'string') {
            this._config = { connectionString: config };
        }
        else {
            this._config = config ?? {};
        }
        // Initialize the pool
        this._initializePool();
        // Register shutdown signal handlers to prevent reconnection during app shutdown
        this._registerShutdownHandlers();
        // Register this pool in the global registry for coordinated shutdown
        registerClient(this, { hotReloadKey: computePoolHotReloadKey(this._config) });
        // If preconnect is enabled, establish connection immediately
        if (this._config.preconnect) {
            const p = this._warmConnection();
            // Attach no-op catch to suppress unhandled rejection warnings
            p.catch(() => { });
            this._connectPromise = p;
        }
    }
    /**
     * Whether the pool is currently connected.
     */
    get connected() {
        return this._connected;
    }
    /**
     * Whether the pool is shutting down (won't attempt reconnection).
     */
    get shuttingDown() {
        return this._shuttingDown;
    }
    /**
     * Whether a reconnection attempt is in progress.
     */
    get reconnecting() {
        return this._reconnecting;
    }
    /**
     * Pool statistics.
     */
    get stats() {
        return {
            ...this._stats,
            connected: this._connected,
            reconnecting: this._reconnecting,
            totalCount: this._pool?.totalCount ?? 0,
            idleCount: this._pool?.idleCount ?? 0,
            waitingCount: this._pool?.waitingCount ?? 0,
        };
    }
    /**
     * Total number of clients in the pool.
     */
    get totalCount() {
        return this._pool?.totalCount ?? 0;
    }
    /**
     * Number of idle clients in the pool.
     */
    get idleCount() {
        return this._pool?.idleCount ?? 0;
    }
    /**
     * Number of clients waiting to be acquired.
     */
    get waitingCount() {
        return this._pool?.waitingCount ?? 0;
    }
    /**
     * Number of expired clients in the pool.
     */
    get expiredCount() {
        const pool = this._pool;
        return pool?.expiredCount ?? 0;
    }
    /**
     * Whether the pool is ending (compat with pg.Pool).
     */
    get ending() {
        const pool = this._pool;
        return pool?.ending ?? this._shuttingDown;
    }
    /**
     * Whether the pool has ended (compat with pg.Pool).
     */
    get ended() {
        const pool = this._pool;
        return pool?.ended ?? this._closed;
    }
    /**
     * Pool configuration options.
     */
    get options() {
        return this._poolConfig;
    }
    query(text, values, callback) {
        const handler = typeof values === 'function' ? values : callback;
        const queryValues = typeof values === 'function' ? undefined : values;
        if (handler) {
            void this._executeWithRetry(async () => {
                const pool = await this._ensureConnectedAsync();
                return pool.query(text, queryValues);
            })
                .then((result) => handler(null, result))
                .catch((error) => handler(error instanceof Error ? error : new Error(String(error))));
            return;
        }
        return this._executeWithRetry(async () => {
            const pool = await this._ensureConnectedAsync();
            return pool.query(text, queryValues);
        });
    }
    connect(callback) {
        if (callback) {
            void this._executeWithRetry(async () => {
                const pool = await this._ensureConnectedAsync();
                return pool.connect();
            })
                .then((client) => callback(null, client, client.release.bind(client)))
                .catch((error) => callback(error instanceof Error ? error : new Error(String(error))));
            return;
        }
        return this._executeWithRetry(async () => {
            const pool = await this._ensureConnectedAsync();
            return pool.connect();
        });
    }
    /**
     * Signal that the application is shutting down.
     * This prevents reconnection attempts but doesn't immediately close the pool.
     * Use this when you want to gracefully drain connections before calling close().
     */
    shutdown() {
        this._shuttingDown = true;
    }
    /**
     * Close the pool and release all connections.
     * Alias for end() for compatibility with PostgresClient.
     */
    async close() {
        return this.end();
    }
    /**
     * Close the pool and release all connections.
     */
    async end() {
        this._closed = true;
        this._shuttingDown = true;
        this._connected = false;
        this._reconnecting = false;
        // Remove signal handlers
        this._removeShutdownHandlers();
        // Unregister from global registry
        unregisterClient(this);
        if (this._pool) {
            await this._pool.end();
            this._pool = null;
        }
    }
    /**
     * Access to the raw pg.Pool instance for advanced use cases.
     * Returns the underlying pg.Pool instance.
     */
    get raw() {
        return this._ensureConnected();
    }
    /**
     * Wait for the connection to be established.
     * If the connection hasn't been established yet (lazy connection), this will
     * warm the connection by acquiring and releasing a client.
     * If reconnection is in progress, waits for it to complete.
     *
     * @param timeoutMs - Optional timeout in milliseconds
     * @throws {ConnectionClosedError} If the pool has been closed or connection fails
     */
    async waitForConnection(timeoutMs) {
        if (this._connected && this._pool) {
            return;
        }
        if (this._closed) {
            throw new ConnectionClosedError({
                message: 'Pool has been closed',
            });
        }
        const connectOperation = async () => {
            // Wait for preconnect if in progress
            if (this._connectPromise) {
                try {
                    await this._connectPromise;
                }
                catch (err) {
                    this._connectPromise = null;
                    throw err;
                }
                this._connectPromise = null;
            }
            // Wait for reconnection if in progress
            if (this._reconnecting && this._reconnectPromise) {
                await this._reconnectPromise;
            }
            // If still not connected, warm the connection
            if (!this._connected && this._pool) {
                await this._warmConnection();
            }
            if (!this._connected || !this._pool) {
                throw new ConnectionClosedError({
                    message: 'Failed to establish connection',
                });
            }
        };
        if (timeoutMs !== undefined) {
            let timerId;
            const timeoutPromise = new Promise((_, reject) => {
                timerId = setTimeout(() => reject(new QueryTimeoutError({
                    timeoutMs,
                })), timeoutMs);
            });
            try {
                await Promise.race([connectOperation(), timeoutPromise]);
            }
            finally {
                if (timerId !== undefined) {
                    clearTimeout(timerId);
                }
            }
        }
        else {
            await connectOperation();
        }
    }
    /**
     * Registers signal handlers to detect application shutdown.
     * When shutdown is detected, reconnection is disabled.
     */
    _registerShutdownHandlers() {
        const shutdownHandler = () => {
            this._shuttingDown = true;
        };
        const signals = ['SIGTERM', 'SIGINT'];
        for (const signal of signals) {
            process.on(signal, shutdownHandler);
            this._signalHandlers.push({ signal, handler: shutdownHandler });
        }
    }
    /**
     * Removes signal handlers registered for shutdown detection.
     */
    _removeShutdownHandlers() {
        for (const { signal, handler } of this._signalHandlers) {
            process.off(signal, handler);
        }
        this._signalHandlers = [];
    }
    /**
     * Initializes the pg.Pool instance.
     */
    _initializePool() {
        if (this._closed || this._pool) {
            return;
        }
        const { reconnect: _reconnect, preconnect: _preconnect, onclose: _onclose, onreconnect: _onreconnect, onreconnected: _onreconnected, onreconnectfailed: _onreconnectfailed, ...pgConfig } = this._config;
        const poolConfig = { ...pgConfig };
        if (!poolConfig.connectionString) {
            poolConfig.connectionString = process.env.DATABASE_URL;
        }
        // Normalize sslmode=require to sslmode=verify-full to suppress pg v8
        // deprecation warning. pg currently treats 'require' as 'verify-full'
        // but warns that v9 will use weaker libpq semantics. Since we want the
        // stronger behavior, explicitly use verify-full.
        if (poolConfig.connectionString) {
            try {
                const parsed = new URL(poolConfig.connectionString);
                const sslmode = parsed.searchParams.get('sslmode');
                if (sslmode === 'require') {
                    parsed.searchParams.set('sslmode', 'verify-full');
                    poolConfig.connectionString = parsed.toString();
                }
            }
            catch {
                // Not a parseable URL — leave as-is
            }
        }
        this._poolConfig = poolConfig;
        this._pool = new pg.Pool(poolConfig);
        this._pool.on('connect', (client) => {
            this.emit('connect', client);
        });
        this._pool.on('acquire', (client) => {
            this.emit('acquire', client);
        });
        this._pool.on('release', (client) => {
            this.emit('release', client);
        });
        this._pool.on('remove', (client) => {
            this.emit('remove', client);
        });
        this._pool.on('error', (err, client) => {
            this._handleIdleClientError(err, client);
        });
    }
    /**
     * Warms the connection by acquiring and releasing a client.
     * This verifies the pool can connect to the database.
     */
    async _warmConnection() {
        if (this._closed || this._connected) {
            return;
        }
        if (!this._pool) {
            this._initializePool();
        }
        // Acquire a client to verify connectivity
        const client = await this._pool.connect();
        client.release();
        this._connected = true;
        this._stats.totalConnections++;
        this._stats.lastConnectedAt = new Date();
    }
    /**
     * Handles idle-client errors from the underlying pg.Pool.
     *
     * pg.Pool emits `error` when a connected but idle client is dropped
     * (proxy idle timeout, server restart, network blip). The pool already
     * removes that client; remaining clients stay usable and the next
     * checkout opens a replacement. Treat this as a self-heal signal, not
     * pool death — do not mark the pool disconnected or tear it down.
     *
     * Emitting `error` with zero listeners crashes Node/Bun, so only forward
     * when a consumer has attached a listener.
     */
    _handleIdleClientError(error, client) {
        if (this.listenerCount('error') > 0) {
            this.emit('error', error, client);
        }
    }
    /**
     * Ensures the pool is initialized and returns it.
     */
    _ensureConnected() {
        if (this._closed) {
            throw new ConnectionClosedError({
                message: 'Pool has been closed',
            });
        }
        if (!this._pool) {
            throw new ConnectionClosedError({
                message: 'Pool not initialized',
                wasReconnecting: this._reconnecting,
            });
        }
        return this._pool;
    }
    /**
     * Ensures the pool is connected and returns it.
     * If reconnection is in progress, waits for it to complete.
     * If connection hasn't been established yet, warms it first.
     */
    async _ensureConnectedAsync() {
        if (this._closed) {
            throw new ConnectionClosedError({
                message: 'Pool has been closed',
            });
        }
        // If preconnect is in progress, wait for it
        if (this._connectPromise) {
            try {
                await this._connectPromise;
            }
            catch (err) {
                this._connectPromise = null;
                throw err;
            }
            this._connectPromise = null;
        }
        // If reconnection is in progress, wait for it to complete
        if (this._reconnecting && this._reconnectPromise) {
            await this._reconnectPromise;
        }
        if (!this._pool) {
            throw new ConnectionClosedError({
                message: 'Pool not initialized',
                wasReconnecting: false,
            });
        }
        // If not yet connected, warm the connection
        if (!this._connected) {
            await this._warmConnection();
        }
        return this._pool;
    }
    /**
     * Executes an operation with retry logic for retryable errors.
     * Waits for reconnection if one is in progress.
     */
    async _executeWithRetry(operation, maxRetries = 3) {
        let lastError;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                // Wait for preconnect if in progress
                if (this._connectPromise) {
                    try {
                        await this._connectPromise;
                    }
                    catch (err) {
                        this._connectPromise = null;
                        throw err;
                    }
                    this._connectPromise = null;
                }
                // Wait for reconnection if in progress
                if (this._reconnecting && this._reconnectPromise) {
                    await this._reconnectPromise;
                }
                if (!this._pool) {
                    throw new ConnectionClosedError({
                        message: 'Pool not initialized',
                        wasReconnecting: this._reconnecting,
                    });
                }
                // If not yet connected, warm the connection
                if (!this._connected) {
                    await this._warmConnection();
                }
                return await operation();
            }
            catch (error) {
                lastError = error instanceof Error ? error : new Error(String(error));
                // If it's a retryable error and we have retries left, wait and retry
                if (isRetryableError(error) && attempt < maxRetries) {
                    // Wait for reconnection to complete if it started
                    if (this._reconnecting && this._reconnectPromise) {
                        try {
                            await this._reconnectPromise;
                        }
                        catch {
                            // Reconnection failed, will throw below
                        }
                    }
                    continue;
                }
                throw error;
            }
        }
        throw lastError;
    }
}
/**
 * Creates a new PostgresPool.
 *
 * @param config - Connection configuration
 * @param options - Pool creation options. Set `shared: true` during Bun hot reload
 *                  development to reuse one pool across module re-evaluations.
 * @returns A PostgresPool instance
 */
export function createPool(config, options) {
    if (options?.shared && isHotReloadEnabled()) {
        const key = computePoolHotReloadKey(config);
        const cached = getSharedHotReloadPool(key);
        if (cached) {
            return cached;
        }
    }
    return new PostgresPool(config);
}
/**
 * Alias for PostgresPool for convenient imports.
 */
export { PostgresPool as Pool };
//# sourceMappingURL=pool.js.map