/**
 * Database Manager
 * Central database connection management and query execution
 */
import { OpenTelemetry } from '../observability/OpenTelemetry.js';
import { Cloudflare } from '../config/cloudflare.js';
import { Env } from '../config/env.js';
import { Logger } from '../config/logger.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { EventEmitter } from '../node-singletons/events.js';
import { D1Adapter } from './adapters/D1Adapter.js';
import { D1RemoteAdapter } from './adapters/D1RemoteAdapter.js';
import { MySQLAdapter } from './adapters/MySQLAdapter.js';
import { MySQLProxyAdapter } from './adapters/MySQLProxyAdapter.js';
import { DatabaseConnectionRegistry } from './DatabaseConnectionRegistry.js';
import { PostgreSQLAdapter } from './adapters/PostgreSQLAdapter.js';
import { PostgreSQLProxyAdapter } from './adapters/PostgreSQLProxyAdapter.js';
import { SQLiteAdapter } from './adapters/SQLiteAdapter.js';
import { SQLServerAdapter } from './adapters/SQLServerAdapter.js';
import { createSqlServerProxyAdapter } from './adapters/SqlServerProxyAdapter.js';
import { DatabaseAdapterRegistry } from './DatabaseAdapterRegistry.js';
import { QueryBuilder } from './QueryBuilder.js';
/**
 * Database Manager
 * Refactored to Functional Object pattern
 */
/**
 * Create appropriate adapter based on driver
 */
