use std::collections::{BTreeSet, HashMap, HashSet};

use thiserror::Error;

use crate::dialects::Dialect;
use crate::operations::Operation;
use crate::states::{
    Column, EnumDef, ExtensionDef, FunctionDef,
    Schema, Table, ViewDef,
};

#[derive(Debug, Error)]
pub enum DiffError {
    #[error("dependency cycle detected among operations — this is a bug in the diff engine")]
    DependencyCycle,
}

// Walk the schema maps directly. The BTreeMap keys are the canonical identity
// for each entity, so a plain keyed comparison is enough here.

pub fn generate_diff(current: &Schema, previous: &Schema) -> Vec<Operation> {
    let mut ops: Vec<Operation> = Vec::new();

    diff_map(&current.extensions, &previous.extensions, &mut ops, diff_extension);
    diff_map(&current.enums, &previous.enums, &mut ops, diff_enum);
    diff_map(&current.functions, &previous.functions, &mut ops, diff_function);
    diff_map(&current.views, &previous.views, &mut ops, diff_view);
    diff_map(&current.tables, &previous.tables, &mut ops, diff_table);

    ops
}

fn diff_map<T: PartialEq>(
    current: &std::collections::BTreeMap<String, T>,
    previous: &std::collections::BTreeMap<String, T>,
    ops: &mut Vec<Operation>,
    handler: fn(Option<&T>, Option<&T>, &mut Vec<Operation>),
) {
    for (key, curr_val) in current {
        match previous.get(key) {
            None => handler(Some(curr_val), None, ops),
            Some(prev_val) if prev_val != curr_val => handler(Some(curr_val), Some(prev_val), ops),
            _ => {}
        }
    }
    for (key, prev_val) in previous {
        if !current.contains_key(key) {
            handler(None, Some(prev_val), ops);
        }
    }
}

fn diff_extension(curr: Option<&ExtensionDef>, prev: Option<&ExtensionDef>, ops: &mut Vec<Operation>) {
    match (curr, prev) {
        (Some(c), None) => ops.push(Operation::CreateExtension { extension: c.clone() }),
        (None, Some(p)) => ops.push(Operation::DropExtension { extension: p.clone() }),
        (Some(c), Some(p)) => {
            ops.push(Operation::DropExtension { extension: p.clone() });
            ops.push(Operation::CreateExtension { extension: c.clone() });
        }
        _ => {}
    }
}

fn diff_enum(curr: Option<&EnumDef>, prev: Option<&EnumDef>, ops: &mut Vec<Operation>) {
    match (curr, prev) {
        (Some(c), None) => ops.push(Operation::CreateEnum { enum_def: c.clone() }),
        (None, Some(p)) => ops.push(Operation::DropEnum { enum_def: p.clone() }),
        (Some(c), Some(p)) => {
            let old_set: HashSet<&str> = p.values.iter().map(|v| v.as_str()).collect();
            let new_set: HashSet<&str> = c.values.iter().map(|v| v.as_str()).collect();
            if old_set.is_subset(&new_set) && p.values == c.values[..p.values.len()] {
                ops.push(Operation::AlterEnum { old: p.clone(), new: c.clone() });
            } else {
                ops.push(Operation::DropEnum { enum_def: p.clone() });
                ops.push(Operation::CreateEnum { enum_def: c.clone() });
            }
        }
        _ => {}
    }
}

fn diff_function(curr: Option<&FunctionDef>, prev: Option<&FunctionDef>, ops: &mut Vec<Operation>) {
    match (curr, prev) {
        (Some(c), None) => ops.push(Operation::CreateFunction { function: c.clone() }),
        (None, Some(p)) => ops.push(Operation::DropFunction { function: p.clone() }),
        (Some(c), Some(p)) => {
            ops.push(Operation::AlterFunction { old: p.clone(), new: c.clone() });
        }
        _ => {}
    }
}

fn diff_view(curr: Option<&ViewDef>, prev: Option<&ViewDef>, ops: &mut Vec<Operation>) {
    match (curr, prev) {
        (Some(c), None) => ops.push(Operation::CreateView { view: c.clone() }),
        (None, Some(p)) => ops.push(Operation::DropView { view: p.clone() }),
        (Some(c), Some(p)) => {
            ops.push(Operation::ReplaceView { old: p.clone(), new: c.clone() });
        }
        _ => {}
    }
}

