'use strict';

/**
 * Apply Engine — translate delta dari diff-engine menjadi `ALTER TABLE`
 * statement incremental. Komplemen `ddl-generator.js` yang menghasilkan full
 * CREATE TABLE.
 *
 * Operasi yang di-handle:
 *   - ADD COLUMN              (additive, default)
 *   - DROP COLUMN             (destruktif, butuh allowDrop)
 *   - MODIFY COLUMN           (length / nullable, butuh allowModify)
 *   - CREATE INDEX            (additive)
 *   - DROP INDEX              (destruktif, butuh allowDrop)
 *   - ADD CONSTRAINT UNIQUE   (additive)
 *   - DROP CONSTRAINT UNIQUE  (destruktif, butuh allowDrop)
 *   - ADD FOREIGN KEY         (additive, default — kecuali sqlite)
 *   - DROP FOREIGN KEY        (destruktif, butuh allowDrop — kecuali sqlite)
 *   - REPLACE FOREIGN KEY action (destruktif: DROP+ADD, butuh allowModify — kecuali sqlite)
 *
 * Operasi yang TIDAK di-handle (deferred):
 *   - ALTER COLUMN type change (butuh data conversion strategy per dialect)
 *   - PK changes (butuh rebuild table)
 *   - DEFAULT value changes (defer ke fase berikutnya)
 *   - CHECK constraint changes
 *   - FK changes pada sqlite (butuh rebuild table)
 *
 * SQLite tidak mendukung ALTER COLUMN / DROP COLUMN / ALTER ADD CONSTRAINT FK
 * tanpa rebuild table. Untuk dialect ini, MODIFY/DROP COLUMN dan semua perubahan
 * FK otomatis di-skip dengan reason 'sqlite limitation' walaupun flag opt-in
 * aktif.
 *
 * Output dipesan dengan urutan aman untuk dependency:
 *   1. ADD COLUMN                 (no dependencies)
 *   2. CREATE INDEX               (depend on column existence)
 *   3. ADD CONSTRAINT UNIQUE      (depend on column existence)
 *   4. ADD CONSTRAINT FOREIGN KEY (depend on column existence di kedua sisi)
 *   5. ALTER/MODIFY COLUMN        (modify after additive done)
 *   6. DROP CONSTRAINT FK         (release FK sebelum drop unique/index/column)
 *   7. DROP CONSTRAINT UNIQUE     (before drop column to avoid dependency error)
 *   8. DROP INDEX                 (before drop column)
 *   9. DROP COLUMN                (last)
 *
 * @module lib/dbschema-kit/apply-engine
 */

const { getDialect } = require('./dialect');
const {
    emitAddColumn,
    emitDropColumn,
    emitModifyColumn,
    emitCreateIndex,
    emitDropIndex,
    emitAddUnique,
    emitDropUnique,
    emitAddForeignKey,
    emitDropForeignKey
} = require('./emitters/alter-table');

const VALID_DIALECTS = ['postgres', 'mysql', 'oracle', 'sqlite'];

// Kategori reason dari diff-engine compareFieldDetails. Reason berbentuk string
// dengan prefix "type:", "length:", "precision:", "scale:", "nullable:",
// "default:", "default kind:", atau "default (weak):". Kategorisasi ini
// menentukan apakah modify aman di-emit (length/nullable) atau di-skip
// (type/default — deferred).
function classifyMismatchReasons(reasons) {
    const result = {
        hasType: false,
        hasLength: false,
        hasPrecision: false,
        hasScale: false,
        hasNullable: false,
        hasDefault: false
    };
    if (!Array.isArray(reasons)) return result;
    for (const r of reasons) {
        if (typeof r !== 'string') continue;
        if (r.startsWith('type:')) result.hasType = true;
        else if (r.startsWith('length:')) result.hasLength = true;
        else if (r.startsWith('precision:')) result.hasPrecision = true;
        else if (r.startsWith('scale:')) result.hasScale = true;
        else if (r.startsWith('nullable:')) result.hasNullable = true;
        else if (r.startsWith('default')) result.hasDefault = true;
    }
    return result;
}

