import { ErrorFactory } from '../../exceptions/ZintrustError.js';
import * as fs from '../../node-singletons/fs.js';
import * as path from '../../node-singletons/path.js';
import { AdaptersEnum } from '../../migrations/enum/index.js';
import { MigrationDiscovery } from '../../migrations/MigrationDiscovery.js';
import { MigrationLoader } from '../../migrations/MigrationLoader.js';
import { BaseAdapter } from '../../orm/DatabaseAdapter.js';
const RESOLVED_VOID = Promise.resolve();
const ensureDir = (dir) => {
    if (fs.existsSync(dir))
        return;
    fs.mkdirSync(dir, { recursive: true });
};
const getDefaultOutputFileName = (migrationName, index) => {
    const m = /^(\d+)_(.+)$/.exec(migrationName);
    if (m !== null) {
        return `${m[1]}_${m[2]}.sql`;
    }
    const padded = String(index).padStart(4, '0');
    return `${padded}_${migrationName}.sql`;
};
const normalizeSql = (sql) => {
    const trimmed = sql.trim();
    if (trimmed === '')
        return '';
    return trimmed.endsWith(';') ? trimmed : `${trimmed};`;
};
const normalizeAlterTableAddColumn = (sql) => {
    const lowered = sql.trimStart().toLowerCase();
    if (!lowered.startsWith('alter table'))
        return sql;
    if (!/\badd\b/i.test(sql))
        return sql;
    if (/\badd\s+column\b/i.test(sql))
        return sql;
    return sql.replace(/\bADD\b\s+(?!COLUMN\b)/i, 'ADD COLUMN ');
};
const interpolateSql = (sql, parameters) => {
    const placeholders = (sql.match(/\?/g) ?? []).length;
    if (placeholders !== parameters.length) {
        throw ErrorFactory.createValidationError(`Cannot compile parameterized SQL: expected ${placeholders} params but got ${parameters.length}. SQL: ${sql}`);
    }
    let idx = 0;
    return sql.replaceAll('?', () => {
        const value = parameters[idx];
        idx += 1;
        return BaseAdapter.sanitize(value);
    });
};
const isMutatingSql = (sql) => {
    const s = sql.trimStart().toLowerCase();
    return (s.startsWith('insert') ||
        s.startsWith('update') ||
        s.startsWith('delete') ||
        s.startsWith('create') ||
        s.startsWith('drop') ||
        s.startsWith('alter') ||
        s.startsWith('replace') ||
        s.startsWith('pragma'));
};
const createNoopAdapter = (isConnected, setConnected) => ({
    async connect() {
        setConnected(true);
        await RESOLVED_VOID;
    },
    async disconnect() {
        setConnected(false);
        await RESOLVED_VOID;
    },
    async query() {
        await RESOLVED_VOID;
        return { rows: [], rowCount: 0 };
    },
    async queryOne() {
        await RESOLVED_VOID;
        return null;
    },
    async ping() {
        await RESOLVED_VOID;
    },
    async transaction(callback) {
        await RESOLVED_VOID;
        return callback(this);
    },
    async rawQuery() {
        await RESOLVED_VOID;
        return [];
    },
    getType() {
        return AdaptersEnum.d1;
    },
    isConnected() {
        return isConnected();
    },
    getPlaceholder() {
        return '?';
    },
});
const captureSql = (onSql, sql, parameters) => {
    const compiled = parameters.length > 0 ? interpolateSql(sql, parameters) : sql;
    if (!isMutatingSql(compiled))
        return;
    const normalized = normalizeSql(normalizeAlterTableAddColumn(compiled));
    if (normalized !== '')
        onSql(normalized);
};
const createCaptureDb = (onSql) => {
    let connected = false;
    const setConnected = (v) => {
        connected = v;
    };
    const noopAdapter = createNoopAdapter(() => connected, setConnected);
    const db = {
        async connect() {
            connected = true;
            await RESOLVED_VOID;
        },
        async disconnect() {
            connected = false;
            await RESOLVED_VOID;
        },
        isConnected() {
            return connected;
        },
        async query(sql, parameters = []) {
            captureSql(onSql, sql, parameters);
            await RESOLVED_VOID;
            return [];
        },
        async queryOne(sql, parameters = []) {
            captureSql(onSql, sql, parameters);
            await RESOLVED_VOID;
            return null;
        },
        async execute(sql, parameters = []) {
            captureSql(onSql, sql, parameters);
            await RESOLVED_VOID;
            return { rows: [], rowCount: 0 };
        },
        raw(value) {
            return { __raw: value };
        },
        async transaction(callback) {
            await RESOLVED_VOID;
            return callback(this);
        },
        table() {
            throw ErrorFactory.createCliError('D1 SQL compilation does not support QueryBuilder-based migrations yet. Use db.query(...) with explicit SQL.');
        },
        onBeforeQuery() {
            return;
        },
        onAfterQuery() {
            return;
        },
        offBeforeQuery() {
            return;
        },
        offAfterQuery() {
            return;
        },
        getAdapterInstance() {
            return noopAdapter;
        },
        getType() {
            return AdaptersEnum.d1;
        },
        getConfig() {
            return { driver: AdaptersEnum.d1 };
        },
        dispose() {
            // No resources to dispose for capture DB
        },
    };
    return db;
};
async function runSerial(items, fn) {
    let chain = Promise.resolve();
    let idx = 0;
    for (const item of items) {
        const current = idx;
        chain = chain.then(async () => fn(item, current));
        idx += 1;
    }
    await chain;
}
const resolveServiceDir = (opts) => {
    const service = typeof opts.service === 'string' && opts.service.length > 0 ? opts.service : null;
    if (service === null)
        return null;
    const rel = opts.serviceDirOverride ?? path.join('services', service, 'database/migrations');
    return MigrationDiscovery.resolveDir(opts.projectRoot, rel);
};
const discoverMigrations = async (opts) => {
    const globalDir = MigrationDiscovery.resolveDir(opts.projectRoot, opts.globalDir);
    const includeGlobal = opts.includeGlobal !== false;
    const dirs = [];
    if (includeGlobal)
        dirs.push(globalDir);
    const serviceDir = resolveServiceDir(opts);
    if (serviceDir !== null)
        dirs.push(serviceDir);
    const files = dirs.flatMap((dir) => MigrationDiscovery.listMigrationFiles(dir, opts.extension));
    const loaded = [];
    let chain = Promise.resolve();
    for (const file of files) {
        chain = chain.then(async () => {
            loaded.push(await MigrationLoader.load(file));
        });
    }
    await chain;
    return loaded.sort((a, b) => a.name.localeCompare(b.name));
};
export const D1SqlMigrations = Object.freeze({
    async compileAndWrite(opts) {
        const migrations = await discoverMigrations(opts);
        ensureDir(opts.outputDir);
        const out = [];
        await runSerial(migrations, async (m, idx) => {
            const statements = [];
            const db = createCaptureDb((sql) => statements.push(sql));
            await db.connect();
            try {
                await m.up(db);
            }
            finally {
                await db.disconnect();
            }
            const outputFileName = getDefaultOutputFileName(m.name, idx);
            const outputFilePath = path.join(opts.outputDir, outputFileName);
            const header = `-- Generated from ${m.name}\n`;
            const body = statements.length > 0 ? `${statements.join('\n')}\n` : '';
            fs.writeFileSync(outputFilePath, `${header}${body}`, 'utf8');
            out.push({
                sourceName: m.name,
                outputFileName,
                outputFilePath,
                statements,
            });
        });
        return out;
    },
});