fn diff_table(curr: Option<&Table>, prev: Option<&Table>, ops: &mut Vec<Operation>) {
    match (curr, prev) {
        (Some(c), None) => ops.push(Operation::CreateTable { table: c.clone() }),
        (None, Some(p)) => ops.push(Operation::DropTable { table: p.clone() }),
        (Some(c), Some(p)) => diff_table_children(p, c, ops),
        _ => {}
    }
}

fn diff_table_children(prev: &Table, curr: &Table, ops: &mut Vec<Operation>) {
    let table_name = curr.qualified_name();

    for change in diff_by_name(&prev.columns, &curr.columns, |c| c.name.as_str()) {
        match change {
            SubChange::Removed(col) => ops.push(Operation::DropColumn {
                table_name: table_name.clone(), column: col.clone(), cascade: false,
            }),
            SubChange::Added(col) => ops.push(Operation::AddColumn {
                table_name: table_name.clone(), column: col.clone(),
            }),
            SubChange::Modified(old, new) => ops.push(Operation::AlterColumn {
                table_name: table_name.clone(), old: old.clone(), new: new.clone(), cast_expr: None,
            }),
        }
    }

    for change in diff_by_name(&prev.foreign_keys, &curr.foreign_keys, |fk| fk.name.as_str()) {
        match change {
            SubChange::Removed(fk) => ops.push(Operation::DropForeignKey {
                table_name: table_name.clone(), foreign_key: fk.clone(), cascade: false,
            }),
            SubChange::Added(fk) => ops.push(Operation::AddForeignKey {
                table_name: table_name.clone(), foreign_key: fk.clone(),
            }),
            SubChange::Modified(old, new) => {
                ops.push(Operation::DropForeignKey {
                    table_name: table_name.clone(), foreign_key: old.clone(), cascade: false,
                });
                ops.push(Operation::AddForeignKey {
                    table_name: table_name.clone(), foreign_key: new.clone(),
                });
            }
        }
    }

    for change in diff_by_name(&prev.indexes, &curr.indexes, |i| i.name.as_str()) {
        match change {
            SubChange::Removed(idx) => ops.push(Operation::DropIndex {
                table_name: table_name.clone(), index: idx.clone(), concurrent: false,
            }),
            SubChange::Added(idx) => ops.push(Operation::AddIndex {
                table_name: table_name.clone(), index: idx.clone(), concurrent: false,
            }),
            SubChange::Modified(old, new) => {
                ops.push(Operation::DropIndex {
                    table_name: table_name.clone(), index: old.clone(), concurrent: false,
                });
                ops.push(Operation::AddIndex {
                    table_name: table_name.clone(), index: new.clone(), concurrent: false,
                });
            }
        }
    }

    for change in diff_by_name(&prev.constraints, &curr.constraints, |c| c.name()) {
        match change {
            SubChange::Removed(con) => ops.push(Operation::DropConstraint {
                table_name: table_name.clone(), constraint: con.clone(),
            }),
            SubChange::Added(con) => ops.push(Operation::AddConstraint {
                table_name: table_name.clone(), constraint: con.clone(),
            }),
            SubChange::Modified(old, new) => {
                ops.push(Operation::DropConstraint {
                    table_name: table_name.clone(), constraint: old.clone(),
                });
                ops.push(Operation::AddConstraint {
                    table_name: table_name.clone(), constraint: new.clone(),
                });
            }
        }
    }

    for change in diff_by_name(&prev.triggers, &curr.triggers, |t| t.name.as_deref().unwrap_or("")) {
        match change {
            SubChange::Removed(trg) => ops.push(Operation::DropTrigger {
                table_name: table_name.clone(), trigger: trg.clone(),
            }),
            SubChange::Added(trg) => ops.push(Operation::CreateTrigger {
                table_name: table_name.clone(), trigger: trg.clone(),
            }),
            SubChange::Modified(old, new) => ops.push(Operation::AlterTrigger {
                table_name: table_name.clone(), old: old.clone(), new: new.clone(),
            }),
        }
    }
}