// Resolve IR field definition dari delta summary. Delta engine menyimpan
// summary terbatas (name, type, length, precision, scale, nullable). Untuk
// emitter kita perlu shape kompatibel dengan dialect.mapType + notnull flag.
function buildFieldFromSummary(summary) {
    const field = {};
    if (summary.type !== undefined) field.type = summary.type;
    if (summary.length !== undefined) field.length = summary.length;
    if (summary.precision !== undefined) field.precision = summary.precision;
    if (summary.scale !== undefined) field.scale = summary.scale;
    if (summary.nullable === false) field.notnull = true;
    return field;
}

// Resolve field IR dari SDF map bila tersedia (untuk default value dan
// notnull yang akurat). Bila tidak ditemukan, fallback ke summary.
function resolveFieldIR(sdfModels, tableName, fieldName, fallbackSummary) {
    if (sdfModels) {
        const ir = lookupSdfIR(sdfModels, tableName);
        if (ir && ir.fields && ir.fields[fieldName]) {
            return ir.fields[fieldName];
        }
    }
    return buildFieldFromSummary(fallbackSummary);
}

function lookupSdfIR(sdfModels, tableName) {
    if (!sdfModels) return null;
    if (sdfModels instanceof Map) {
        for (const ir of sdfModels.values()) {
            if (ir && ir.tableName === tableName) return ir;
        }
        return null;
    }
    if (Array.isArray(sdfModels)) {
        for (const ir of sdfModels) {
            if (ir && ir.tableName === tableName) return ir;
        }
        return null;
    }
    if (typeof sdfModels === 'object') {
        for (const key of Object.keys(sdfModels)) {
            const ir = sdfModels[key];
            if (ir && ir.tableName === tableName) return ir;
        }
    }
    return null;
}

function resolveTableIR(sdfModels, delta) {
    const fromSdf = lookupSdfIR(sdfModels, delta.tableName);
    if (fromSdf) {
        return { tableName: fromSdf.tableName, schemaName: fromSdf.schemaName || null };
    }
    return { tableName: delta.tableName, schemaName: null };
}

// ─────────────────────────────────────────────────────────────
// Statement bucketing per kategori (untuk ordering aman)
// ─────────────────────────────────────────────────────────────

function buildBuckets() {
    return {
        addColumns: [],
        createIndexes: [],
        addUniques: [],
        addForeignKeys: [],
        modifyColumns: [],
        dropForeignKeys: [],
        replaceForeignKeysAdd: [],
        dropConstraints: [],
        dropIndexes: [],
        dropColumns: []
    };
}

function flattenBuckets(buckets) {
    return [
        ...buckets.addColumns,
        ...buckets.createIndexes,
        ...buckets.addUniques,
        ...buckets.addForeignKeys,
        ...buckets.modifyColumns,
        ...buckets.dropForeignKeys,
        ...buckets.replaceForeignKeysAdd,
        ...buckets.dropConstraints,
        ...buckets.dropIndexes,
        ...buckets.dropColumns
    ];
}

// ─────────────────────────────────────────────────────────────
// Per-delta processing
// ─────────────────────────────────────────────────────────────

