/* eslint-disable no-await-in-loop */
/* eslint-disable @typescript-eslint/require-await */
/**
 * Persistent Connection Manager for ZinTrust Framework
 * Handles database connections across different runtime environments
 * Supports: PostgreSQL, MySQL, SQL Server with connection pooling for Lambda
 */
import { Env } from '../config/env.js';
import { Logger } from '../config/logger.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { D1Adapter } from './adapters/D1Adapter.js';
import { D1RemoteAdapter } from './adapters/D1RemoteAdapter.js';
import { MySQLAdapter } from './adapters/MySQLAdapter.js';
import { PostgreSQLAdapter } from './adapters/PostgreSQLAdapter.js';
import { SQLiteAdapter } from './adapters/SQLiteAdapter.js';
import { SQLServerAdapter } from './adapters/SQLServerAdapter.js';
import { DatabaseAdapterRegistry } from './DatabaseAdapterRegistry.js';
let instance;
/**
 * Close a specific connection
 */
const closeConnection = async (conn) => {
    if (isDatabaseAdapter(conn)) {
        await conn.disconnect();
    }
};
/**
 * Test if connection is still alive
 */
const testConnection = async (_config, conn) => {
    if (!isDatabaseAdapter(conn))
        return false;
    if (!conn.isConnected())
        return false;
    try {
        await conn.ping();
        return true;
    }
    catch (error) {
        Logger.warn('Connection health check failed', error);
        return false;
    }
};
/**
 * Update connection usage metrics
 */
const updateConnectionUsage = (connectionPool, id) => {
    const entry = connectionPool.get(id); // O(1) lookup instead of O(n) find
    if (entry !== undefined) {
        entry.lastUsedAt = Date.now();
        entry.queryCount++;
        entry.isActive = true;
    }
};
const isDatabaseAdapter = (value) => {
    if (typeof value !== 'object' || value === null)
        return false;
    const candidate = value;
    return (typeof candidate.connect === 'function' &&
        typeof candidate.disconnect === 'function' &&
        typeof candidate.ping === 'function' &&
        typeof candidate.isConnected === 'function');
};
const createAdapterFromConfig = (config) => {
    if (config.adapter === 'aurora-data-api') {
        throw ErrorFactory.createConfigError('Aurora Data API connections should be created via getAuroraDataApiConnection()');
    }
    const driver = config.adapter;
    const adapterConfig = {
        driver,
        database: config.database,
        host: config.host,
        port: config.port,
        username: config.username,
        password: config.password,
    };
    const registered = DatabaseAdapterRegistry.get(driver);
    if (registered !== undefined) {
        return registered(adapterConfig);
    }
    switch (driver) {
        case 'postgresql':
            return PostgreSQLAdapter.create(adapterConfig);
        case 'mysql':
            return MySQLAdapter.create(adapterConfig);
        case 'sqlserver':
            return SQLServerAdapter.create(adapterConfig);
        case 'd1':
            return D1Adapter.create(adapterConfig);
        case 'd1-remote':
            return D1RemoteAdapter.create(adapterConfig);
        case 'sqlite':
        default:
            return SQLiteAdapter.create(adapterConfig);
    }
};
/**
 * Create new database connection
 */
const createConnection = async (config, id) => {
    Logger.info(`Creating ${config.adapter} connection (${id}) to ${config.host}:${config.port}`);
    if (config.adapter === 'aurora-data-api') {
        throw ErrorFactory.createConfigError('Aurora Data API connections should be created via getAuroraDataApiConnection()');
    }
    const adapter = createAdapterFromConfig(config);
    await adapter.connect();
    return adapter;
};
/**
 * Create Aurora Data API connection
 */