enum SubChange<'a, T> {
    Added(&'a T),
    Removed(&'a T),
    Modified(&'a T, &'a T),
}

fn diff_by_name<'a, T: PartialEq>(
    prev: &'a [T],
    curr: &'a [T],
    name_fn: impl Fn(&T) -> &str,
) -> Vec<SubChange<'a, T>> {
    let prev_map: HashMap<&str, &T> = prev.iter().map(|v| (name_fn(v), v)).collect();
    let curr_map: HashMap<&str, &T> = curr.iter().map(|v| (name_fn(v), v)).collect();
    let mut changes = Vec::new();

    for (&name, &prev_val) in &prev_map {
        if !curr_map.contains_key(name) {
            changes.push(SubChange::Removed(prev_val));
        }
    }
    for (&name, &curr_val) in &curr_map {
        match prev_map.get(name) {
            None => changes.push(SubChange::Added(curr_val)),
            Some(&prev_val) if prev_val != curr_val => changes.push(SubChange::Modified(prev_val, curr_val)),
            _ => {}
        }
    }
    changes
}

// When a function is dropped but a surviving table still carries a trigger
// referencing it, that trigger is not detected by the detection pass (the
// trigger node is identical in both states). We inject the DropTrigger here.
fn inject_orphan_triggers(ops: Vec<Operation>, previous: &Schema) -> Vec<Operation> {
    let dropped_fns: HashSet<String> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropFunction { function } => Some(function.qualified_name()),
            _ => None,
        })
        .collect();

    if dropped_fns.is_empty() {
        return ops;
    }

    let dropped_tables: HashSet<String> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropTable { table } => Some(table.qualified_name()),
            _ => None,
        })
        .collect();

    let existing_drop_keys: HashSet<String> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropTrigger { table_name, trigger } => {
                trigger.name.as_deref().map(|n| format!("{}:{}", table_name, n))
            }
            _ => None,
        })
        .collect();

    let mut injected: Vec<Operation> = Vec::new();
    for (table_name, table) in &previous.tables {
        if dropped_tables.contains(table_name) {
            continue;
        }
        for trg in &table.triggers {
            let references_dropped = trg.function_name.as_deref()
                .map_or(false, |f| dropped_fns.contains(f));
            if !references_dropped {
                continue;
            }
            let key = trg.name.as_deref()
                .map(|n| format!("{}:{}", table_name, n))
                .unwrap_or_default();
            if !existing_drop_keys.contains(&key) {
                injected.push(Operation::DropTrigger {
                    table_name: table_name.clone(),
                    trigger: trg.clone(),
                });
            }
        }
    }

    let mut result = ops;
    result.extend(injected);
    result
}

// Incompatible enum changes (drop+recreate) require surviving columns that use
// the enum to be temporarily cast to text and back. Without this, DropEnum fails
// because the type is still in use.
// Limitation: enum references inside SQL (defaults, check constraints, function
// bodies, view definitions) cannot be tracked without parsing SQL.
fn inject_enum_column_casts(ops: Vec<Operation>, previous: &Schema) -> Vec<Operation> {
    let dropped_enums: HashSet<&str> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropEnum { enum_def } => Some(enum_def.name.as_str()),
            _ => None,
        })
        .collect();
    let created_enums: HashSet<&str> = ops.iter()
        .filter_map(|op| match op {
            Operation::CreateEnum { enum_def } => Some(enum_def.name.as_str()),
            _ => None,
        })
        .collect();

    let recreated: HashSet<&str> = dropped_enums.intersection(&created_enums).copied().collect();
    if recreated.is_empty() {
        return ops;
    }

    let dropped_tables: HashSet<String> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropTable { table } => Some(table.qualified_name()),
            _ => None,
        })
        .collect();

    let touched_columns: HashSet<(&str, &str)> = ops.iter()
        .filter_map(|op| match op {
            Operation::DropColumn { table_name, column, .. } => Some((table_name.as_str(), column.name.as_str())),
            Operation::AlterColumn { table_name, old, .. } => Some((table_name.as_str(), old.name.as_str())),
            _ => None,
        })
        .collect();

    let mut casts: Vec<Operation> = Vec::new();

    for (table_name, table) in &previous.tables {
        if dropped_tables.contains(table_name) {
            continue;
        }
        for col in &table.columns {
            if !recreated.contains(col.col_type.as_str()) {
                continue;
            }
            if touched_columns.contains(&(table_name.as_str(), col.name.as_str())) {
                continue;
            }
            let text_col = Column {
                col_type: "text".to_string(),
                ..col.clone()
            };
            casts.push(Operation::AlterColumn {
                table_name: table_name.clone(),
                old: col.clone(),
                new: text_col.clone(),
                cast_expr: Some(format!("{}::text", col.name)),
            });
            casts.push(Operation::AlterColumn {
                table_name: table_name.clone(),
                old: text_col,
                new: col.clone(),
                cast_expr: Some(format!("{}::{}", col.name, col.col_type)),
            });
        }
    }

    if casts.is_empty() {
        return ops;
    }

    let mut result = Vec::with_capacity(ops.len() + casts.len());
    result.extend(ops);
    result.extend(casts);
    result
}