function processFieldsOnlyInSdf(delta, tableIR, dialect, buckets, skipped, sdfModels, summary) {
    const list = delta.fields && Array.isArray(delta.fields.onlyInSdf) ? delta.fields.onlyInSdf : [];
    for (const fSummary of list) {
        const fieldIR = resolveFieldIR(sdfModels, delta.tableName, fSummary.name, fSummary);
        try {
            buckets.addColumns.push(emitAddColumn(tableIR, fSummary.name, fieldIR, dialect));
            summary.totalAdditions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'add-column',
                target: fSummary.name,
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

function processFieldsOnlyInDb(delta, tableIR, dialect, options, buckets, skipped, summary) {
    const list = delta.fields && Array.isArray(delta.fields.onlyInDb) ? delta.fields.onlyInDb : [];
    for (const fSummary of list) {
        if (!options.allowDrop) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-column',
                target: fSummary.name,
                reason: 'requires --allow-drop',
                description: `${delta.tableName}.${fSummary.name} exists in DB but not in SDF`
            });
            continue;
        }
        if (dialect.name === 'sqlite') {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-column',
                target: fSummary.name,
                reason: 'sqlite limitation',
                description: `SQLite does not support DROP COLUMN without table rebuild (${delta.tableName}.${fSummary.name})`
            });
            continue;
        }
        try {
            buckets.dropColumns.push(emitDropColumn(tableIR, fSummary.name, dialect));
            summary.totalDeletions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-column',
                target: fSummary.name,
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

function processFieldsMismatched(delta, tableIR, dialect, options, buckets, skipped, sdfModels, summary) {
    const list = delta.fields && Array.isArray(delta.fields.mismatched) ? delta.fields.mismatched : [];
    for (const m of list) {
        const cls = classifyMismatchReasons(m.reasons);

        // Type change: defer (butuh data conversion strategy)
        if (cls.hasType) {
            skipped.push({
                table: delta.tableName,
                kind: 'modify-type',
                target: m.name,
                reason: 'deferred',
                description: `Type change for ${delta.tableName}.${m.name} requires explicit data conversion strategy (deferred from MVP)`
            });
            continue;
        }
        // Default change: defer
        if (cls.hasDefault) {
            skipped.push({
                table: delta.tableName,
                kind: 'modify-default',
                target: m.name,
                reason: 'deferred',
                description: `Default value change for ${delta.tableName}.${m.name} deferred from MVP`
            });
            continue;
        }
        // Precision / scale: defer (mirip type change)
        if (cls.hasPrecision || cls.hasScale) {
            skipped.push({
                table: delta.tableName,
                kind: 'modify-precision',
                target: m.name,
                reason: 'deferred',
                description: `Precision/scale change for ${delta.tableName}.${m.name} deferred from MVP`
            });
            continue;
        }

        const changeNullable = cls.hasNullable;
        const changeLength = cls.hasLength;
        if (!changeNullable && !changeLength) {
            // Nothing actionable
            continue;
        }

        if (!options.allowModify) {
            const detailParts = [];
            if (changeLength) detailParts.push('length');
            if (changeNullable) detailParts.push('nullable');
            skipped.push({
                table: delta.tableName,
                kind: 'modify-column',
                target: m.name,
                reason: 'requires --allow-modify',
                description: `${delta.tableName}.${m.name} ${detailParts.join('/')} drift requires --allow-modify`
            });
            continue;
        }

        if (dialect.name === 'sqlite') {
            skipped.push({
                table: delta.tableName,
                kind: 'modify-column',
                target: m.name,
                reason: 'sqlite limitation',
                description: `SQLite does not support ALTER COLUMN without table rebuild (${delta.tableName}.${m.name})`
            });
            continue;
        }

        const fieldIR = resolveFieldIR(sdfModels, delta.tableName, m.name, m.sdf);
        try {
            const statements = emitModifyColumn(tableIR, m.name, fieldIR, { changeNullable, changeLength }, dialect);
            for (const s of statements) {
                buckets.modifyColumns.push(s);
                summary.totalModifications++;
            }
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'modify-column',
                target: m.name,
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

function processIndexes(delta, tableIR, dialect, options, buckets, skipped, summary) {
    if (!delta.indexes) return;

    const addList = Array.isArray(delta.indexes.onlyInSdf) ? delta.indexes.onlyInSdf : [];
    for (const idx of addList) {
        const cols = Array.isArray(idx.columns) ? idx.columns : [];
        if (cols.length === 0) continue;
        try {
            buckets.createIndexes.push(emitCreateIndex(tableIR, cols, dialect));
            summary.totalAdditions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'create-index',
                target: cols.join(','),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }

    const dropList = Array.isArray(delta.indexes.onlyInDb) ? delta.indexes.onlyInDb : [];
    for (const idx of dropList) {
        const cols = Array.isArray(idx.columns) ? idx.columns : [];
        if (cols.length === 0) continue;
        if (!options.allowDrop) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-index',
                target: cols.join(','),
                reason: 'requires --allow-drop',
                description: `Index on ${delta.tableName}(${cols.join(', ')}) exists in DB but not in SDF`
            });
            continue;
        }
        try {
            buckets.dropIndexes.push(emitDropIndex(tableIR, cols, dialect, { name: idx.name }));
            summary.totalDeletions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-index',
                target: cols.join(','),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

function processUniques(delta, tableIR, dialect, options, buckets, skipped, summary) {
    if (!delta.uniques) return;

    const addList = Array.isArray(delta.uniques.onlyInSdf) ? delta.uniques.onlyInSdf : [];
    for (const uq of addList) {
        const cols = Array.isArray(uq.columns) ? uq.columns : [];
        if (cols.length === 0) continue;
        try {
            buckets.addUniques.push(emitAddUnique(tableIR, cols, dialect));
            summary.totalAdditions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'add-unique',
                target: cols.join(','),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }

    const dropList = Array.isArray(delta.uniques.onlyInDb) ? delta.uniques.onlyInDb : [];
    for (const uq of dropList) {
        const cols = Array.isArray(uq.columns) ? uq.columns : [];
        if (cols.length === 0) continue;
        if (!options.allowDrop) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-unique',
                target: cols.join(','),
                reason: 'requires --allow-drop',
                description: `Unique on ${delta.tableName}(${cols.join(', ')}) exists in DB but not in SDF`
            });
            continue;
        }
        try {
            buckets.dropConstraints.push(emitDropUnique(tableIR, cols, dialect, { name: uq.name }));
            summary.totalDeletions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'drop-unique',
                target: cols.join(','),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

// ─────────────────────────────────────────────────────────────
// FK delta processing
// ─────────────────────────────────────────────────────────────

// FK delta entries dari diff-engine memuat triplet (localKey, target, references)
// plus optional `name` (relName dari relations map) dan `dbName` (untuk mismatched).
// Apply path hanya menjalankan validator opsional, jadi kita fallback ke localKey
// untuk derive constraint name bila relName tidak tersedia.
function pickRelName(fk) {
    if (fk && typeof fk.name === 'string' && fk.name !== '') return fk.name;
    if (fk && typeof fk.localKey === 'string' && fk.localKey !== '') return fk.localKey;
    return null;
}

function fkSkipTarget(fk) {
    const local = fk && fk.localKey ? fk.localKey : '?';
    const target = fk && fk.target ? fk.target : '?';
    const ref = fk && fk.references ? fk.references : '?';
    return `${local} -> ${target}.${ref}`;
}

function processForeignKeys(delta, tableIR, dialect, options, buckets, skipped, summary) {
    if (!delta.foreignKeys) return;

    const fkOnlySdf = Array.isArray(delta.foreignKeys.onlyInSdf) ? delta.foreignKeys.onlyInSdf : [];
    const fkOnlyDb = Array.isArray(delta.foreignKeys.onlyInDb) ? delta.foreignKeys.onlyInDb : [];
    const fkMismatch = Array.isArray(delta.foreignKeys.mismatched) ? delta.foreignKeys.mismatched : [];

    // SQLite: semua perubahan FK butuh table rebuild — defer dengan reason akurat
    if (dialect.name === 'sqlite') {
        for (const fk of fkOnlySdf) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'sqlite limitation',
                description: `SQLite does not support ALTER TABLE ADD CONSTRAINT FOREIGN KEY (${delta.tableName}.${fk.localKey})`
            });
        }
        for (const fk of fkOnlyDb) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'sqlite limitation',
                description: `SQLite does not support ALTER TABLE DROP CONSTRAINT FOREIGN KEY (${delta.tableName}.${fk.localKey})`
            });
        }
        for (const fk of fkMismatch) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'sqlite limitation',
                description: `SQLite does not support ALTER TABLE for FK action change (${delta.tableName}.${fk.localKey})`
            });
        }
        return;
    }

    // FK additive (onlyInSdf) — non-destruktif, emit langsung tanpa flag opt-in
    for (const fk of fkOnlySdf) {
        const relName = pickRelName(fk);
        if (!relName) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: `Foreign key for ${delta.tableName} missing both name and localKey`
            });
            continue;
        }
        try {
            buckets.addForeignKeys.push(emitAddForeignKey(tableIR, relName, fk, dialect));
            summary.totalAdditions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }

    // FK drop (onlyInDb) — destruktif, butuh --allow-drop
    for (const fk of fkOnlyDb) {
        const relName = pickRelName(fk);
        if (!relName) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: `Foreign key for ${delta.tableName} missing both name and localKey`
            });
            continue;
        }
        if (!options.allowDrop) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'requires --allow-drop',
                description: `Drop foreign key on ${delta.tableName}.${fk.localKey} requires --allow-drop`
            });
            continue;
        }
        try {
            // DROP memakai conname aktual dari introspeksi (fk.dbConstraintName) agar
            // statement menargetkan constraint yang benar-benar ada di DB. Bila conname
            // tidak tersedia (driver kustom), emitter fallback ke derivasi nama lama
            // (generateConstraintName) — perilaku legacy, tanpa entry skipped/warning.
            buckets.dropForeignKeys.push(
                emitDropForeignKey(tableIR, relName, dialect, { name: fk.dbConstraintName })
            );
            summary.totalDeletions++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }

    // FK mismatch (action change) — destruktif (DROP + ADD), butuh --allow-modify
    for (const fk of fkMismatch) {
        const relName = pickRelName(fk);
        if (!relName) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: `Foreign key for ${delta.tableName} missing both name and localKey`
            });
            continue;
        }
        if (!options.allowModify) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'requires --allow-modify',
                description: `Foreign key action change on ${delta.tableName}.${fk.localKey} requires --allow-modify`
            });
            continue;
        }
        try {
            const sdfActions = fk.sdf || {};
            const addRel = {
                localKey: fk.localKey,
                target: fk.target,
                references: fk.references,
                onDelete: sdfActions.onDelete,
                onUpdate: sdfActions.onUpdate
            };
            // DROP memakai conname aktual dari DB (fk.dbConstraintName, fallback derive
            // bila absen); ADD memakai nama SDF-derive (relName) konsisten dengan
            // create-table. ADD masuk replaceForeignKeysAdd bucket supaya terbit SETELAH
            // semua DROP FK, mencegah konflik bila nama lama dan baru kebetulan sama.
            buckets.dropForeignKeys.push(
                emitDropForeignKey(tableIR, relName, dialect, { name: fk.dbConstraintName })
            );
            buckets.replaceForeignKeysAdd.push(emitAddForeignKey(tableIR, relName, addRel, dialect));
            summary.totalModifications++;
        } catch (err) {
            skipped.push({
                table: delta.tableName,
                kind: 'foreign-key',
                target: fkSkipTarget(fk),
                reason: 'emit-error',
                description: err && err.message ? err.message : String(err)
            });
        }
    }
}