function isMissingEsmPackage(error, packageName) {
    if (typeof error !== 'object' || error === null)
        return false;
    const maybe = error;
    const code = typeof maybe.code === 'string' ? maybe.code : '';
    const message = typeof maybe.message === 'string' ? maybe.message : '';
    if (code === 'ERR_MODULE_NOT_FOUND' && message.length === 0)
        return true;
    if (code === 'ERR_MODULE_NOT_FOUND' && message.includes(`'${packageName}'`))
        return true;
    if (message.includes(`Cannot find package '${packageName}'`))
        return true;
    return false;
}
async function importOptionalModule(modulePath) {
    return import(modulePath);
}
const loadClientRdsDataModule = async () => {
    try {
        return (await importOptionalModule('@zintrust/client-rds-data'));
    }
    catch (error) {
        if (isMissingEsmPackage(error, '@zintrust/client-rds-data')) {
            throw ErrorFactory.createConfigError("Aurora Data API requires '@zintrust/client-rds-data' (install the package to enable AWS Data API support).");
        }
        throw ErrorFactory.createTryCatchError('Failed to load @zintrust/client-rds-data', {
            cause: error,
        });
    }
};
const createAuroraDataApiConnection = () => {
    const getClient = async () => {
        const mod = await loadClientRdsDataModule();
        return mod.getRdsDataClient(Env.AWS_REGION);
    };
    const resourceArn = Env.get('AURORA_RESOURCE_ARN');
    const secretArn = Env.get('AURORA_SECRET_ARN');
    const database = Env.get('AURORA_DATABASE', Env.DB_DATABASE);
    const assertConfig = () => {
        if (resourceArn.length === 0 || secretArn.length === 0) {
            throw ErrorFactory.createConfigError('Aurora Data API requires AURORA_RESOURCE_ARN and AURORA_SECRET_ARN env vars');
        }
    };
    const executeStatement = async (sql, params) => {
        assertConfig();
        const client = await getClient();
        const input = {
            resourceArn,
            secretArn,
            database,
            sql,
            parameters: (params ?? []).map((value) => ({ value: { stringValue: String(value) } })),
        };
        const response = (await client.executeStatement(input));
        return {
            numberOfRecordsUpdated: response.numberOfRecordsUpdated ?? 0,
            records: response.records ?? [],
        };
    };
    return {
        execute: executeStatement,
        batch: async (statements) => {
            const results = [];
            for (const statement of statements) {
                const result = await executeStatement(statement.sql, statement.params);
                results.push(result);
            }
            return results;
        },
    };
};
/**
 * Get healthy existing connection if available
 */
const getHealthyExistingConnection = async (config, state, id) => {
    if (!state.connections.has(id))
        return null;
    const conn = state.connections.get(id);
    if (conn !== undefined && conn !== null && (await testConnection(config, conn))) {
        updateConnectionUsage(state.connectionPool, id);
        return conn;
    }
    state.connections.delete(id);
    state.connectionPool.delete(id); // O(1) instead of O(n) filter
    return null;
};
/**
 * Find an idle connection in the pool
 */
const findIdleConnection = (state) => {
    let lruEntry = null;
    let lruTime = Date.now();
    // Iterate through Map values to find least recently used idle connection
    for (const poolEntry of state.connectionPool.values()) {
        if (!poolEntry.isActive && poolEntry.lastUsedAt < lruTime) {
            lruTime = poolEntry.lastUsedAt;
            lruEntry = poolEntry;
        }
    }
    if (lruEntry === null)
        return null;
    updateConnectionUsage(state.connectionPool, lruEntry.id);
    const conn = state.connections.get(lruEntry.id);
    return conn ?? null;
};
/**
 * Wait for a connection to become available
 */
const waitForIdleConnection = async (state) => {
    return new Promise((resolve, reject) => {
        let timeoutId;
        const cleanup = () => {
            if (timeoutId !== undefined) {
                globalThis.clearTimeout(timeoutId);
                timeoutId = undefined;
            }
        };
        const waiter = {
            resolve: (conn) => {
                cleanup();
                resolve(conn);
            },
            reject: (err) => {
                cleanup();
                reject(err);
            },
            timeoutId,
            cleanup,
        };
        timeoutId = globalThis.setTimeout(() => {
            state.waiters = state.waiters.filter((entry) => entry !== waiter);
            cleanup();
            reject(ErrorFactory.createConnectionError('Connection pool exhausted - timeout waiting for available connection'));
        }, 30000);
        waiter.timeoutId = timeoutId;
        if (isUnrefableTimer(timeoutId)) {
            timeoutId.unref();
        }
        state.waiters.push(waiter);
    });
};
function isUnrefableTimer(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    return 'unref' in value && typeof value.unref === 'function';
}
/**
 * Get or reuse a connection when at max capacity
 */
const getOrReuseConnection = async (state) => {
    const idle = findIdleConnection(state);
    if (idle !== null)
        return idle;
    return waitForIdleConnection(state);
};
/**
 * Periodically clean up idle connections
 */