// Splits compound CreateTable/DropTable into atomic operations so
// the topological sort can order sub-entities independently.
// CreateTable → CreateTable(columns only) + AddFK* + AddIndex* + AddConstraint* + CreateTrigger*
// DropTable → DropTrigger* + DropConstraint* + DropIndex* + DropFK* + DropTable(columns only)
fn decompose(ops: Vec<Operation>) -> Vec<Operation> {
    let mut result = Vec::with_capacity(ops.len() * 2);
    for op in ops {
        match op {
            Operation::CreateTable { mut table } => {
                let fks = std::mem::take(&mut table.foreign_keys);
                let indexes = std::mem::take(&mut table.indexes);
                let constraints = std::mem::take(&mut table.constraints);
                let triggers = std::mem::take(&mut table.triggers);
                let tname = table.name.clone();
                result.push(Operation::CreateTable { table });
                for fk in fks {
                    result.push(Operation::AddForeignKey { table_name: tname.clone(), foreign_key: fk });
                }
                for idx in indexes {
                    result.push(Operation::AddIndex { table_name: tname.clone(), index: idx, concurrent: false });
                }
                for con in constraints {
                    result.push(Operation::AddConstraint { table_name: tname.clone(), constraint: con });
                }
                for trg in triggers {
                    result.push(Operation::CreateTrigger { table_name: tname.clone(), trigger: trg });
                }
            }
            Operation::DropTable { mut table } => {
                let fks = std::mem::take(&mut table.foreign_keys);
                let indexes = std::mem::take(&mut table.indexes);
                let constraints = std::mem::take(&mut table.constraints);
                let triggers = std::mem::take(&mut table.triggers);
                let tname = table.name.clone();
                for trg in triggers {
                    result.push(Operation::DropTrigger { table_name: tname.clone(), trigger: trg });
                }
                for con in constraints {
                    result.push(Operation::DropConstraint { table_name: tname.clone(), constraint: con });
                }
                for idx in indexes {
                    result.push(Operation::DropIndex { table_name: tname.clone(), index: idx, concurrent: false });
                }
                for fk in fks {
                    result.push(Operation::DropForeignKey { table_name: tname.clone(), foreign_key: fk, cascade: false });
                }
                result.push(Operation::DropTable { table });
            }
            other => result.push(other),
        }
    }
    result
}

// After sorting, folds decomposed sub-entity ops back into their parent
// CreateTable when the dialect supports inline definitions (e.g. Postgres
// can inline FKs, indexes, constraints). This produces cleaner migrations.
// FKs are only inlined when the reference is self-referential or points to a
// table that already existed before this migration — cross-table FKs within
// the same migration stay as standalone ops (already sorted after all creates).
fn fk_can_be_inlined(
    to_table: &str,
    owner_table: &str,
    tables_being_created: &HashSet<String>,
) -> bool {
    to_table == owner_table || !tables_being_created.contains(to_table)
}