function noteDeferredSections(delta, skipped) {
    // PK changes — deferred dari MVP karena butuh rebuild table.
    if (delta.primaryKey && delta.primaryKey.match === false) {
        const sdf = Array.isArray(delta.primaryKey.sdf) ? delta.primaryKey.sdf : [];
        const db = Array.isArray(delta.primaryKey.db) ? delta.primaryKey.db : [];
        skipped.push({
            table: delta.tableName,
            kind: 'primary-key',
            target: `pk(${sdf.join(',') || '∅'} vs ${db.join(',') || '∅'})`,
            reason: 'deferred',
            description: `Primary key changes for ${delta.tableName} deferred from MVP (requires table rebuild)`
        });
    }

    if (delta.checks) {
        const cOnlySdf = Array.isArray(delta.checks.onlyInSdf) ? delta.checks.onlyInSdf : [];
        const cOnlyDb = Array.isArray(delta.checks.onlyInDb) ? delta.checks.onlyInDb : [];
        for (const c of cOnlySdf) {
            skipped.push({
                table: delta.tableName,
                kind: 'check-constraint',
                target: c.field || c.expression || '(check)',
                reason: 'deferred',
                description: `Check constraint changes deferred from MVP (additive ${delta.tableName})`
            });
        }
        for (const c of cOnlyDb) {
            skipped.push({
                table: delta.tableName,
                kind: 'check-constraint',
                target: c.field || c.expression || '(check)',
                reason: 'deferred',
                description: `Check constraint changes deferred from MVP (drop ${delta.tableName})`
            });
        }
    }
}