const startIdleConnectionCleanup = (state, idleTimeout) => {
    if (state.cleanupInterval) {
        clearInterval(state.cleanupInterval);
    }
    state.cleanupInterval = setInterval(() => {
        const now = Date.now();
        const toRemove = [];
        for (const [id, poolEntry] of state.connectionPool.entries()) {
            if (!poolEntry.isActive && now - poolEntry.lastUsedAt > idleTimeout) {
                toRemove.push(id);
            }
        }
        for (const id of toRemove) {
            const conn = state.connections.get(id);
            closeConnection(conn).catch((err) => Logger.error(`Failed to close idle connection ${id}:`, err));
            state.connections.delete(id);
            state.connectionPool.delete(id); // O(1) instead of O(n) filter
            Logger.info(`Removed idle connection: ${id}`);
        }
    }, 300000); // Every 5 minutes
    if (isUnrefableTimer(state.cleanupInterval)) {
        state.cleanupInterval.unref();
    }
};
/**
 * Create and register a new connection
 */
const createNewConnection = async (config, state, id) => {
    const connection = await createConnection(config, id);
    state.connections.set(id, connection);
    state.connectionPool.set(id, {
        // O(1) instead of O(n) push
        id,
        adapter: config.adapter,
        createdAt: Date.now(),
        lastUsedAt: Date.now(),
        isActive: true,
        queryCount: 0,
    });
    return connection;
};
/**
 * Close all connections (graceful shutdown)
 */
const closeAllConnections = async (state) => {
    if (state.cleanupInterval) {
        clearInterval(state.cleanupInterval);
        state.cleanupInterval = undefined;
    }
    if (state.waiters.length > 0) {
        const err = ErrorFactory.createConnectionError('Connection manager shutting down');
        for (const waiter of state.waiters) {
            if (waiter.timeoutId !== undefined)
                clearTimeout(waiter.timeoutId);
            waiter.reject(err);
        }
        state.waiters = [];
    }
    for (const [id, conn] of state.connections.entries()) {
        try {
            await closeConnection(conn);
        }
        catch (error) {
            ErrorFactory.createConnectionError(`Failed to close connection ${id}:`, error);
        }
    }
    state.connections.clear();
    state.connectionPool.clear(); // Use Map.clear() instead of array reset
};
/**
 * Get connection pool statistics
 */
const getPoolStatistics = (state) => {
    let active = 0;
    let idle = 0;
    // Iterate through Map values instead of filtering array
    for (const poolEntry of state.connectionPool.values()) {
        if (poolEntry.isActive) {
            active++;
        }
        else {
            idle++;
        }
    }
    return {
        total: state.connectionPool.size, // Use Map.size instead of array length
        active,
        idle,
        queued: state.waiters.length,
    };
};
/**
 * ConnectionManager implementation
 * Refactored to Functional Object pattern
 */
const ConnectionManagerImpl = {
    /**
     * Create a new connection manager instance
     */
    create(config) {
        const state = {
            connections: new Map(),
            connectionPool: new Map(), // Changed from Array to Map
            waiters: [],
        };
        const maxConnections = config.maxConnections ?? 10;
        const idleTimeout = config.idleTimeout ?? 900000; // 15 minutes
        // Cleanup idle connections every 5 minutes
        startIdleConnectionCleanup(state, idleTimeout);
        return {
            /**
             * Get or create database connection
             */
            async getConnection(id = 'default') {
                const existing = await getHealthyExistingConnection(config, state, id);
                if (existing !== null)
                    return existing;
                if (state.connectionPool.size < maxConnections) {
                    // Use Map.size instead of array length
                    return createNewConnection(config, state, id);
                }
                return getOrReuseConnection(state);
            },
            /**
             * Release connection back to pool (but keep persistent)
             */
            async releaseConnection(connectionId = 'default') {
                const poolEntry = state.connectionPool.get(connectionId); // O(1) instead of O(n) find
                if (poolEntry === undefined)
                    return;
                poolEntry.isActive = false;
                poolEntry.lastUsedAt = Date.now();
                if (state.waiters.length === 0)
                    return;
                const waiter = state.waiters.shift();
                if (waiter === undefined)
                    return;
                waiter.cleanup();
                const conn = state.connections.get(connectionId);
                if (conn === undefined) {
                    waiter.reject(ErrorFactory.createConnectionError('Released connection not found'));
                    return;
                }
                updateConnectionUsage(state.connectionPool, connectionId);
                waiter.resolve(conn);
            },
            /**
             * Close all connections (graceful shutdown)
             */
            async closeAll() {
                return closeAllConnections(state);
            },
            /**
             * Get connection pool statistics
             */
            getPoolStats() {
                return getPoolStatistics(state);
            },
            /**
             * Enable RDS Proxy for connection pooling
             */
            async enableRdsProxy(endpoint) {
                config.enableRdsProxy = true;
                config.rdsProxyEndpoint = endpoint;
                config.host = endpoint;
                Logger.info(`RDS Proxy enabled: ${endpoint}`);
            },
            /**
             * Use Aurora Data API for serverless queries (no persistent connection)
             */
            async getAuroraDataApiConnection() {
                return createAuroraDataApiConnection();
            },
        };
    },
};
/**
 * Manages database connections across Lambda warm invocations
 * Reuses connections to reduce cold start impact and connection overhead
 * Sealed namespace object following Pattern 2
 *
 * @see FRAMEWORK_REFACTOR_FUNCTION_PATTERN.md for Pattern 2 details
 */