fn merge_operations(ops: Vec<Operation>, dialect: &Dialect) -> Vec<Operation> {
    let tables_being_created: HashSet<String> = ops.iter()
        .filter_map(|op| match op {
            Operation::CreateTable { table } => Some(table.qualified_name()),
            _ => None,
        })
        .collect();

    let mut result: Vec<Operation> = Vec::with_capacity(ops.len());
    let mut created_tables: HashMap<String, usize> = HashMap::new();
    for op in ops {
        let merged = match &op {
            Operation::AddForeignKey { table_name, foreign_key }
                if created_tables.contains_key(table_name)
                    && dialect.should_merge(table_name, &op)
                    && fk_can_be_inlined(&foreign_key.to_table, table_name, &tables_being_created) =>
            {
                let idx = created_tables[table_name];
                if let Operation::CreateTable { ref mut table } = result[idx] {
                    table.foreign_keys.push(foreign_key.clone());
                }
                true
            }
            Operation::AddIndex { table_name, index, .. }
                if created_tables.contains_key(table_name) && dialect.should_merge(table_name, &op) =>
            {
                let idx = created_tables[table_name];
                if let Operation::CreateTable { ref mut table } = result[idx] {
                    table.indexes.push(index.clone());
                }
                true
            }
            Operation::AddConstraint { table_name, constraint }
                if created_tables.contains_key(table_name) && dialect.should_merge(table_name, &op) =>
            {
                let idx = created_tables[table_name];
                if let Operation::CreateTable { ref mut table } = result[idx] {
                    table.constraints.push(constraint.clone());
                }
                true
            }
            Operation::CreateTrigger { table_name, trigger }
                if created_tables.contains_key(table_name) && dialect.should_merge(table_name, &op) =>
            {
                let idx = created_tables[table_name];
                if let Operation::CreateTable { ref mut table } = result[idx] {
                    table.triggers.push(trigger.clone());
                }
                true
            }
            _ => false,
        };
        if !merged {
            if let Operation::CreateTable { ref table } = op {
                created_tables.insert(table.qualified_name(), result.len());
            }
            result.push(op);
        }
    }
    result
}

// Topologically sort operations with Kahn's algorithm. When multiple nodes are
// otherwise equal, break ties deterministically so output stays reproducible.

pub fn sort_operations(ops: Vec<Operation>) -> Result<Vec<Operation>, DiffError> {
    let n = ops.len();
    if n == 0 {
        return Ok(ops);
    }

    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
    let mut in_deg: Vec<usize> = vec![0; n];

    build_dependency_edges(&ops, &mut adj, &mut in_deg);

    // Kahn's BFS with deterministic tie-breaking via BTreeSet.
    let mut queue: BTreeSet<(u8, u8, String, usize)> = BTreeSet::new();
    for i in 0..n {
        if in_deg[i] == 0 {
            let (tp, sp) = tiebreak_priority(&ops[i]);
            queue.insert((tp, sp, ops[i].entity_name().to_string(), i));
        }
    }

    let mut sorted_indices: Vec<usize> = Vec::with_capacity(n);
    while let Some(entry) = queue.iter().next().cloned() {
        queue.remove(&entry);
        let idx = entry.3;
        sorted_indices.push(idx);
        for &dep in &adj[idx] {
            in_deg[dep] -= 1;
            if in_deg[dep] == 0 {
                let (tp, sp) = tiebreak_priority(&ops[dep]);
                queue.insert((tp, sp, ops[dep].entity_name().to_string(), dep));
            }
        }
    }

    if sorted_indices.len() != n {
        return Err(DiffError::DependencyCycle);
    }

    let mut slots: Vec<Option<Operation>> = ops.into_iter().map(Some).collect();
    let result = sorted_indices.into_iter()
        .map(|i| slots[i].take().expect("index used twice"))
        .collect();
    Ok(result)
}

fn tiebreak_priority(op: &Operation) -> (u8, u8) {
    match op {
        Operation::CreateExtension { .. }                           => (0, 0),
        Operation::CreateEnum { .. } | Operation::AlterEnum { .. }  => (1, 0),
        Operation::DropView { .. }                                  => (2, 0),
        Operation::ReplaceView { .. }                               => (11, 0),
        Operation::DropTable { .. }                                 => (4, 0),
        Operation::CreateFunction { .. } | Operation::AlterFunction { .. } => (5, 0),
        Operation::CreateTable { .. }                               => (6, 0),
        Operation::DropTrigger { .. }      => (8, 0),
        Operation::DropConstraint { .. }   => (8, 1),
        Operation::DropIndex { .. }        => (8, 2),
        Operation::DropForeignKey { .. }   => (8, 3),
        Operation::DropColumn { .. }       => (8, 4),
        Operation::AlterColumn { .. }      => (8, 5),
        Operation::AddColumn { .. }        => (8, 6),
        Operation::AddForeignKey { .. }    => (8, 7),
        Operation::AddIndex { .. }         => (8, 8),
        Operation::AddConstraint { .. }    => (8, 9),
        Operation::CreateTrigger { .. } | Operation::AlterTrigger { .. } => (8, 10),
        Operation::DropFunction { .. }     => (10, 0),
        Operation::CreateView { .. }       => (11, 0),
        Operation::DropEnum { .. }         => (12, 0),
        Operation::DropExtension { .. }    => (13, 0),
        Operation::Statement { .. }
        | Operation::Invoke { .. }
        | Operation::RenameTable { .. }
        | Operation::RenameColumn { .. }   => (8, 5),
    }
}