const resolveMySqlProxyAdapter = (cfg) => {
    if (cfg.driver !== 'mysql')
        return null;
    const proxyUrl = Env.get('MYSQL_PROXY_URL', '').trim();
    const useProxy = Env.getBool('USE_MYSQL_PROXY', false);
    if (!useProxy)
        return null;
    if (proxyUrl.length === 0) {
        Logger.warn('[Database] USE_MYSQL_PROXY enabled but MYSQL_PROXY_URL is empty', {
            driver: cfg.driver,
        });
    }
    Logger.info('[Database] Selecting MySQL proxy adapter', {
        driver: cfg.driver,
        useMySqlProxy: useProxy,
        mysqlProxyUrlConfigured: proxyUrl.length > 0,
        mysqlProxyUrl: proxyUrl,
    });
    return MySQLProxyAdapter.create(cfg);
};
const resolvePostgresProxyAdapter = (cfg) => {
    if (cfg.driver !== 'postgresql')
        return null;
    const proxyUrl = Env.get('POSTGRES_PROXY_URL', '').trim();
    const useProxy = Env.getBool('USE_POSTGRES_PROXY', false);
    if (!useProxy)
        return null;
    if (proxyUrl.length === 0) {
        Logger.warn('[Database] USE_POSTGRES_PROXY enabled but POSTGRES_PROXY_URL is empty', {
            driver: cfg.driver,
        });
    }
    Logger.info('[Database] Selecting PostgreSQL proxy adapter', {
        driver: cfg.driver,
        usePostgresProxy: useProxy,
        postgresProxyUrlConfigured: proxyUrl.length > 0,
        postgresProxyUrl: proxyUrl,
    });
    return PostgreSQLProxyAdapter.create(cfg);
};
const resolveSqlServerProxyAdapter = (cfg) => {
    if (cfg.driver !== 'sqlserver')
        return null;
    const proxyUrl = Env.get('SQLSERVER_PROXY_URL', '').trim();
    const useProxy = Env.getBool('USE_SQLSERVER_PROXY', false);
    if (!useProxy)
        return null;
    if (proxyUrl.length === 0) {
        Logger.warn('[Database] USE_SQLSERVER_PROXY enabled but SQLSERVER_PROXY_URL is empty', {
            driver: cfg.driver,
        });
    }
    Logger.info('[Database] Selecting SQL Server proxy adapter', {
        driver: cfg.driver,
        useSqlServerProxy: useProxy,
        sqlServerProxyUrlConfigured: proxyUrl.length > 0,
        sqlServerProxyUrl: proxyUrl,
    });
    return createSqlServerProxyAdapter();
};
const resolveExplicitProxyAdapter = (cfg) => {
    // Allow proxy adapters to be forced in any runtime (Node or Workers) when
    // the feature flags are enabled.
    const mysqlProxy = resolveMySqlProxyAdapter(cfg);
    if (mysqlProxy)
        return mysqlProxy;
    const postgresProxy = resolvePostgresProxyAdapter(cfg);
    if (postgresProxy)
        return postgresProxy;
    const sqlServerProxy = resolveSqlServerProxyAdapter(cfg);
    if (sqlServerProxy)
        return sqlServerProxy;
    return null;
};
const ensureCloudflareSocketSupport = (cfg) => {
    const isSocketDriver = cfg.driver === 'postgresql' || cfg.driver === 'mysql';
    if (!isSocketDriver)
        return;
    if (Cloudflare.isCloudflareSocketsEnabled())
        return;
    Logger.warn('[Database] Cloudflare socket driver selected but sockets are disabled', {
        driver: cfg.driver,
        enableCloudflareSockets: Env.getBool('ENABLE_CLOUDFLARE_SOCKETS', false),
    });
    throw ErrorFactory.createConfigError('Cloudflare sockets are disabled. Set ENABLE_CLOUDFLARE_SOCKETS=true to use SQL adapters on Workers.');
};
const resolveWorkersAdapter = (cfg) => {
    const workersEnv = Cloudflare.getWorkersEnv();
    if (workersEnv === null)
        return null;
    Logger.debug('[Database] Resolving adapter for Cloudflare Workers runtime', {
        driver: cfg.driver,
        useMySqlProxy: Env.getBool('USE_MYSQL_PROXY', false),
        mysqlProxyUrlConfigured: Env.get('MYSQL_PROXY_URL', '').trim() !== '',
        usePostgresProxy: Env.getBool('USE_POSTGRES_PROXY', false),
        postgresProxyUrlConfigured: Env.get('POSTGRES_PROXY_URL', '').trim() !== '',
        cloudflareSocketsEnabled: Cloudflare.isCloudflareSocketsEnabled(),
        workersBindingsCount: Object.keys(workersEnv).length,
    });
    const mysqlProxy = resolveMySqlProxyAdapter(cfg);
    if (mysqlProxy)
        return mysqlProxy;
    const postgresProxy = resolvePostgresProxyAdapter(cfg);
    if (postgresProxy)
        return postgresProxy;
    ensureCloudflareSocketSupport(cfg);
    return null;
};
const createAdapter = (cfg) => {
    const explicitProxy = resolveExplicitProxyAdapter(cfg);
    if (explicitProxy)
        return explicitProxy;
    // First: Check the specific configured driver via registry
    const registered = DatabaseAdapterRegistry.get(cfg.driver);
    if (registered !== undefined) {
        return registered(cfg);
    }
    // Second: Try workers adapter resolution (only if registry lookup failed)
    const workersAdapter = resolveWorkersAdapter(cfg);
    if (workersAdapter)
        return workersAdapter;
    // Third: Fallback to direct adapter creation
    switch (cfg.driver) {
        case 'postgresql':
            return PostgreSQLAdapter.create(cfg);
        case 'mysql':
            return MySQLAdapter.create(cfg);
        case 'sqlserver':
            return SQLServerAdapter.create(cfg);
        case 'd1':
            return D1Adapter.create(cfg);
        case 'd1-remote':
            return D1RemoteAdapter.create(cfg);
        case 'sqlite':
        default:
            return SQLiteAdapter.create(cfg);
    }
};
/**
 * Initialize write and read adapters
 */
const initializeAdapters = (dbConfig) => {
    const writeAdapter = createAdapter(dbConfig);
    const readAdapters = [writeAdapter];
    return { writeAdapter, readAdapters };
};
/**
 * Execute a query with events and timing
 */
