"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.compareSchemas = compareSchemas;
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const prompt_1 = require("./prompt");
/*@ts-ignore*/
var { __ΩDatabaseSchema, __ΩDialect, __ΩSchemaDiff, __ΩTableSchema, __ΩTableDiff, __ΩColumnSchema, __ΩColumnModification, __ΩIndexSchema, __ΩForeignKeySchema } = require('./schema-model');
async function compareSchemas(entitySchema, dbSchema, dialect, interactive, pgSchema) {
    const addedTables = [];
    const removedTables = [];
    const modifiedTables = [];
    // Find added tables (in entities but not in DB)
    for (const [name, table] of entitySchema) {
        if (!dbSchema.has(name)) {
            addedTables.push(table);
        }
    }
    // Find removed tables (in DB but not in entities)
    for (const [name, table] of dbSchema) {
        if (!entitySchema.has(name)) {
            removedTables.push(table);
        }
    }
    // Compare matching tables
    for (const [name, entityTable] of entitySchema) {
        const dbTable = dbSchema.get(name);
        if (!dbTable)
            continue;
        const diff = await compareTable(entityTable, dbTable, dialect, interactive);
        if (diff) {
            modifiedTables.push(diff);
        }
    }
    // Collect all enum type names from entity tables (for safe PG DROP TYPE filtering)
    let entityEnumTypes;
    if (dialect === 'postgres') {
        entityEnumTypes = (Set.Ω = [['&']], new Set());
        for (const table of entitySchema.values()) {
            for (const col of table.columns) {
                if (col.type === 'enum' && col.enumTypeName) {
                    entityEnumTypes.add(col.enumTypeName);
                }
            }
        }
    }
    return { dialect, pgSchema, addedTables, removedTables, modifiedTables, entityEnumTypes };
}
compareSchemas.__type = [() => __ΩDatabaseSchema, 'entitySchema', () => __ΩDatabaseSchema, 'dbSchema', () => __ΩDialect, 'dialect', 'interactive', 'pgSchema', () => __ΩSchemaDiff, 'compareSchemas', 'Pn!2"n#2$n%2&)2\'&2(8n)`/*'];
async function compareTable(entityTable, dbTable, dialect, interactive) {
    const diff = {
        tableName: entityTable.name,
        addedColumns: [],
        removedColumns: [],
        modifiedColumns: [],
        renamedColumns: [],
        reorderedColumns: [],
        addedIndexes: [],
        removedIndexes: [],
        addedForeignKeys: [],
        removedForeignKeys: [],
        primaryKeyChanged: false,
        addedEnumTypes: [],
        removedEnumTypes: [],
        modifiedEnumTypes: [],
        entityColumns: entityTable.columns
    };
    // --- Columns ---
    const entityColMap = new Map(entityTable.columns.map(__assignType(c => [c.name, c], ['c', '', 'P"2!"/"'])));
    const dbColMap = new Map(dbTable.columns.map(__assignType(c => [c.name, c], ['c', '', 'P"2!"/"'])));
    let candidateAdds = entityTable.columns.filter(__assignType(c => !dbColMap.has(c.name), ['c', '', 'P"2!"/"']));
    let candidateDrops = dbTable.columns.filter(__assignType(c => !entityColMap.has(c.name) && !entityTable.skippedColumns?.has(c.name), ['c', '', 'P"2!"/"']));
    // Rename detection
    if (candidateAdds.length > 0 && candidateDrops.length > 0) {
        if (interactive) {
            const renames = await detectRenames(entityTable.name, candidateAdds, candidateDrops);
            for (const rename of renames) {
                diff.renamedColumns.push(rename);
                candidateAdds = candidateAdds.filter(__assignType(c => c.name !== rename.to, ['c', '', 'P"2!"/"']));
                candidateDrops = candidateDrops.filter(__assignType(c => c.name !== rename.from, ['c', '', 'P"2!"/"']));
            }
        }
        else {
            console.warn(`Warning: Table \`${entityTable.name}\` has columns added (${candidateAdds.map(__assignType(c => c.name, ['c', '', 'P"2!"/"'])).join(', ')}) ` +
                `and removed (${candidateDrops.map(__assignType(c => c.name, ['c', '', 'P"2!"/"'])).join(', ')}). ` +
                `In non-interactive mode, these are treated as separate DROP/ADD operations (potential data loss). ` +
                `Run without --non-interactive to detect renames.`);
        }
    }
    diff.addedColumns = candidateAdds;
    diff.removedColumns = candidateDrops;
    // Modified columns (present in both by same name)
    for (const [name, entityCol] of entityColMap) {
        const dbCol = dbColMap.get(name);
        if (!dbCol)
            continue;
        const mod = compareColumn(entityCol, dbCol);
        if (mod)
            diff.modifiedColumns.push(mod);
    }
    // Renamed columns may also have property changes (type, nullable, default, etc.)
    // For MySQL, CHANGE COLUMN already includes the full new definition so these are handled.
    // For PG, RENAME COLUMN only renames — separate ALTER COLUMN statements are needed.
    for (const rename of diff.renamedColumns) {
        const dbCol = dbColMap.get(rename.from);
        if (!dbCol)
            continue;
        const mod = compareColumn(rename.column, dbCol);
        if (mod)
            diff.modifiedColumns.push(mod);
    }
    // Column reordering (MySQL only)
    if (dialect === 'mysql') {
        diff.reorderedColumns = detectReorderingMySQL(entityTable, dbTable, diff);
    }
    // --- Primary Key ---
    const entityPK = entityTable.columns.filter(__assignType(c => c.isPrimaryKey, ['c', '', 'P"2!"/"'])).map(__assignType(c => c.name, ['c', '', 'P"2!"/"']));
    const rawDbPK = dbTable.columns.filter(__assignType(c => c.isPrimaryKey, ['c', '', 'P"2!"/"'])).map(__assignType(c => c.name, ['c', '', 'P"2!"/"']));
    // Apply rename mappings to DB PK names so that pure PK column renames
    // are not falsely detected as PK changes
    const renamedFromToMap = new Map(diff.renamedColumns.map(__assignType(r => [r.from, r.to], ['r', '', 'P"2!"/"'])));
    const dbPK = rawDbPK.map(__assignType(name => renamedFromToMap.get(name) ?? name, ['name', '', 'P"2!"/"']));
    if (!arraysEqual(entityPK, dbPK)) {
        diff.primaryKeyChanged = true;
        diff.newPrimaryKey = entityPK;
        // Store the raw DB column names (before rename mapping) so that DDL generation
        // can reference columns by their current DB names when stripping AUTO_INCREMENT
        // before DROP PRIMARY KEY (which happens before CHANGE COLUMN renames).
        diff.oldPrimaryKey = rawDbPK;
        diff.oldPrimaryKeyConstraintName = dbTable.primaryKeyConstraintName;
    }
    // --- Indexes ---
    const { added: addedIdx, removed: removedIdx } = compareIndexes(entityTable.indexes, dbTable.indexes);
    diff.addedIndexes = addedIdx;
    diff.removedIndexes = removedIdx;
    // --- Foreign Keys ---
    const { added: addedFK, removed: removedFK } = compareForeignKeys(entityTable.foreignKeys, dbTable.foreignKeys);
    diff.addedForeignKeys = addedFK;
    diff.removedForeignKeys = removedFK;
    // --- Enum types (PG only) ---
    if (dialect === 'postgres') {
        detectEnumChanges(diff, entityTable, dbTable);
    }
    // Check if there are any actual changes
    if (diff.addedColumns.length === 0 &&
        diff.removedColumns.length === 0 &&
        diff.modifiedColumns.length === 0 &&
        diff.renamedColumns.length === 0 &&
        diff.reorderedColumns.length === 0 &&
        diff.addedIndexes.length === 0 &&
        diff.removedIndexes.length === 0 &&
        diff.addedForeignKeys.length === 0 &&
        diff.removedForeignKeys.length === 0 &&
        !diff.primaryKeyChanged &&
        diff.addedEnumTypes.length === 0 &&
        diff.removedEnumTypes.length === 0 &&
        diff.modifiedEnumTypes.length === 0) {
        return null;
    }
    return diff;
}
compareTable.__type = [() => __ΩTableSchema, 'entityTable', () => __ΩTableSchema, 'dbTable', () => __ΩDialect, 'dialect', 'interactive', () => __ΩTableDiff, 'compareTable', 'Pn!2"n#2$n%2&)2\'Pn(,J`/)'];
function compareColumn(entityCol, dbCol) {
    const typeChanged = !typesMatch(entityCol, dbCol);
    const nullableChanged = entityCol.nullable !== dbCol.nullable;
    const defaultChanged = !defaultsMatch(entityCol, dbCol);
    const autoIncrementChanged = entityCol.autoIncrement !== dbCol.autoIncrement;
    // ON UPDATE expression (MySQL only). If entity side has no onUpdateExpression, treat as "remove it".
    const onUpdateChanged = (entityCol.onUpdateExpression || '') !== (dbCol.onUpdateExpression || '');
    if (!typeChanged && !nullableChanged && !defaultChanged && !autoIncrementChanged && !onUpdateChanged) {
        return null;
    }
    return {
        name: entityCol.name,
        oldColumn: dbCol,
        newColumn: entityCol,
        typeChanged,
        nullableChanged,
        defaultChanged,
        autoIncrementChanged,
        onUpdateChanged
    };
}
compareColumn.__type = [() => __ΩColumnSchema, 'entityCol', () => __ΩColumnSchema, 'dbCol', () => __ΩColumnModification, 'compareColumn', 'Pn!2"n#2$Pn%,J/&'];
function normalizeTypeAlias(type) {
    switch (type) {
        case 'integer':
            return 'int';
        case 'numeric':
            return 'decimal';
        default:
            return type;
    }
}
normalizeTypeAlias.__type = ['type', 'normalizeTypeAlias', 'P&2!&/"'];
function typesMatch(a, b) {
    if (normalizeTypeAlias(a.type) !== normalizeTypeAlias(b.type))
        return false;
    // Compare size when relevant
    if (a.size !== undefined || b.size !== undefined) {
        if (a.size !== b.size)
            return false;
    }
    // Compare scale
    if (a.scale !== undefined || b.scale !== undefined) {
        if (a.scale !== b.scale)
            return false;
    }
    // Compare unsigned
    if (a.unsigned !== b.unsigned)
        return false;
    // Compare enum type name (PG)
    if (a.enumTypeName || b.enumTypeName) {
        if (a.enumTypeName !== b.enumTypeName)
            return false;
    }
    // Compare enum values (sorted to ignore order-only differences)
    if (a.enumValues || b.enumValues) {
        const aVals = [...(a.enumValues || [])].sort();
        const bVals = [...(b.enumValues || [])].sort();
        if (!arraysEqual(aVals, bVals))
            return false;
    }
    return true;
}
typesMatch.__type = [() => __ΩColumnSchema, 'a', () => __ΩColumnSchema, 'b', 'typesMatch', 'Pn!2"n#2$)/%'];
function normalizeDefaultExpression(expr) {
    let s = expr.toUpperCase().trim();
    // Normalize now() and CURRENT_TIMESTAMP() variants to CURRENT_TIMESTAMP (no parens)
    s = s.replace(/\bNOW\s*\(\)/g, 'CURRENT_TIMESTAMP');
    s = s.replace(/\bCURRENT_TIMESTAMP\s*\(\)/g, 'CURRENT_TIMESTAMP');
    return s;
}
normalizeDefaultExpression.__type = ['expr', 'normalizeDefaultExpression', 'P&2!&/"'];
function defaultsMatch(a, b) {
    // Skip default comparison for auto-increment columns
    if (a.autoIncrement || b.autoIncrement)
        return true;
    // If entity side has no default info at all, treat as "unspecified" and skip comparison.
    // Columns without field initializers have no default info and are skipped to avoid spurious diffs.
    if (a.defaultValue === undefined && a.defaultExpression === undefined)
        return true;
    // Both have expression defaults
    if (a.defaultExpression && b.defaultExpression) {
        return normalizeDefaultExpression(a.defaultExpression) === normalizeDefaultExpression(b.defaultExpression);
    }
    // One has expression, other doesn't
    if (a.defaultExpression !== b.defaultExpression) {
        if ((a.defaultExpression && !b.defaultExpression) || (!a.defaultExpression && b.defaultExpression)) {
            return false;
        }
    }
    // Compare values
    if (a.defaultValue === undefined && b.defaultValue === undefined)
        return true;
    if (a.defaultValue === undefined || b.defaultValue === undefined)
        return false;
    return String(a.defaultValue) === String(b.defaultValue);
}
defaultsMatch.__type = [() => __ΩColumnSchema, 'a', () => __ΩColumnSchema, 'b', 'defaultsMatch', 'Pn!2"n#2$)/%'];
async function detectRenames(tableName, candidateAdds, candidateDrops) {
    const renames = [];
    let remainingDrops = [...candidateDrops];
    for (const added of candidateAdds) {
        // Prefer exact type matches
        let compatibleDrops = remainingDrops.filter(__assignType(d => typesMatch(added, d), ['d', '', 'P"2!"/"']));
        // Fall back to all remaining drops (type differs — prompt will warn)
        if (compatibleDrops.length === 0 && remainingDrops.length > 0) {
            compatibleDrops = remainingDrops;
        }
        if (compatibleDrops.length === 0)
            continue;
        const result = await (0, prompt_1.promptRename)(tableName, added.name, compatibleDrops.map(__assignType(d => d.name, ['d', '', 'P"2!"/"'])));
        if (result) {
            renames.push({ from: result, to: added.name, column: added });
            remainingDrops = remainingDrops.filter(__assignType(d => d.name !== result, ['d', '', 'P"2!"/"']));
        }
    }
    return renames;
}
detectRenames.__type = ['tableName', () => __ΩColumnSchema, 'candidateAdds', () => __ΩColumnSchema, 'candidateDrops', 'from', 'to', () => __ΩColumnSchema, 'column', 'detectRenames', 'P&2!n"F2#n$F2%P&4&&4\'n(4)MF`/*'];
function detectReorderingMySQL(entityTable, dbTable, diff) {
    // Build the expected column order from the entity
    const entityOrder = entityTable.columns.map(__assignType(c => c.name, ['c', '', 'P"2!"/"']));
    // Build the current DB order, accounting for adds/drops/renames
    const renamedFrom = new Map(diff.renamedColumns.map(__assignType(r => [r.from, r.to], ['r', '', 'P"2!"/"'])));
    const removedNames = new Set(diff.removedColumns.map(__assignType(c => c.name, ['c', '', 'P"2!"/"'])));
    const addedNames = new Set(diff.addedColumns.map(__assignType(c => c.name, ['c', '', 'P"2!"/"'])));
    // Current DB order with renames applied and removals excluded
    const dbOrder = dbTable.columns.map(__assignType(c => renamedFrom.get(c.name) || c.name, ['c', '', 'P"2!"/"'])).filter(__assignType(name => !removedNames.has(name), ['name', '', 'P"2!"/"']));
    // Entity order without new additions (the columns that should be in DB order)
    const existingEntityOrder = entityOrder.filter(__assignType(name => !addedNames.has(name), ['name', '', 'P"2!"/"']));
    // Check if the existing columns are in the same relative order
    if (arraysEqual(existingEntityOrder, dbOrder)) {
        return [];
    }
    // Compute which columns need MODIFY with AFTER
    const reorders = [];
    for (let i = 0; i < existingEntityOrder.length; i++) {
        if (existingEntityOrder[i] !== dbOrder[i]) {
            const after = i === 0 ? null : existingEntityOrder[i - 1];
            reorders.push({ name: existingEntityOrder[i], after });
        }
    }
    return reorders;
}
detectReorderingMySQL.__type = [() => __ΩTableSchema, 'entityTable', () => __ΩTableSchema, 'dbTable', () => __ΩTableDiff, 'diff', 'name', 'after', 'detectReorderingMySQL', 'Pn!2"n#2$n%2&P&4\'P&,J4(MF/)'];
function compareIndexes(entityIndexes, dbIndexes) {
    // Match by column set + uniqueness + spatial, not by name
    const indexKey = __assignType((idx) => `${idx.columns.join(',')}:${idx.unique}:${idx.spatial}`, [() => __ΩIndexSchema, 'idx', '', 'Pn!2""/#']);
    const entityKeys = new Map(entityIndexes.map(__assignType(i => [indexKey(i), i], ['i', '', 'P"2!"/"'])));
    const dbKeys = new Map(dbIndexes.map(__assignType(i => [indexKey(i), i], ['i', '', 'P"2!"/"'])));
    const added = entityIndexes.filter(__assignType(i => !dbKeys.has(indexKey(i)), ['i', '', 'P"2!"/"']));
    const removed = dbIndexes.filter(__assignType(i => !entityKeys.has(indexKey(i)), ['i', '', 'P"2!"/"']));
    return { added, removed };
}
compareIndexes.__type = [() => __ΩIndexSchema, 'entityIndexes', () => __ΩIndexSchema, 'dbIndexes', () => __ΩIndexSchema, 'added', () => __ΩIndexSchema, 'removed', 'compareIndexes', 'Pn!F2"n#F2$Pn%F4&n\'F4(M/)'];
function normalizeFkAction(action) {
    const upper = action.toUpperCase();
    // NO ACTION and RESTRICT are semantically equivalent for comparison
    return upper === 'NO ACTION' ? 'RESTRICT' : upper;
}
normalizeFkAction.__type = ['action', 'normalizeFkAction', 'P&2!&/"'];
function compareForeignKeys(entityFKs, dbFKs) {
    // Match by structure including actions, not by name
    // NO ACTION and RESTRICT are treated as equivalent to avoid diff churn
    const fkKey = __assignType((fk) => `${fk.columns.join(',')}→${fk.referencedTable}(${fk.referencedColumns.join(',')})/${normalizeFkAction(fk.onDelete)}/${normalizeFkAction(fk.onUpdate)}`, [() => __ΩForeignKeySchema, 'fk', '', 'Pn!2""/#']);
    const entityKeys = new Map(entityFKs.map(__assignType(fk => [fkKey(fk), fk], ['fk', '', 'P"2!"/"'])));
    const dbKeys = new Map(dbFKs.map(__assignType(fk => [fkKey(fk), fk], ['fk', '', 'P"2!"/"'])));
    const added = entityFKs.filter(__assignType(fk => !dbKeys.has(fkKey(fk)), ['fk', '', 'P"2!"/"']));
    const removed = dbFKs.filter(__assignType(fk => !entityKeys.has(fkKey(fk)), ['fk', '', 'P"2!"/"']));
    return { added, removed };
}
compareForeignKeys.__type = [() => __ΩForeignKeySchema, 'entityFKs', () => __ΩForeignKeySchema, 'dbFKs', () => __ΩForeignKeySchema, 'added', () => __ΩForeignKeySchema, 'removed', 'compareForeignKeys', 'Pn!F2"n#F2$Pn%F4&n\'F4(M/)'];
function detectEnumChanges(diff, entityTable, dbTable) {
    const dbColMap = new Map(dbTable.columns.map(__assignType(c => [c.name, c], ['c', '', 'P"2!"/"'])));
    for (const col of entityTable.columns) {
        if (col.type !== 'enum' || !col.enumValues || !col.enumTypeName)
            continue;
        const dbCol = dbColMap.get(col.name);
        // New enum type for new columns
        if (!dbCol) {
            diff.addedEnumTypes.push({
                typeName: col.enumTypeName,
                values: col.enumValues
            });
            continue;
        }
        // Existing column: non-enum → enum requires CREATE TYPE
        if (dbCol.type !== 'enum') {
            diff.addedEnumTypes.push({
                typeName: col.enumTypeName,
                values: col.enumValues
            });
            continue;
        }
        // Enum type name changed — need to create the new type and drop the old one
        if (dbCol.enumTypeName && dbCol.enumTypeName !== col.enumTypeName) {
            diff.addedEnumTypes.push({
                typeName: col.enumTypeName,
                values: col.enumValues
            });
            diff.removedEnumTypes.push(dbCol.enumTypeName);
            // The type change will be picked up by compareColumn() → typesMatch() → enumTypeName comparison
            continue;
        }
        // Existing column with enum value changes (same type name)
        if (dbCol.enumValues) {
            const added = col.enumValues.filter(__assignType(v => !dbCol.enumValues.includes(v), ['v', '', 'P"2!"/"']));
            const removed = dbCol.enumValues.filter(__assignType(v => !col.enumValues.includes(v), ['v', '', 'P"2!"/"']));
            if (added.length > 0 || removed.length > 0) {
                diff.modifiedEnumTypes.push({
                    typeName: col.enumTypeName,
                    added,
                    removed,
                    newValues: col.enumValues,
                    tableName: diff.tableName,
                    columnName: col.name
                });
            }
        }
    }
    // Detect orphaned enum types from removed columns or enum→non-enum changes
    const entityColMap = new Map(entityTable.columns.map(__assignType(c => [c.name, c], ['c', '', 'P"2!"/"'])));
    for (const dbCol of dbTable.columns) {
        if (dbCol.type !== 'enum' || !dbCol.enumTypeName)
            continue;
        const entityCol = entityColMap.get(dbCol.name);
        // Column removed entirely — its enum type may be orphaned
        // Column changed from enum to non-enum — old type may be orphaned
        if (!entityCol || entityCol.type !== 'enum' || entityCol.enumTypeName !== dbCol.enumTypeName) {
            // Only schedule drop if not already handled above (type-name change already pushes to removedEnumTypes)
            if (!diff.removedEnumTypes.includes(dbCol.enumTypeName)) {
                diff.removedEnumTypes.push(dbCol.enumTypeName);
            }
        }
    }
}
detectEnumChanges.__type = [() => __ΩTableDiff, 'diff', () => __ΩTableSchema, 'entityTable', () => __ΩTableSchema, 'dbTable', 'detectEnumChanges', 'Pn!2"n#2$n%2&$/\''];
function arraysEqual(a, b) {
    if (a.length !== b.length)
        return false;
    return a.every(__assignType((v, i) => v === b[i], ['v', 'i', '', 'P"2!"2""/#']));
}
arraysEqual.__type = ['a', 'b', 'arraysEqual', 'P"F2!"F2")/#'];
//# sourceMappingURL=comparator.js.map