export const ConnectionManager = Object.freeze({
    /**
     * Get or create singleton instance
     */
    getInstance(config) {
        if (instance === undefined && config !== undefined) {
            instance = ConnectionManagerImpl.create(config);
        }
        if (instance === undefined) {
            throw ErrorFactory.createConfigError('ConnectionManager not initialized. Call getInstance(config) first.');
        }
        return instance;
    },
    /**
     * Shutdown connection manager if it has been initialized
     * Safe to call even if no instance exists
     */
    async shutdownIfInitialized() {
        if (instance !== undefined) {
            try {
                await instance.closeAll();
                instance = undefined;
            }
            catch (err) {
                Logger.error('Error while shutting down ConnectionManager:', err);
            }
        }
    },
    /**
     * Get or create database connection
     */
    async getConnection(id = 'default') {
        return this.getInstance().getConnection(id);
    },
    /**
     * Release connection back to pool (but keep persistent)
     */
    async releaseConnection(connectionId = 'default') {
        return this.getInstance().releaseConnection(connectionId);
    },
    /**
     * Close all connections (graceful shutdown)
     */
    async closeAll() {
        return this.getInstance().closeAll();
    },
    /**
     * Get connection pool statistics
     */
    getPoolStats() {
        return this.getInstance().getPoolStats();
    },
    /**
     * Enable RDS Proxy for connection pooling
     */
    async enableRdsProxy(endpoint) {
        return this.getInstance().enableRdsProxy(endpoint);
    },
    /**
     * Use Aurora Data API for serverless queries (no persistent connection)
     */
    async getAuroraDataApiConnection() {
        return this.getInstance().getAuroraDataApiConnection();
    },
});
/**
 * Get database credentials from AWS Secrets Manager
 */
export async function getDatabaseSecret(secretName) {
    try {
        const mod = await loadClientRdsDataModule();
        const client = await mod.getSecretsManagerClient(Env.AWS_REGION);
        const response = await client.getSecretValue(secretName);
        const secretString = response.SecretString;
        if (secretString === undefined || secretString === null || secretString.trim().length === 0) {
            throw ErrorFactory.createConfigError('Secrets Manager returned an empty secret');
        }
        const parsed = JSON.parse(secretString);
        if (parsed.username === undefined ||
            parsed.password === undefined ||
            parsed.host === undefined ||
            parsed.port === undefined ||
            parsed.database === undefined) {
            throw ErrorFactory.createConfigError('Secrets Manager secret is missing required fields');
        }
        return {
            username: parsed.username,
            password: parsed.password,
            host: parsed.host,
            port: Number(parsed.port),
            database: parsed.database,
        };
    }
    catch (error) {
        throw ErrorFactory.createTryCatchError('Failed to fetch database secret', { cause: error });
    }
}
/**
 * Get database credentials from environment variables
 */
export function getDatabaseCredentialsFromEnv() {
    return {
        username: Env.DB_USERNAME,
        password: Env.DB_PASSWORD,
        host: Env.DB_HOST,
        port: Env.DB_PORT,
        database: Env.DB_DATABASE,
    };
}
/**
 * Secrets Manager for retrieving database credentials securely
 * Sealed namespace object following Pattern 2
 *
 * @see FRAMEWORK_REFACTOR_FUNCTION_PATTERN.md for Pattern 2 details
 */
export const SecretsHelper = Object.freeze({
    getDatabaseSecret,
    getDatabaseCredentialsFromEnv,
});