const executeQuery = async (adapter, eventEmitter, sql, parameters, method) => {
    eventEmitter.emit('before-query', sql, parameters);
    const startTime = Date.now();
    const result = await adapter[method](sql, parameters);
    const duration = Date.now() - startTime;
    eventEmitter.emit('after-query', sql, parameters, duration);
    return result.rows;
};
const executeFullQuery = async (adapter, eventEmitter, sql, parameters) => {
    eventEmitter.emit('before-query', sql, parameters);
    const startTime = Date.now();
    const result = await adapter.query(sql, parameters);
    const duration = Date.now() - startTime;
    eventEmitter.emit('after-query', sql, parameters, duration);
    return result;
};
const executeQueryOne = async (adapter, eventEmitter, sql, parameters) => {
    eventEmitter.emit('before-query', sql, parameters);
    const startTime = Date.now();
    const result = await adapter.queryOne(sql, parameters);
    const duration = Date.now() - startTime;
    eventEmitter.emit('after-query', sql, parameters, duration);
    return result;
};
const installDbMetricsIfEnabled = (dbConfig, eventEmitter) => {
    if (Env.getBool('METRICS_ENABLED', false) === false)
        return null;
    let observeDbQueryPromise = null;
    const ensureObserveDbQuery = async () => {
        if (observeDbQueryPromise !== null)
            return observeDbQueryPromise;
        observeDbQueryPromise = import('../observability/PrometheusMetrics.js')
            .then((m) => m.PrometheusMetrics.observeDbQuery)
            .catch(() => null);
        return observeDbQueryPromise;
    };
    const handler = (_sql, _params, durationMs) => {
        void ensureObserveDbQuery().then((observe) => {
            if (observe === null)
                return;
            void observe({ driver: dbConfig.driver, durationMs });
        });
    };
    eventEmitter.on('after-query', handler);
    // Return cleanup function
    return () => {
        eventEmitter.off('after-query', handler);
    };
};
const installDbTracingIfEnabled = (dbConfig, eventEmitter) => {
    if (Env.getBool('OTEL_ENABLED', false) === false)
        return null;
    const handler = (_sql, _params, durationMs) => {
        OpenTelemetry.recordDbQuerySpan({ driver: dbConfig.driver, durationMs });
    };
    eventEmitter.on('after-query', handler);
    // Return cleanup function
    return () => {
        eventEmitter.off('after-query', handler);
    };
};
/**
 * Create connect/disconnect handlers
 */
const createConnectionHandlers = (writeAdapter, readAdapters, connected, connectInFlight) => {
    return {
        async connect() {
            if (connected.value)
                return;
            const inFlight = connectInFlight.value;
            if (inFlight !== undefined) {
                await inFlight;
                return;
            }
            connectInFlight.value = (async () => {
                await writeAdapter.connect();
                await Promise.all(readAdapters
                    .filter((adapter) => adapter !== writeAdapter)
                    .map(async (adapter) => adapter.connect()));
                connected.value = true;
            })();
            try {
                await connectInFlight.value;
            }
            finally {
                connectInFlight.value = undefined;
            }
        },
        async disconnect() {
            if (connectInFlight.value !== undefined) {
                await connectInFlight.value.catch(() => {
                    // ignore
                });
            }
            await writeAdapter.disconnect();
            await Promise.all(readAdapters
                .filter((adapter) => adapter !== writeAdapter)
                .map(async (adapter) => adapter.disconnect()));
            connected.value = false;
        },
    };
};
/**
 * Create query handlers
 */