use crate::states::types::{Dep, EntityKind};

fn build_dependency_edges(ops: &[Operation], adj: &mut [Vec<usize>], in_deg: &mut [usize]) {
    let n = ops.len();
    let mut seen: HashSet<(usize, usize)> = HashSet::new();

    let entity_names: Vec<std::borrow::Cow<str>> = ops.iter().map(|op| op.entity_name()).collect();
    let mut create_idx: HashMap<(EntityKind, &str), Vec<usize>> = HashMap::new();
    let mut drop_idx: HashMap<(EntityKind, &str), Vec<usize>> = HashMap::new();

    for (i, op) in ops.iter().enumerate() {
        if let Some(kind) = op.entity_kind() {
            let name: &str = &entity_names[i];
            if op.is_drop() {
                drop_idx.entry((kind, name)).or_default().push(i);
            } else {
                create_idx.entry((kind, name)).or_default().push(i);
            }
        }
    }

    let mut add_edge = |from: usize, to: usize| {
        if from != to && seen.insert((from, to)) {
            adj[from].push(to);
            in_deg[to] += 1;
        }
    };

    let resolve_create = |dep: &Dep| -> Vec<usize> {
        match &dep.name {
            Some(name) => create_idx.get(&(dep.kind, name.as_str())).cloned().unwrap_or_default(),
            None => create_idx.iter()
                .filter(|((k, _), _)| *k == dep.kind)
                .flat_map(|(_, v)| v.iter().copied())
                .collect(),
        }
    };

    let resolve_drop = |dep: &Dep| -> Vec<usize> {
        match &dep.name {
            Some(name) => drop_idx.get(&(dep.kind, name.as_str())).cloned().unwrap_or_default(),
            None => drop_idx.iter()
                .filter(|((k, _), _)| *k == dep.kind)
                .flat_map(|(_, v)| v.iter().copied())
                .collect(),
        }
    };

    for i in 0..n {
        let op = &ops[i];
        if !op.is_drop() {
            for dep in op.forward_deps() {
                for j in resolve_create(&dep) {
                    add_edge(j, i);
                }
            }
        }
        if !op.is_create() {
            for dep in op.backward_deps() {
                for k in resolve_drop(&dep) {
                    add_edge(i, k);
                }
            }
        }
    }

    // Intra-table: within the same table, sub-entity drops come before adds.
    let mut by_table: HashMap<&str, Vec<usize>> = HashMap::new();
    for i in 0..n {
        if let Some(tn) = ops[i].table_name() {
            let (tp, _) = tiebreak_priority(&ops[i]);
            if tp == 8 {
                by_table.entry(tn).or_default().push(i);
            }
        }
    }
    for indices in by_table.values() {
        for &a in indices {
            for &b in indices {
                if a == b { continue; }
                let (_, spa) = tiebreak_priority(&ops[a]);
                let (_, spb) = tiebreak_priority(&ops[b]);
                if spa < spb {
                    add_edge(a, b);
                }
            }
        }
    }
}

// Public entry point for diff generation.

pub struct DiffEngine;

impl DiffEngine {
    pub fn new() -> Self {
        Self
    }

    pub fn diff(&self, current: &Schema, previous: &Schema, dialect: &Dialect) -> Result<Vec<Operation>, DiffError> {
        let mut current = current.clone();
        let mut previous = previous.clone();
        current.canonicalize(dialect);
        previous.canonicalize(dialect);

        let raw_ops = generate_diff(&current, &previous);
        let ops = inject_orphan_triggers(raw_ops, &previous);
        let ops = inject_enum_column_casts(ops, &previous);
        let ops = decompose(ops);
        let ops = sort_operations(ops)?;
        Ok(merge_operations(ops, dialect))
    }
}

impl Default for DiffEngine {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
#[path = "test_diff.rs"]
mod test_diff;
