/**
 * Database Configuration
 * Database connections and pooling settings
 * Sealed namespace for immutability
 */
import { Cloudflare } from './cloudflare.js';
import { Env } from './env.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { StartupConfigFile, StartupConfigFileRegistry } from '../runtime/StartupConfigFileRegistry.js';
const isNodeProcess = () => {
    return typeof process !== 'undefined' && typeof process.cwd === 'function';
};
const readEnvString = (key, fallback = '') => {
    const anyEnv = Env;
    const fromEnv = typeof anyEnv.get === 'function' ? anyEnv.get(key, fallback) : fallback;
    if (typeof fromEnv === 'string' && fromEnv.trim() !== '')
        return fromEnv;
    if (typeof process !== 'undefined') {
        const raw = process.env?.[key];
        if (typeof raw === 'string' && raw.trim() !== '')
            return raw;
    }
    return fromEnv ?? '';
};
const readWorkersEnvString = (key) => {
    const workerValue = Cloudflare.getWorkersVar(key);
    if (workerValue !== null && workerValue.trim() !== '')
        return workerValue;
    return '';
};
const readWorkersFallbackString = (workersKey, fallbackKey) => {
    const workerValue = readWorkersEnvString(workersKey);
    if (workerValue.trim() !== '')
        return workerValue;
    // Also check if the fallback key is present in the Workers bindings (e.g. DB_PASSWORD)
    const fallbackWorkerValue = readWorkersEnvString(fallbackKey);
    if (fallbackWorkerValue.trim() !== '')
        return fallbackWorkerValue;
    return readEnvString(fallbackKey, '');
};
const readWorkersFallbackInt = (workersKey, fallbackKey, fallback) => {
    const raw = readWorkersFallbackString(workersKey, fallbackKey);
    if (raw.trim() === '')
        return fallback;
    const parsed = Number.parseInt(raw, 10);
    return Number.isFinite(parsed) ? parsed : fallback;
};
const isExplicitEnvValue = (key) => {
    if (!isNodeProcess())
        return false;
    const direct = Env[key];
    if (typeof direct === 'string' && direct.trim() !== '')
        return true;
    const raw = process.env[key] ?? Env.get(key, '');
    return typeof raw === 'string' && raw.trim() !== '';
};
const looksLikeSqliteFilePath = (value) => {
    const v = String(value ?? '').trim();
    if (v === '')
        return false;
    if (v === ':memory:')
        return true;
    // Heuristic: if it's a path, has an extension, or is explicitly relative.
    if (v.includes('/') || v.includes('\\'))
        return true;
    if (v.startsWith('.'))
        return true;
    if (v.startsWith('~'))
        return true;
    if (v.endsWith('.sqlite') || v.endsWith('.db'))
        return true;
    // Otherwise it's likely a placeholder name (e.g. "zintrust"), not a file path.
    return false;
};
const toSafeDbBasename = (raw) => {
    const trimmed = raw.trim();
    if (trimmed === '')
        return 'zintrust';
    // Conservative filename allowlist: letters/numbers/_/-, collapse everything else.
    // Avoid regexes that can exhibit catastrophic backtracking; do deterministic collapsing/trimming.
    const collapsed = trimmed.toLowerCase().replaceAll(/[^a-z0-9_-]+/g, '-');
    // Collapse consecutive hyphens deterministically without regex backtracking.
    let normalized = '';
    let prevHyphen = false;
    for (const element of collapsed) {
        const ch = element;
        if (ch === '-') {
            if (!prevHyphen) {
                normalized += ch;
                prevHyphen = true;
            }
        }
        else {
            normalized += ch;
            prevHyphen = false;
        }
    }
    // Trim leading/trailing hyphens deterministically without regex.
    let start = 0;
    let end = normalized.length;
    while (start < end && normalized.charAt(start) === '-')
        start++;
    while (end > start && normalized.charAt(end - 1) === '-')
        end--;
    const result = normalized.slice(start, end);
    return result === '' ? 'zintrust' : result;
};
const resolveSqliteDefaultBasename = () => {
    const service = typeof Env.SERVICE_NAME === 'string' && Env.SERVICE_NAME.trim() !== ''
        ? Env.SERVICE_NAME
        : readEnvString('SERVICE_NAME', '');
    if (service.trim() !== '')
        return toSafeDbBasename(service);
    const app = typeof Env.APP_NAME === 'string' && Env.APP_NAME.trim() !== ''
        ? Env.APP_NAME
        : readEnvString('APP_NAME', '');
    if (app.trim() !== '')
        return toSafeDbBasename(app);
    return 'zintrust';
};
const resolveDefaultSqliteDatabasePath = () => {
    // Respect explicit sqlite *file path* configuration.
    // Note: we intentionally treat plain names like "zintrust" as placeholders, not file paths,
    // to avoid creating stray DB files in the project root.
    if (isExplicitEnvValue('DB_DATABASE') || isExplicitEnvValue('DB_PATH')) {
        const configured = Env.DB_DATABASE;
        if (looksLikeSqliteFilePath(configured))
            return configured;
    }
    // Only change the default behavior for dev/testing; production should be explicit.
    if (!isNodeProcess() || Env.NODE_ENV === 'production')
        return Env.DB_DATABASE;
    // Store dev sqlite files in project-local metadata folder.
    // Note: the SQLite adapter ensures the parent directory exists.
    const base = resolveSqliteDefaultBasename();
    return `.zintrust/dbs/${base}.sqlite`;
};
const hasOwn = (obj, key) => {
    return Object.hasOwn(obj, key);
};
const parseReadHosts = (raw) => {
    const list = String(raw ?? '')
        .split(',')
        .map((v) => v.trim())
        .filter((v) => v.length > 0);
    return list.length > 0 ? list : undefined;
};
const normalizeReadHosts = (primaryHost, hosts) => {
    if (hosts === undefined || hosts.length === 0)
        return undefined;
    const primary = String(primaryHost ?? '').trim();
    const filtered = hosts.filter((host) => host !== '' && host !== primary);
    return filtered.length > 0 ? filtered : undefined;
};
const getDefaultConnection = (connections) => {
    const envSelectedRaw = Env.get('DB_CONNECTION', '');
    const value = String(envSelectedRaw ?? '').trim();
    if (value.length > 0 && hasOwn(connections, value))
        return value;
    if (envSelectedRaw.trim().length > 0) {
        throw ErrorFactory.createConfigError(`Database connection not configured: ${value}`);
    }
    return hasOwn(connections, 'sqlite') ? 'sqlite' : (Object.keys(connections)[0] ?? 'sqlite');
};
const getDatabaseConnection = (config) => {
    const connName = config.default;
    const resolved = config.connections[connName];
    if (resolved !== undefined)
        return resolved;
    // Backwards-compatible fallback.
    const sqliteFallback = config.connections['sqlite'];
    if (sqliteFallback !== undefined)
        return sqliteFallback;
    const first = Object.values(config.connections)[0];
    if (first !== undefined)
        return first;
    throw ErrorFactory.createConfigError(`No database connections are configured (default='${connName}').`);
};
const connections = {
    sqlite: {
        driver: 'sqlite',
        database: resolveDefaultSqliteDatabasePath(),
        migrations: 'database/migrations',
    },
    d1: {
        driver: 'd1',
    },
    'd1-remote': {
        driver: 'd1-remote',
    },
    postgresql: {
        driver: 'postgresql',
        host: readWorkersFallbackString('WORKERS_PG_HOST', 'DB_HOST') || Env.DB_HOST,
        port: readWorkersFallbackInt('WORKERS_PG_PORT', 'DB_PORT_POSTGRESQL', Env.DB_PORT_POSTGRESQL),
        database: readWorkersFallbackString('WORKERS_PG_DATABASE', 'DB_DATABASE_POSTGRESQL') ||
            Env.DB_DATABASE_POSTGRESQL,
        username: readWorkersFallbackString('WORKERS_PG_USER', 'DB_USERNAME_POSTGRESQL') ||
            Env.DB_USERNAME_POSTGRESQL,
        password: readWorkersFallbackString('WORKERS_PG_PASSWORD', 'DB_PASSWORD_POSTGRESQL') ||
            Env.DB_PASSWORD_POSTGRESQL,
        ssl: Env.getBool('DB_SSL', false),
        readHosts: normalizeReadHosts(readWorkersFallbackString('WORKERS_PG_HOST', 'DB_HOST') || Env.DB_HOST, parseReadHosts(readWorkersFallbackString('WORKERS_PG_READ_HOSTS', 'DB_READ_HOSTS_POSTGRESQL') ||
            Env.DB_READ_HOSTS_POSTGRESQL)),
        pooling: {
            enabled: Env.getBool('DB_POOLING', true),
            min: Env.getInt('DB_POOL_MIN', 5),
            max: Env.getInt('DB_POOL_MAX', 20),
            idleTimeout: Env.getInt('DB_IDLE_TIMEOUT', 30000),
            connectionTimeout: Env.getInt('DB_CONNECTION_TIMEOUT', 10000),
        },
    },
    mysql: {
        driver: 'mysql',
        host: readWorkersFallbackString('WORKERS_MYSQL_HOST', 'DB_HOST') || Env.DB_HOST,
        port: readWorkersFallbackInt('WORKERS_MYSQL_PORT', 'DB_PORT', Env.DB_PORT),
        database: readWorkersFallbackString('WORKERS_MYSQL_DATABASE', 'DB_DATABASE') || Env.DB_DATABASE,
        username: readWorkersFallbackString('WORKERS_MYSQL_USER', 'DB_USERNAME') || Env.DB_USERNAME,
        password: readWorkersFallbackString('WORKERS_MYSQL_PASSWORD', 'DB_PASSWORD') || Env.DB_PASSWORD,
        readHosts: normalizeReadHosts(readWorkersFallbackString('WORKERS_MYSQL_HOST', 'DB_HOST') || Env.DB_HOST, parseReadHosts(readWorkersFallbackString('WORKERS_MYSQL_READ_HOSTS', 'DB_READ_HOSTS') || Env.DB_READ_HOSTS)),
        pooling: {
            enabled: Env.getBool('DB_POOLING', true),
            min: Env.getInt('DB_POOL_MIN', 5),
            max: Env.getInt('DB_POOL_MAX', 20),
        },
    },
    sqlserver: {
        driver: 'sqlserver',
        host: Env.DB_HOST_MSSQL,
        port: Env.DB_PORT_MSSQL,
        database: Env.DB_DATABASE_MSSQL,
        username: Env.DB_USERNAME_MSSQL,
        password: Env.DB_PASSWORD_MSSQL,
        readHosts: normalizeReadHosts(Env.DB_HOST_MSSQL, parseReadHosts(Env.DB_READ_HOSTS_MSSQL)),
    },
};
const createDatabaseConfig = () => {
    const overrides = StartupConfigFileRegistry.get(StartupConfigFile.Database) ?? {};
    const mergedConnections = {
        ...connections,
        ...overrides.connections,
    };
    const baseLogging = {
        enabled: Env.getBool('DB_LOG_QUERIES', Env.DEBUG),
        level: Env.get('DB_LOG_LEVEL', 'debug'),
    };
    const baseMigrations = {
        directory: 'database/migrations',
        extension: Env.get('DB_MIGRATION_EXT', '.ts'),
    };
    const baseSeeders = {
        directory: 'database/seeders',
    };
    const mergedDefault = typeof overrides.default === 'string' && overrides.default.trim() !== ''
        ? overrides.default.trim()
        : getDefaultConnection(mergedConnections);
    const databaseConfigObj = {
        /**
         * Default database connection
         */
        default: mergedDefault,
        /**
         * Database connections
         */
        connections: mergedConnections,
        /**
         * Get current connection config
         */
        getConnection() {
            return getDatabaseConnection(this);
        },
        /**
         * Enable query logging
         */
        logging: {
            ...baseLogging,
            ...overrides.logging,
        },
        /**
         * Migration settings
         */
        migrations: {
            ...baseMigrations,
            ...overrides.migrations,
        },
        /**
         * Seeding settings
         */
        seeders: {
            ...baseSeeders,
            ...overrides.seeders,
        },
    };
    return Object.freeze(databaseConfigObj);
};
let cached = null;
const proxyTarget = {};
const ensureDatabaseConfig = () => {
    if (cached)
        return cached;
    cached = createDatabaseConfig();
    try {
        Object.defineProperties(proxyTarget, Object.getOwnPropertyDescriptors(cached));
    }
    catch {
        // best-effort
    }
    return cached;
};
export const databaseConfig = new Proxy(proxyTarget, {
    get(_target, prop) {
        return ensureDatabaseConfig()[prop];
    },
    ownKeys() {
        ensureDatabaseConfig();
        return Reflect.ownKeys(proxyTarget);
    },
    getOwnPropertyDescriptor(_target, prop) {
        ensureDatabaseConfig();
        return Object.getOwnPropertyDescriptor(proxyTarget, prop);
    },
});