const createQueryHandlers = (writeAdapter, _readAdapters, eventEmitter, connected, db, getAdapter) => {
    let registryChecked = false;
    const assertRegistryReady = () => {
        if (registryChecked)
            return;
        registryChecked = true;
        const registry = DatabaseAdapterRegistry.list();
        if (registry.length === 0) {
            throw ErrorFactory.createConfigError('No database adapters are registered. Call DatabaseAdapterRegistry.register() during startup to register database adapters.');
        }
    };
    return {
        async query(sql, parameters = [], isRead = false) {
            if (connected.value === false)
                await db.connect();
            const adapter = getAdapter(isRead);
            assertRegistryReady();
            return executeQuery(adapter, eventEmitter, sql, parameters, 'query');
        },
        async queryOne(sql, parameters = [], isRead = false) {
            if (connected.value === false)
                await db.connect();
            const adapter = getAdapter(isRead);
            assertRegistryReady();
            return executeQueryOne(adapter, eventEmitter, sql, parameters);
        },
        async execute(sql, parameters = [], isRead = false) {
            if (connected.value === false)
                await db.connect();
            const adapter = getAdapter(isRead);
            assertRegistryReady();
            return executeFullQuery(adapter, eventEmitter, sql, parameters);
        },
        async transaction(callback) {
            if (connected.value === false)
                await db.connect();
            assertRegistryReady();
            return writeAdapter.transaction(async () => callback(db));
        },
    };
};
const setupDbInstrumentation = (dbConfig, eventEmitter) => {
    const cleanupFunctions = [];
    const metricsCleanup = installDbMetricsIfEnabled(dbConfig, eventEmitter);
    const tracingCleanup = installDbTracingIfEnabled(dbConfig, eventEmitter);
    if (metricsCleanup)
        cleanupFunctions.push(metricsCleanup);
    if (tracingCleanup)
        cleanupFunctions.push(tracingCleanup);
    return cleanupFunctions;
};
const applyQueryHandlers = (db, queryHandlers) => {
    db.query = queryHandlers.query;
    db.queryOne = queryHandlers.queryOne;
    db.execute = queryHandlers.execute;
    db.transaction = queryHandlers.transaction;
};
const createDbFacade = (input) => {
    const { dbConfig, writeAdapter, eventEmitter, connected, getAdapter, connectionHandlers, queryHandlers, cleanupFunctions, } = input;
    const db = {
        connect: connectionHandlers.connect,
        disconnect: connectionHandlers.disconnect,
        isConnected() {
            return connected.value;
        },
        query: queryHandlers.query,
        queryOne: queryHandlers.queryOne,
        execute: queryHandlers.execute,
        raw(value) {
            return { __raw: value };
        },
        transaction: queryHandlers.transaction,
        table(name) {
            return QueryBuilder.create(name, db);
        },
        onBeforeQuery: (h) => eventEmitter.on('before-query', h),
        onAfterQuery: (h) => eventEmitter.on('after-query', h),
        offBeforeQuery: (h) => eventEmitter.off('before-query', h),
        offAfterQuery: (h) => eventEmitter.off('after-query', h),
        getAdapterInstance(isRead = false) {
            return getAdapter(isRead);
        },
        getType() {
            return writeAdapter.getType();
        },
        getConfig() {
            return { ...dbConfig };
        },
        dispose() {
            // Clean up event listeners to prevent memory leaks
            for (const cleanup of cleanupFunctions) {
                cleanup();
            }
            cleanupFunctions.length = 0;
        },
    };
    return db;
};
const createDatabaseInstance = (dbConfig) => {
    const eventEmitter = new EventEmitter();
    const connected = { value: false };
    const connectInFlight = { value: undefined };
    let readIndex = 0;
    const { writeAdapter, readAdapters } = initializeAdapters(dbConfig);
    const cleanupFunctions = setupDbInstrumentation(dbConfig, eventEmitter);
    const getAdapter = (isRead = false) => {
        if (isRead === false || readAdapters.length === 0) {
            return writeAdapter;
        }
        const adapter = readAdapters[readIndex];
        if (adapter === undefined)
            return writeAdapter;
        readIndex = (readIndex + 1) % readAdapters.length;
        return adapter;
    };
    const connectionHandlers = createConnectionHandlers(writeAdapter, readAdapters, connected, connectInFlight);
    // Create temporary handlers with db as undefined, then update after db is created
    let queryHandlers = createQueryHandlers(writeAdapter, readAdapters, eventEmitter, connected, {}, getAdapter);
    const db = createDbFacade({
        dbConfig,
        writeAdapter,
        eventEmitter,
        connected,
        getAdapter,
        connectionHandlers,
        queryHandlers,
        cleanupFunctions,
    });
    // Update handlers with actual db reference for circular dependency
    queryHandlers = createQueryHandlers(writeAdapter, readAdapters, eventEmitter, connected, db, getAdapter);
    applyQueryHandlers(db, queryHandlers);
    return db;
};
export const Database = Object.freeze({
    /**
     * Create a new database instance
     */
    create(config) {
        const dbConfig = config ?? { driver: 'sqlite', database: ':memory:' };
        return createDatabaseInstance(dbConfig);
    },
});
const databaseInstances = new Map();
export const useEnsureDbConnected = async (config = undefined, connectionName = 'default') => {
    const db = useDatabase(config, connectionName);
    if (!db.isConnected()) {
        await db.connect();
    }
    return db;
};
export function useDatabase(config, connection = 'default') {
    if (databaseInstances.has(connection) === false) {
        const resolvedConfig = config ?? DatabaseConnectionRegistry.get(connection);
        if (resolvedConfig === undefined) {
            // Diagnostic logging
            Logger.error('[DEBUG] Database instances keys:', Array.from(databaseInstances.keys()));
            Logger.error('[DEBUG] Requesting connection:', connection);
            throw ErrorFactory.createConfigError(`Database connection '${connection}' is not registered. ` +
                `Call useDatabase(config, '${connection}') during startup to register it.`);
        }
        databaseInstances.set(connection, Database.create(resolvedConfig));
    }
    const instance = databaseInstances.get(connection);
    if (instance === undefined) {
        throw ErrorFactory.createConfigError(`Failed to initialize database instance: ${connection}`);
    }
    return instance;
}
export async function resetDatabase() {
    const promises = Array.from(databaseInstances.values()).map(async (instance) => {
        try {
            await instance.disconnect();
            instance.dispose();
        }
        catch {
            // Ignore errors during disconnect
        }
    });
    await Promise.all(promises);
    databaseInstances.clear();
    DatabaseConnectionRegistry.clear();
}