// ─────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────

/**
 * Generate ALTER statement dari delta diff-engine per model.
 *
 * @param {Array<Object>} deltas - Output dari diffModels()
 * @param {Object} options
 * @param {string} options.dialect - 'postgres' | 'mysql' | 'oracle' | 'sqlite'
 * @param {boolean} [options.allowDrop=false]
 * @param {boolean} [options.allowModify=false]
 * @param {Map|Object|Array} [options.sdfModels] - SDF model map untuk resolve field IR penuh
 * @returns {{
 *   statements: Array<{ table: string, sql: string }>,
 *   statementsByTable: Object<string, string[]>,
 *   skipped: Array<{ table, kind, target, reason, description }>,
 *   summary: { totalAdditions, totalModifications, totalDeletions, totalSkipped, tablesAffected }
 * }}
 */
function generateAlterStatements(deltas, options) {
    if (!Array.isArray(deltas)) {
        throw new Error('generateAlterStatements: deltas must be an array');
    }
    if (!options || typeof options !== 'object') {
        throw new Error('generateAlterStatements: options object is required');
    }
    if (!VALID_DIALECTS.includes(options.dialect)) {
        throw new Error(
            `generateAlterStatements: invalid dialect '${options.dialect}'. ` +
            `Supported: ${VALID_DIALECTS.join(', ')}`
        );
    }

    const allowDrop = options.allowDrop === true;
    const allowModify = options.allowModify === true;
    const sdfModels = options.sdfModels || null;
    const dialect = getDialect(options.dialect);

    const statements = [];
    const statementsByTable = {};
    const skipped = [];
    const summary = {
        totalAdditions: 0,
        totalModifications: 0,
        totalDeletions: 0,
        totalSkipped: 0,
        tablesAffected: 0
    };

    const tablesWithStatements = new Set();
    const localOptions = { allowDrop, allowModify };

    for (const delta of deltas) {
        if (!delta || typeof delta.tableName !== 'string') continue;
        if (!delta.hasDrift) continue;

        const tableIR = resolveTableIR(sdfModels, delta);
        const buckets = buildBuckets();

        processFieldsOnlyInSdf(delta, tableIR, dialect, buckets, skipped, sdfModels, summary);
        processFieldsOnlyInDb(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
        processFieldsMismatched(delta, tableIR, dialect, localOptions, buckets, skipped, sdfModels, summary);
        processIndexes(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
        processUniques(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
        processForeignKeys(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
        noteDeferredSections(delta, skipped);

        const flat = flattenBuckets(buckets);
        if (flat.length > 0) {
            tablesWithStatements.add(delta.tableName);
            statementsByTable[delta.tableName] = flat.slice();
            for (const sql of flat) {
                statements.push({ table: delta.tableName, sql });
            }
        }
    }

    summary.totalSkipped = skipped.length;
    summary.tablesAffected = tablesWithStatements.size;

    return {
        statements,
        statementsByTable,
        skipped,
        summary
    };
}

module.exports = {
    generateAlterStatements,
    _internal: {
        classifyMismatchReasons,
        buildFieldFromSummary,
        resolveTableIR,
        lookupSdfIR
    }
};
