/* eslint-disable @typescript-eslint/require-await */
/**
 * SQLite Database Adapter
 * Production Implementation
 */
import { databaseConfig } from '../../config/database.js';
import { FeatureFlags } from '../../config/features.js';
import { Logger } from '../../config/logger.js';
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
import { AdaptersEnum } from '../../migrations/enum/index.js';
import fs from '../../node-singletons/fs.js';
import * as path from '../../node-singletons/path.js';
import { performance } from '../../node-singletons/perf-hooks.js';
import { QueryBuilder } from '../QueryBuilder.js';
const SAFE_SQLITE_IDENTIFIER = /^[A-Za-z_]\w*$/;
const toSqliteIdentifier = (value) => {
    if (!SAFE_SQLITE_IDENTIFIER.test(value)) {
        throw ErrorFactory.createDatabaseError('Unsafe sqlite identifier');
    }
    return value;
};
const quoteSqliteIdentifier = (id) => {
    // Safe due to SAFE_SQLITE_IDENTIFIER allowlist.
    return `"${id}"`;
};
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 : '';
    // Some runners/wrappers preserve `code` but sanitize/omit the 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 importSqliteDatabaseConstructor() {
    try {
        // Avoid a literal dynamic import so bundlers (e.g. Wrangler/esbuild) don't
        // try to bundle the native sqlite driver into non-Node targets.
        const pkg = globalThis
            .__zintrustSqliteDriver;
        const mod = (await import(pkg ?? 'better-sqlite3'));
        const ctor = mod.default;
        if (typeof ctor === 'function')
            return ctor;
        // Some CJS packages may not present a `default` export under certain loaders.
        return mod;
    }
    catch (error) {
        if (isMissingEsmPackage(error, 'better-sqlite3')) {
            throw ErrorFactory.createConfigError("SQLite adapter requires the 'better-sqlite3' package (run `zin add db:sqlite` or `zin plugin install db:sqlite`).");
        }
        throw ErrorFactory.createTryCatchError('Failed to load SQLite driver', { cause: error });
    }
}
function normalizeFilename(database) {
    const value = (database ?? '').trim();
    return value.length > 0 ? value : ':memory:';
}
function isSelectQuery(sql) {
    return sql.trimStart().toLowerCase().startsWith('select');
}
function requireDb(db) {
    if (db === null)
        throw ErrorFactory.createConnectionError('Database not connected');
    return db;
}
function executeQuery(db, sql, parameters) {
    const start = performance.now();
    const stmt = db.prepare(sql);
    if (isSelectQuery(sql)) {
        const rows = stmt.all(parameters);
        if (databaseConfig.logging.enabled) {
            Logger.debug('SQLite query executed', { durationMs: performance.now() - start, sql });
        }
        return { rows, rowCount: rows.length };
    }
    const info = stmt.run(parameters);
    if (databaseConfig.logging.enabled) {
        Logger.debug('SQLite query executed', { durationMs: performance.now() - start, sql });
    }
    return { rows: [], rowCount: info.changes, lastInsertId: info.lastInsertRowid };
}
function executeRawQuery(db, sql, parameters) {
    const stmt = db.prepare(sql);
    if (isSelectQuery(sql))
        return stmt.all(parameters);
    stmt.run(parameters);
    return [];
}
async function connectSQLite(state) {
    if (state.db !== null)
        return;
    const filename = normalizeFilename(state.config.database);
    // Ensure file-backed sqlite DB directories exist (e.g. .zintrust/dbs/*.sqlite).
    if (filename !== ':memory:') {
        try {
            fs.mkdirSync(path.dirname(filename), { recursive: true });
        }
        catch (error) {
            throw ErrorFactory.createTryCatchError('Failed to create SQLite database directory', {
                filename,
                cause: error,
            });
        }
    }
    const SqliteDatabaseCtor = await importSqliteDatabaseConstructor();
    state.db = new SqliteDatabaseCtor(filename);
    // Enable WAL mode for better concurrency
    state.db.pragma('journal_mode = WAL');
    Logger.info(`✓ SQLite connected (${filename})`);
}
async function disconnectSQLite(state) {
    if (state.db === null)
        return;
    state.db.close();
    state.db = null;
    Logger.info('✓ SQLite disconnected');
}
async function querySQLite(state, sql, parameters) {
    const currentDb = requireDb(state.db);
    try {
        return executeQuery(currentDb, sql, parameters);
    }
    catch (error) {
        throw ErrorFactory.createTryCatchError('Query failed', { sql, parameters, cause: error });
    }
}
async function rawQuerySQLite(state, sql, parameters) {
    if (!FeatureFlags.isRawQueryEnabled()) {
        throw ErrorFactory.createConfigError('Raw SQL queries are disabled');
    }
    const currentDb = requireDb(state.db);
    Logger.warn(`Raw SQL Query executed: ${sql}`);
    try {
        return executeRawQuery(currentDb, sql, parameters);
    }
    catch (error) {
        throw ErrorFactory.createTryCatchError('Raw query failed', { sql, parameters, cause: error });
    }
}
async function transactionSQLite(state, adapter, callback) {
    const currentDb = requireDb(state.db);
    await adapter.query('BEGIN', []);
    try {
        const result = await callback(adapter, currentDb);
        await adapter.query('COMMIT', []);
        return result;
    }
    catch (error) {
        await adapter.query('ROLLBACK', []);
        throw ErrorFactory.createTryCatchError('Transaction failed', { cause: error });
    }
}
function createSQLiteAdapter(config) {
    const state = { db: null, config };
    const adapter = {
        async connect() {
            await connectSQLite(state);
        },
        async disconnect() {
            await disconnectSQLite(state);
        },
        async query(sql, parameters = []) {
            return querySQLite(state, sql, parameters);
        },
        async queryOne(sql, parameters = []) {
            const result = await adapter.query(sql, parameters);
            return result.rows[0] ?? null;
        },
        async ping() {
            await adapter.query(QueryBuilder.create('').select('1').toSQL(), []);
        },
        async transaction(callback) {
            return transactionSQLite(state, adapter, callback);
        },
        async rawQuery(sql, parameters = []) {
            return rawQuerySQLite(state, sql, parameters);
        },
        async ensureMigrationsTable() {
            await adapter.query(`CREATE TABLE IF NOT EXISTS migrations (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          scope TEXT NOT NULL DEFAULT 'global',
          service TEXT NOT NULL DEFAULT '',
          batch INTEGER NOT NULL,
          status TEXT NOT NULL,
          applied_at TEXT NULL,
          created_at TEXT NOT NULL,
          UNIQUE(name, scope, service)
        )`, []);
        },
        async resetSchema() {
            // Best-effort for SQLite.
            await adapter.query('PRAGMA foreign_keys = OFF', []);
            const tables = (await adapter.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []));
            await Promise.all(tables.rows.map(async (t) => {
                const name = typeof t.name === 'string' ? t.name : '';
                if (name.length === 0)
                    return;
                const tableName = toSqliteIdentifier(name);
                await adapter.query(`DROP TABLE IF EXISTS ${quoteSqliteIdentifier(tableName)}`, []);
            }));
            await adapter.query('PRAGMA foreign_keys = ON', []);
        },
        getType() {
            return AdaptersEnum.sqlite;
        },
        isConnected() {
            return Boolean(state.db);
        },
        getPlaceholder(_index) {
            return '?';
        },
    };
    return adapter;
}
export const SQLiteAdapter = Object.freeze({
    create: createSQLiteAdapter,
});
