//! `pgevolve diff` — print the change set from `source` against a live DB.

use anyhow::Result;

use pgevolve_core::catalog::CatalogFilter;
use pgevolve_core::catalog::read_catalog;
use pgevolve_core::diff::diff;

use crate::cli::{DiffArgs, OutputFormat};
use crate::config::PgevolveConfig;
use crate::connection::{connect, resolve_db};
use crate::pg_querier::PgCatalogQuerier;

/// Run `pgevolve diff`.
pub async fn run(args: DiffArgs, cfg: &PgevolveConfig, format: OutputFormat) -> Result<i32> {
    let opts = resolve_db(cfg, &args.db, args.url.as_deref())?;
    let source = pgevolve_core::parse::parse_directory(&cfg.project.schema_dir, &[])
        .map_err(|e| anyhow::anyhow!("parse error: {e}"))?;

    let client = connect(&opts).await?;
    let querier = PgCatalogQuerier::new(client)?;
    let filter = CatalogFilter::new(opts.managed_schemas.clone(), opts.ignore_objects.clone())?;
    let (target, drift) = tokio::task::spawn_blocking(move || read_catalog(&querier, &filter))
        .await
        .map_err(|e| anyhow::anyhow!("join error: {e}"))??;

    let changes = diff(&target, &source, &drift);
    match format {
        OutputFormat::Human => print_human(&changes),
        OutputFormat::Json => print_json(&changes)?,
        OutputFormat::Sql => print_sql(&changes),
    }

    if args.shadow_validate {
        let shadow_cfg = cfg.shadow.as_ref().ok_or_else(|| {
            anyhow::anyhow!("--shadow-validate requires a [shadow] section in pgevolve.toml")
        })?;
        let backend = crate::shadow::resolve(shadow_cfg)?;
        // v0.1: default to PG 17. v0.2 will thread the real major from the
        // live DB connection or from [shadow].postgres_version.
        let major = shadow_cfg
            .postgres_version
            .as_deref()
            .and_then(|v| v.trim().parse::<u32>().ok())
            .unwrap_or(17);
        let report = crate::shadow::validate::cross_check(
            backend.as_ref(),
            &source,
            major,
            args.shadow_strict,
        )
        .await?;
        let mismatch_count = report.canonical_mismatches.len()
            + report.extra_ast_edges.len()
            + report.missing_ast_edges.len();
        if mismatch_count > 0 {
            for m in &report.canonical_mismatches {
                eprintln!(
                    "  canonical mismatch {}: source={:?} catalog={:?}",
                    m.view_qname, m.source_canonical, m.catalog_canonical
                );
            }
            for e in &report.extra_ast_edges {
                eprintln!("  extra AST edge {}: {}", e.view_qname, e.dep_node);
            }
            for m in &report.missing_ast_edges {
                eprintln!(
                    "  missing AST edge {}: {}.{}",
                    m.view_qname, m.ref_schema, m.ref_name
                );
            }
            if args.shadow_strict {
                anyhow::bail!("shadow-validate --strict: {mismatch_count} mismatch(es)");
            }
        }
        let n_edges = report.structural_edges_checked;
        eprintln!(
            "shadow-validate: ok ({n_edges} structural edge(s), {mismatch_count} canonical mismatch(es))"
        );
    }

    // Spec §10.1: `diff` is informational — always exit 0 regardless of change count.
    Ok(0)
}

const fn scope_name(s: &pgevolve_core::ir::publication::PublicationScope) -> &'static str {
    match s {
        pgevolve_core::ir::publication::PublicationScope::AllTables => "AllTables",
        pgevolve_core::ir::publication::PublicationScope::Selective { .. } => "Selective",
    }
}

#[allow(clippy::too_many_lines)] // one arm per Change variant for human-readable formatting; extraction would scatter the labels.
fn print_human(changes: &pgevolve_core::diff::ChangeSet) {
    if changes.is_empty() {
        println!("No changes.");
        return;
    }
    println!("{} change(s):", changes.len());
    for e in changes.iter() {
        let kind = std::mem::discriminant(&e.change);
        let destructive = if e.destructiveness.requires_approval() {
            " [destructive]"
        } else {
            ""
        };
        println!("  - {kind:?}{destructive}");
        // Pretty per-variant detail. The diff Change enum lives in core; we
        // emit a one-line form keyed on the variant.
        match &e.change {
            pgevolve_core::diff::change::Change::CreateSchema(s) => {
                println!("      create schema {}", s.name);
            }
            pgevolve_core::diff::change::Change::DropSchema(n) => {
                println!("      drop schema {n}");
            }
            pgevolve_core::diff::change::Change::CreateTable(t) => {
                println!("      create table {}", t.qname);
            }
            pgevolve_core::diff::change::Change::DropTable { qname, .. } => {
                println!("      drop table {qname}");
            }
            pgevolve_core::diff::change::Change::AlterTable { qname, ops } => {
                println!("      alter table {} ({} op(s))", qname, ops.len());
            }
            pgevolve_core::diff::change::Change::CreateIndex(i) => {
                println!("      create index {}", i.qname);
            }
            pgevolve_core::diff::change::Change::DropIndex(q) => {
                println!("      drop index {q}");
            }
            pgevolve_core::diff::change::Change::ReplaceIndex { to, .. } => {
                println!("      replace index {}", to.qname);
            }
            pgevolve_core::diff::change::Change::CreateSequence(s) => {
                println!("      create sequence {}", s.qname);
            }
            pgevolve_core::diff::change::Change::DropSequence(q) => {
                println!("      drop sequence {q}");
            }
            pgevolve_core::diff::change::Change::AlterSequence { qname, ops } => {
                println!("      alter sequence {} ({} op(s))", qname, ops.len());
            }
            pgevolve_core::diff::change::Change::AlterSchema { name, .. } => {
                println!("      alter schema {name}");
            }
            pgevolve_core::diff::change::Change::ValidateConstraint { table, constraint } => {
                println!("      validate constraint {constraint} on {table}");
            }
            pgevolve_core::diff::change::Change::RecreateIndex { qname } => {
                println!("      recreate invalid index {qname}");
            }
            pgevolve_core::diff::change::Change::View(vc) => {
                use pgevolve_core::diff::change::ViewChange;
                match vc {
                    ViewChange::Create(v) => println!("      create view {}", v.qname),
                    ViewChange::Drop(q) => println!("      drop view {q}"),
                    ViewChange::ReplaceBody {
                        source, strategy, ..
                    } => {
                        use pgevolve_core::diff::change::BodyReplaceStrategy;
                        let compat = match strategy {
                            BodyReplaceStrategy::InPlace => "compatible",
                            BodyReplaceStrategy::Recreate => "incompatible",
                        };
                        println!("      replace view body {} ({compat})", source.qname);
                    }
                    ViewChange::SetReloption { qname, .. } => {
                        println!("      set view reloption {qname}");
                    }
                    ViewChange::SetComment { qname, .. } => {
                        println!("      set view comment {qname}");
                    }
                    ViewChange::SetColumnComment { qname, column, .. } => {
                        println!("      set column comment {qname}.{column}");
                    }
                }
            }
            pgevolve_core::diff::change::Change::AlterViewSetCheckOption { qname, new_value } => {
                let label = match new_value {
                    Some(pgevolve_core::ir::view::CheckOption::Local) => "LOCAL",
                    Some(pgevolve_core::ir::view::CheckOption::Cascaded) => "CASCADED",
                    None => "none",
                };
                println!("      ~ ALTER VIEW {qname} SET CHECK OPTION ({label})");
            }
            pgevolve_core::diff::change::Change::Mv(mc) => {
                use pgevolve_core::diff::change::MvChange;
                match mc {
                    MvChange::Create(mv) => println!("      create materialized view {}", mv.qname),
                    MvChange::Drop(q) => println!("      drop materialized view {q}"),
                    MvChange::ReplaceBody { source, .. } => {
                        println!("      replace mv body {}", source.qname);
                    }
                    MvChange::SetComment { qname, .. } => {
                        println!("      set mv comment {qname}");
                    }
                    MvChange::SetColumnComment { qname, column, .. } => {
                        println!("      set mv column comment {qname}.{column}");
                    }
                }
            }
            pgevolve_core::diff::change::Change::UserType(utc) => {
                use pgevolve_core::diff::change::UserTypeChange;
                match utc {
                    UserTypeChange::Create(t) => println!("      create type {}", t.qname),
                    UserTypeChange::Drop(q) => println!("      drop type {q}"),
                    UserTypeChange::EnumAddValue { qname, value, .. } => {
                        println!("      enum {qname}: add value {value}");
                    }
                    UserTypeChange::EnumRenameValue { qname, from, to } => {
                        println!("      enum {qname}: rename {from} -> {to}");
                    }
                    UserTypeChange::DomainAddCheck { qname, .. } => {
                        println!("      domain {qname}: add check");
                    }
                    UserTypeChange::DomainDropCheck { qname, name } => {
                        println!("      domain {qname}: drop check {name}");
                    }
                    UserTypeChange::DomainSetDefault { qname, .. } => {
                        println!("      domain {qname}: set default");
                    }
                    UserTypeChange::DomainSetNotNull { qname, not_null } => {
                        println!("      domain {qname}: set not null = {not_null}");
                    }
                    UserTypeChange::CompositeAddAttribute { qname, attribute } => {
                        println!("      composite {qname}: add attribute {}", attribute.name);
                    }
                    UserTypeChange::CompositeDropAttribute { qname, name } => {
                        println!("      composite {qname}: drop attribute {name}");
                    }
                    UserTypeChange::CompositeAlterAttributeType {
                        qname, attribute, ..
                    } => {
                        println!("      composite {qname}: alter attribute type {attribute}");
                    }
                    UserTypeChange::SetComment { qname, .. } => {
                        println!("      set type comment {qname}");
                    }
                    UserTypeChange::ReplaceWithCascade { source, .. } => {
                        println!("      replace type {} with cascade", source.qname);
                    }
                }
            }
            pgevolve_core::diff::change::Change::Function(fc) => {
                use pgevolve_core::diff::change::FunctionChange;
                match fc {
                    FunctionChange::Create(f) => {
                        println!("      function {}: create", f.qname);
                    }
                    FunctionChange::Drop { qname, args } => {
                        let arg_sig = args
                            .types
                            .iter()
                            .map(pgevolve_core::ir::column_type::ColumnType::render_sql)
                            .collect::<Vec<_>>()
                            .join(", ");
                        println!("      function {qname}({arg_sig}): drop");
                    }
                    FunctionChange::CreateOrReplace(f) => {
                        println!("      function {}: create or replace", f.qname);
                    }
                    FunctionChange::ReplaceViaDropCreate { source, .. } => {
                        println!("      function {}: drop + recreate", source.qname);
                    }
                    FunctionChange::SetComment { qname, comment, .. } => {
                        if comment.is_some() {
                            println!("      function {qname}: set comment");
                        } else {
                            println!("      function {qname}: clear comment");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Procedure(pc) => {
                use pgevolve_core::diff::change::ProcedureChange;
                match pc {
                    ProcedureChange::Create(p) => {
                        println!("      procedure {}: create", p.qname);
                    }
                    ProcedureChange::Drop(q) => {
                        println!("      procedure {q}: drop");
                    }
                    ProcedureChange::CreateOrReplace(p) => {
                        println!("      procedure {}: create or replace", p.qname);
                    }
                    ProcedureChange::SetComment { qname, comment } => {
                        if comment.is_some() {
                            println!("      procedure {qname}: set comment");
                        } else {
                            println!("      procedure {qname}: clear comment");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Extension(ec) => {
                use pgevolve_core::diff::change::ExtensionChange;
                match ec {
                    ExtensionChange::Create(e) => {
                        println!("      create extension {}", e.name);
                    }
                    ExtensionChange::Drop(n) => {
                        println!("      drop extension {n}");
                    }
                    ExtensionChange::AlterUpdate { name, to_version } => {
                        println!("      alter extension {name} update to {to_version}");
                    }
                    ExtensionChange::ReplaceWithCascade(e) => {
                        println!("      replace extension {} with cascade", e.name);
                    }
                    ExtensionChange::CommentOn { name, comment } => {
                        if comment.is_some() {
                            println!("      extension {name}: set comment");
                        } else {
                            println!("      extension {name}: clear comment");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Trigger(tc) => {
                use pgevolve_core::diff::change::TriggerChange;
                match tc {
                    TriggerChange::Create(t) => {
                        println!("      create trigger {} on {}", t.qname, t.table);
                    }
                    TriggerChange::Drop { qname, table } => {
                        println!("      drop trigger {qname} on {table}");
                    }
                    TriggerChange::Replace(t) => {
                        println!(
                            "      replace trigger {} on {} (drop + recreate)",
                            t.qname, t.table
                        );
                    }
                    TriggerChange::CommentOn {
                        qname,
                        table,
                        comment,
                    } => {
                        if comment.is_some() {
                            println!("      trigger {qname} on {table}: set comment");
                        } else {
                            println!("      trigger {qname} on {table}: clear comment");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Table(tc) => {
                use pgevolve_core::diff::change::TableChange;
                match tc {
                    TableChange::AttachPartition { parent, child, .. } => {
                        println!("      attach partition {child} to {parent}");
                    }
                    TableChange::DetachPartition { parent, child } => {
                        println!("      detach partition {child} from {parent}");
                    }
                }
            }
            pgevolve_core::diff::change::Change::GrantObjectPrivilege { object, grant } => {
                println!(
                    "      grant {} on {} {} to {:?}",
                    grant.privilege.sql_keyword(),
                    object.sql_keyword(),
                    object.render_target(),
                    grant.grantee
                );
            }
            pgevolve_core::diff::change::Change::RevokeObjectPrivilege { object, grant } => {
                println!(
                    "      revoke {} on {} {} from {:?}",
                    grant.privilege.sql_keyword(),
                    object.sql_keyword(),
                    object.render_target(),
                    grant.grantee
                );
            }
            pgevolve_core::diff::change::Change::GrantColumnPrivilege { qname, grant } => {
                println!(
                    "      grant column privilege on {qname} to {:?}",
                    grant.grantee
                );
            }
            pgevolve_core::diff::change::Change::RevokeColumnPrivilege { qname, grant } => {
                println!(
                    "      revoke column privilege on {qname} from {:?}",
                    grant.grantee
                );
            }
            pgevolve_core::diff::change::Change::AlterObjectOwner(op) => {
                let from = op
                    .from
                    .as_ref()
                    .map_or_else(|| "<unknown>".to_string(), ToString::to_string);
                println!(
                    "      alter owner of {} {} from {from} to {}",
                    op.object.sql_keyword(),
                    op.object.render_target(),
                    op.to
                );
            }
            pgevolve_core::diff::change::Change::AlterDefaultPrivileges {
                target_role,
                schema,
                object_type,
                direction,
                grant,
            } => {
                use pgevolve_core::diff::change::GrantDirection;
                let action = match direction {
                    GrantDirection::Grant => "grant",
                    GrantDirection::Revoke => "revoke",
                };
                let in_schema = schema
                    .as_ref()
                    .map_or_else(String::new, |s| format!(" in schema {s}"));
                println!(
                    "      alter default privileges for role {target_role}{in_schema}: {action} {:?} {:?} to {:?}",
                    object_type, grant.privilege, grant.grantee
                );
            }
            // Stage 6 will wire these into proper display.
            pgevolve_core::diff::change::Change::CreatePolicy { table, policy } => {
                println!("      create policy {} on {table}", policy.name);
            }
            pgevolve_core::diff::change::Change::DropPolicy { table, name } => {
                println!("      drop policy {name} on {table}");
            }
            pgevolve_core::diff::change::Change::AlterPolicy { table, policy } => {
                println!("      alter policy {} on {table}", policy.name);
            }
            pgevolve_core::diff::change::Change::SetTableRowSecurity { qname, security } => {
                use pgevolve_core::diff::change::RowSecurity;
                let verb = match security {
                    RowSecurity::Enable => "enable",
                    RowSecurity::Disable => "disable",
                };
                println!("      {verb} row level security on {qname}");
            }
            pgevolve_core::diff::change::Change::SetTableForceRowSecurity { qname, force } => {
                use pgevolve_core::diff::change::ForceRowSecurity;
                let verb = match force {
                    ForceRowSecurity::Force => "force",
                    ForceRowSecurity::NoForce => "no force",
                };
                println!("      {verb} row level security on {qname}");
            }
            pgevolve_core::diff::change::Change::SetTableStorage { qname, .. } => {
                println!("      ~ ALTER TABLE {qname} SET (...)");
            }
            pgevolve_core::diff::change::Change::SetIndexStorage { qname, .. } => {
                println!("      ~ ALTER INDEX {qname} SET (...)");
            }
            pgevolve_core::diff::change::Change::SetMaterializedViewStorage { qname, .. } => {
                println!("      ~ ALTER MATERIALIZED VIEW {qname} SET (...)");
            }
            pgevolve_core::diff::change::Change::UnsupportedDiff { reason } => {
                println!("      unsupported diff: {reason}");
            }
            pgevolve_core::diff::change::Change::Publication(pc) => {
                use pgevolve_core::diff::change::PublicationChange;
                match pc {
                    PublicationChange::Create(p) => {
                        println!("      + CREATE PUBLICATION {}", p.name);
                    }
                    PublicationChange::Drop { name } => {
                        println!("      - DROP PUBLICATION {name}");
                    }
                    PublicationChange::Replace { from, to } => {
                        println!(
                            "      ~ REPLACE PUBLICATION {} (mode {} -> {})",
                            from.name,
                            scope_name(&from.scope),
                            scope_name(&to.scope),
                        );
                    }
                    PublicationChange::AddTable { publication, table } => {
                        println!(
                            "      ~ ALTER PUBLICATION {publication} ADD TABLE {}",
                            table.qname
                        );
                    }
                    PublicationChange::DropTable { publication, qname } => {
                        println!("      ~ ALTER PUBLICATION {publication} DROP TABLE {qname}");
                    }
                    PublicationChange::SetTable { publication, table } => {
                        println!(
                            "      ~ ALTER PUBLICATION {publication} SET TABLE {}",
                            table.qname
                        );
                    }
                    PublicationChange::AddSchema {
                        publication,
                        schema,
                    } => {
                        println!(
                            "      ~ ALTER PUBLICATION {publication} ADD TABLES IN SCHEMA {schema}"
                        );
                    }
                    PublicationChange::DropSchema {
                        publication,
                        schema,
                    } => {
                        println!(
                            "      ~ ALTER PUBLICATION {publication} DROP TABLES IN SCHEMA {schema}"
                        );
                    }
                    PublicationChange::SetPublish { publication, .. } => {
                        println!("      ~ ALTER PUBLICATION {publication} SET (publish = ...)");
                    }
                    PublicationChange::SetViaRoot { publication, value } => {
                        println!(
                            "      ~ ALTER PUBLICATION {publication} SET (publish_via_partition_root = {value})"
                        );
                    }
                    PublicationChange::CommentOn { name, .. } => {
                        println!("      ~ COMMENT ON PUBLICATION {name}");
                    }
                }
            }
            pgevolve_core::diff::change::Change::Statistic(sc) => {
                use pgevolve_core::diff::change::StatisticChange;
                match sc {
                    StatisticChange::Create(s) => {
                        println!("      + CREATE STATISTICS {}", s.qname);
                    }
                    StatisticChange::Drop { qname } => {
                        println!("      - DROP STATISTICS {qname}");
                    }
                    StatisticChange::Replace { from, .. } => {
                        println!("      ~ REPLACE STATISTICS {} (DROP + CREATE)", from.qname);
                    }
                    StatisticChange::AlterSetTarget { qname, value } => {
                        println!("      ~ ALTER STATISTICS {qname} SET STATISTICS {value}");
                    }
                    StatisticChange::CommentOn { qname, comment } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON STATISTICS {qname}");
                        } else {
                            println!("      ~ COMMENT ON STATISTICS {qname} IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Collation(cc) => {
                use pgevolve_core::diff::change::CollationChange;
                match cc {
                    CollationChange::Create(c) => {
                        println!("      + CREATE COLLATION {}", c.qname);
                    }
                    CollationChange::Drop { qname } => {
                        println!("      - DROP COLLATION {qname}");
                    }
                    CollationChange::Rename { from, to } => {
                        println!("      ~ ALTER COLLATION {from} RENAME TO {to}");
                    }
                    CollationChange::Replace { from, .. } => {
                        println!("      ~ REPLACE COLLATION {} (DROP + CREATE)", from.qname);
                    }
                    CollationChange::CommentOn { qname, comment } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON COLLATION {qname}");
                        } else {
                            println!("      ~ COMMENT ON COLLATION {qname} IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Subscription(sc) => {
                use pgevolve_core::diff::change::SubscriptionChange;
                match sc {
                    SubscriptionChange::Create(s) => {
                        println!("      + CREATE SUBSCRIPTION {}", s.name);
                    }
                    SubscriptionChange::Drop { name } => {
                        println!("      - DROP SUBSCRIPTION {name}");
                    }
                    SubscriptionChange::AlterConnection { name, .. } => {
                        println!("      ~ ALTER SUBSCRIPTION {name} CONNECTION ...");
                    }
                    SubscriptionChange::AddPublication { name, publication } => {
                        println!("      ~ ALTER SUBSCRIPTION {name} ADD PUBLICATION {publication}");
                    }
                    SubscriptionChange::DropPublication { name, publication } => {
                        println!(
                            "      ~ ALTER SUBSCRIPTION {name} DROP PUBLICATION {publication}"
                        );
                    }
                    SubscriptionChange::SetOptions { name, .. } => {
                        println!("      ~ ALTER SUBSCRIPTION {name} SET (...)");
                    }
                    SubscriptionChange::CommentOn { name, .. } => {
                        println!("      ~ COMMENT ON SUBSCRIPTION {name}");
                    }
                }
            }
            pgevolve_core::diff::change::Change::EventTrigger(ec) => {
                use pgevolve_core::diff::change::EventTriggerChange;
                match ec {
                    EventTriggerChange::Create(et) => {
                        println!("      + CREATE EVENT TRIGGER {}", et.name);
                    }
                    EventTriggerChange::Replace { to, .. } => {
                        println!("      ~ REPLACE EVENT TRIGGER {} (DROP + CREATE)", to.name);
                    }
                    EventTriggerChange::Drop { name } => {
                        println!("      - DROP EVENT TRIGGER {name}");
                    }
                    EventTriggerChange::AlterEnable { name, enabled } => {
                        println!(
                            "      ~ ALTER EVENT TRIGGER {name} {}",
                            enabled.alter_clause()
                        );
                    }
                    EventTriggerChange::AlterOwner { name, owner } => {
                        println!("      ~ ALTER EVENT TRIGGER {name} OWNER TO {owner}");
                    }
                    EventTriggerChange::CommentOn { name, comment } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON EVENT TRIGGER {name}");
                        } else {
                            println!("      ~ COMMENT ON EVENT TRIGGER {name} IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Aggregate(ac) => {
                use pgevolve_core::diff::change::AggregateChange;
                match ac {
                    AggregateChange::Create(agg) => {
                        println!("      + CREATE AGGREGATE {}", agg.qname);
                    }
                    AggregateChange::Replace { to, .. } => {
                        println!("      ~ REPLACE AGGREGATE {} (DROP + CREATE)", to.qname);
                    }
                    AggregateChange::Drop { qname, .. } => {
                        println!("      - DROP AGGREGATE {qname}");
                    }
                    AggregateChange::AlterOwner { qname, owner, .. } => {
                        println!("      ~ ALTER AGGREGATE {qname} OWNER TO {owner}");
                    }
                    AggregateChange::CommentOn { qname, comment, .. } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON AGGREGATE {qname}");
                        } else {
                            println!("      ~ COMMENT ON AGGREGATE {qname} IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::Cast(cc) => {
                use pgevolve_core::diff::change::CastChange;
                match cc {
                    CastChange::Create(c) => {
                        println!("      + CREATE CAST ({} AS {})", c.source, c.target);
                    }
                    CastChange::Replace { to, .. } => {
                        println!(
                            "      ~ REPLACE CAST ({} AS {}) (DROP + CREATE)",
                            to.source, to.target
                        );
                    }
                    CastChange::Drop { source, target } => {
                        println!("      - DROP CAST ({source} AS {target})");
                    }
                    CastChange::CommentOn {
                        source,
                        target,
                        comment,
                    } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON CAST ({source} AS {target})");
                        } else {
                            println!("      ~ COMMENT ON CAST ({source} AS {target}) IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::TsDictionary(tsd) => {
                use pgevolve_core::diff::change::TsDictionaryChange;
                match tsd {
                    TsDictionaryChange::Create(d) => {
                        println!("      + CREATE TEXT SEARCH DICTIONARY {}", d.qname);
                    }
                    TsDictionaryChange::Replace { to, .. } => {
                        println!(
                            "      ~ REPLACE TEXT SEARCH DICTIONARY {} (DROP + CREATE)",
                            to.qname
                        );
                    }
                    TsDictionaryChange::Drop { qname } => {
                        println!("      - DROP TEXT SEARCH DICTIONARY {qname}");
                    }
                    TsDictionaryChange::AlterOptions { qname, .. } => {
                        println!("      ~ ALTER TEXT SEARCH DICTIONARY {qname} (options)");
                    }
                    TsDictionaryChange::AlterOwner { qname, owner } => {
                        println!("      ~ ALTER TEXT SEARCH DICTIONARY {qname} OWNER TO {owner}");
                    }
                    TsDictionaryChange::CommentOn { qname, comment } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON TEXT SEARCH DICTIONARY {qname}");
                        } else {
                            println!("      ~ COMMENT ON TEXT SEARCH DICTIONARY {qname} IS NULL");
                        }
                    }
                }
            }
            pgevolve_core::diff::change::Change::TsConfiguration(tsc) => {
                use pgevolve_core::diff::change::TsConfigurationChange;
                match tsc {
                    TsConfigurationChange::Create(c) => {
                        println!("      + CREATE TEXT SEARCH CONFIGURATION {}", c.qname);
                    }
                    TsConfigurationChange::Replace { to, .. } => {
                        println!(
                            "      ~ REPLACE TEXT SEARCH CONFIGURATION {} (DROP + CREATE)",
                            to.qname
                        );
                    }
                    TsConfigurationChange::Drop { qname } => {
                        println!("      - DROP TEXT SEARCH CONFIGURATION {qname}");
                    }
                    TsConfigurationChange::AddMapping {
                        qname, token_type, ..
                    } => {
                        println!(
                            "      ~ ALTER TEXT SEARCH CONFIGURATION {qname} ADD MAPPING FOR {token_type}"
                        );
                    }
                    TsConfigurationChange::AlterMapping {
                        qname, token_type, ..
                    } => {
                        println!(
                            "      ~ ALTER TEXT SEARCH CONFIGURATION {qname} ALTER MAPPING FOR {token_type}"
                        );
                    }
                    TsConfigurationChange::DropMapping { qname, token_type } => {
                        println!(
                            "      ~ ALTER TEXT SEARCH CONFIGURATION {qname} DROP MAPPING FOR {token_type}"
                        );
                    }
                    TsConfigurationChange::AlterOwner { qname, owner } => {
                        println!(
                            "      ~ ALTER TEXT SEARCH CONFIGURATION {qname} OWNER TO {owner}"
                        );
                    }
                    TsConfigurationChange::CommentOn { qname, comment } => {
                        if comment.is_some() {
                            println!("      ~ COMMENT ON TEXT SEARCH CONFIGURATION {qname}");
                        } else {
                            println!(
                                "      ~ COMMENT ON TEXT SEARCH CONFIGURATION {qname} IS NULL"
                            );
                        }
                    }
                }
            }
        }
    }
}

fn print_json(changes: &pgevolve_core::diff::ChangeSet) -> Result<()> {
    let s = serde_json::to_string_pretty(changes)?;
    println!("{s}");
    Ok(())
}

fn print_sql(changes: &pgevolve_core::diff::ChangeSet) {
    if changes.is_empty() {
        println!("-- no changes");
        return;
    }
    // Naive form per spec §10.1: emit SQL via the rewrite-pass renderer in
    // pgevolve_core (no online rewrites). Not a valid plan; meant for review.
    println!("-- pgevolve diff --format=sql (no online rewrites)");
    println!("-- run `pgevolve plan` for the real applyable form\n");
    for e in changes.iter() {
        println!("-- {} change", change_kind_name(&e.change));
        match &e.change {
            pgevolve_core::diff::change::Change::CreateSchema(s) => {
                println!("{}", pgevolve_core::plan::rewrite::sql::create_schema(s));
            }
            pgevolve_core::diff::change::Change::DropSchema(n) => {
                println!("{}", pgevolve_core::plan::rewrite::sql::drop_schema(n));
            }
            pgevolve_core::diff::change::Change::CreateTable(t) => {
                println!("{}", pgevolve_core::plan::rewrite::sql::create_table(t));
            }
            pgevolve_core::diff::change::Change::DropTable { qname, .. } => {
                println!("{}", pgevolve_core::plan::rewrite::sql::drop_table(qname));
            }
            pgevolve_core::diff::change::Change::CreateIndex(i) => {
                println!(
                    "{}",
                    pgevolve_core::plan::rewrite::sql::create_index(i, false)
                );
            }
            pgevolve_core::diff::change::Change::DropIndex(q) => {
                println!(
                    "{}",
                    pgevolve_core::plan::rewrite::sql::drop_index(q, false)
                );
            }
            pgevolve_core::diff::change::Change::CreateSequence(s) => {
                println!("{}", pgevolve_core::plan::rewrite::sql::create_sequence(s));
            }
            pgevolve_core::diff::change::Change::DropSequence(q) => {
                println!("{}", pgevolve_core::plan::rewrite::sql::drop_sequence(q));
            }
            other => println!("-- (alter/replace not rendered as standalone SQL): {other:?}"),
        }
        println!();
    }
}

#[allow(clippy::too_many_lines)] // one arm per Change variant returning its name; mechanical enumeration, not logic.
const fn change_kind_name(c: &pgevolve_core::diff::change::Change) -> &'static str {
    use pgevolve_core::diff::change::{
        Change, CollationChange, MvChange, PublicationChange, StatisticChange, SubscriptionChange,
        ViewChange,
    };
    match c {
        Change::CreateSchema(_) => "CreateSchema",
        Change::DropSchema(_) => "DropSchema",
        Change::AlterSchema { .. } => "AlterSchema",
        Change::CreateTable(_) => "CreateTable",
        Change::DropTable { .. } => "DropTable",
        Change::AlterTable { .. } => "AlterTable",
        Change::CreateIndex(_) => "CreateIndex",
        Change::DropIndex(_) => "DropIndex",
        Change::ReplaceIndex { .. } => "ReplaceIndex",
        Change::CreateSequence(_) => "CreateSequence",
        Change::DropSequence(_) => "DropSequence",
        Change::AlterSequence { .. } => "AlterSequence",
        Change::ValidateConstraint { .. } => "ValidateConstraint",
        Change::RecreateIndex { .. } => "RecreateIndex",
        Change::View(ViewChange::Create(_)) => "CreateView",
        Change::View(ViewChange::Drop(_)) => "DropView",
        Change::View(ViewChange::ReplaceBody { .. }) => "ReplaceViewBody",
        Change::View(ViewChange::SetReloption { .. }) => "SetViewReloption",
        Change::View(ViewChange::SetComment { .. }) => "SetViewComment",
        Change::View(ViewChange::SetColumnComment { .. }) => "SetViewColumnComment",
        Change::AlterViewSetCheckOption { .. } => "AlterViewSetCheckOption",
        Change::Mv(MvChange::Create(_)) => "CreateMv",
        Change::Mv(MvChange::Drop(_)) => "DropMv",
        Change::Mv(MvChange::ReplaceBody { .. }) => "ReplaceMvBody",
        Change::Mv(MvChange::SetComment { .. }) => "SetMvComment",
        Change::Mv(MvChange::SetColumnComment { .. }) => "SetMvColumnComment",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::Create(_)) => "CreateType",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::Drop(_)) => "DropType",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::EnumAddValue { .. }) => {
            "EnumAddValue"
        }
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::EnumRenameValue {
            ..
        }) => "EnumRenameValue",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::DomainAddCheck {
            ..
        }) => "DomainAddCheck",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::DomainDropCheck {
            ..
        }) => "DomainDropCheck",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::DomainSetDefault {
            ..
        }) => "DomainSetDefault",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::DomainSetNotNull {
            ..
        }) => "DomainSetNotNull",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::CompositeAddAttribute {
            ..
        }) => "CompositeAddAttribute",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::CompositeDropAttribute {
            ..
        }) => "CompositeDropAttribute",
        Change::UserType(
            pgevolve_core::diff::change::UserTypeChange::CompositeAlterAttributeType { .. },
        ) => "CompositeAlterAttributeType",
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::SetComment { .. }) => {
            "SetTypeComment"
        }
        Change::UserType(pgevolve_core::diff::change::UserTypeChange::ReplaceWithCascade {
            ..
        }) => "ReplaceTypeWithCascade",
        Change::Function(pgevolve_core::diff::change::FunctionChange::Create(_)) => {
            "CreateFunction"
        }
        Change::Function(pgevolve_core::diff::change::FunctionChange::Drop { .. }) => {
            "DropFunction"
        }
        Change::Function(pgevolve_core::diff::change::FunctionChange::CreateOrReplace(_)) => {
            "CreateOrReplaceFunction"
        }
        Change::Function(pgevolve_core::diff::change::FunctionChange::ReplaceViaDropCreate {
            ..
        }) => "ReplaceFunctionViaDropCreate",
        Change::Function(pgevolve_core::diff::change::FunctionChange::SetComment { .. }) => {
            "SetFunctionComment"
        }
        Change::Procedure(pgevolve_core::diff::change::ProcedureChange::Create(_)) => {
            "CreateProcedure"
        }
        Change::Procedure(pgevolve_core::diff::change::ProcedureChange::Drop(_)) => "DropProcedure",
        Change::Procedure(pgevolve_core::diff::change::ProcedureChange::CreateOrReplace(_)) => {
            "CreateOrReplaceProcedure"
        }
        Change::Procedure(pgevolve_core::diff::change::ProcedureChange::SetComment { .. }) => {
            "SetProcedureComment"
        }
        Change::Extension(pgevolve_core::diff::change::ExtensionChange::Create(_)) => {
            "CreateExtension"
        }
        Change::Extension(pgevolve_core::diff::change::ExtensionChange::Drop(_)) => "DropExtension",
        Change::Extension(pgevolve_core::diff::change::ExtensionChange::AlterUpdate { .. }) => {
            "AlterExtensionUpdate"
        }
        Change::Extension(pgevolve_core::diff::change::ExtensionChange::ReplaceWithCascade(_)) => {
            "ReplaceExtensionWithCascade"
        }
        Change::Extension(pgevolve_core::diff::change::ExtensionChange::CommentOn { .. }) => {
            "CommentOnExtension"
        }
        Change::Trigger(pgevolve_core::diff::change::TriggerChange::Create(_)) => "CreateTrigger",
        Change::Trigger(pgevolve_core::diff::change::TriggerChange::Drop { .. }) => "DropTrigger",
        Change::Trigger(pgevolve_core::diff::change::TriggerChange::Replace(_)) => "ReplaceTrigger",
        Change::Trigger(pgevolve_core::diff::change::TriggerChange::CommentOn { .. }) => {
            "CommentOnTrigger"
        }
        Change::Table(pgevolve_core::diff::change::TableChange::AttachPartition { .. }) => {
            "AttachPartition"
        }
        Change::Table(pgevolve_core::diff::change::TableChange::DetachPartition { .. }) => {
            "DetachPartition"
        }
        Change::GrantObjectPrivilege { .. } => "GrantObjectPrivilege",
        Change::RevokeObjectPrivilege { .. } => "RevokeObjectPrivilege",
        Change::GrantColumnPrivilege { .. } => "GrantColumnPrivilege",
        Change::RevokeColumnPrivilege { .. } => "RevokeColumnPrivilege",
        Change::AlterObjectOwner(_) => "AlterObjectOwner",
        Change::AlterDefaultPrivileges { .. } => "AlterDefaultPrivileges",
        Change::CreatePolicy { .. } => "CreatePolicy",
        Change::DropPolicy { .. } => "DropPolicy",
        Change::AlterPolicy { .. } => "AlterPolicy",
        Change::SetTableRowSecurity { .. } => "SetTableRowSecurity",
        Change::SetTableForceRowSecurity { .. } => "SetTableForceRowSecurity",
        Change::SetTableStorage { .. } => "SetTableStorage",
        Change::SetIndexStorage { .. } => "SetIndexStorage",
        Change::SetMaterializedViewStorage { .. } => "SetMaterializedViewStorage",
        Change::UnsupportedDiff { .. } => "UnsupportedDiff",
        Change::Publication(PublicationChange::Create(_)) => "create_publication",
        Change::Publication(PublicationChange::Drop { .. }) => "drop_publication",
        Change::Publication(PublicationChange::Replace { .. }) => "replace_publication",
        Change::Publication(PublicationChange::AddTable { .. }) => "alter_publication_add_table",
        Change::Publication(PublicationChange::DropTable { .. }) => "alter_publication_drop_table",
        Change::Publication(PublicationChange::SetTable { .. }) => "alter_publication_set_table",
        Change::Publication(PublicationChange::AddSchema { .. }) => "alter_publication_add_schema",
        Change::Publication(PublicationChange::DropSchema { .. }) => {
            "alter_publication_drop_schema"
        }
        Change::Publication(PublicationChange::SetPublish { .. }) => {
            "alter_publication_set_publish"
        }
        Change::Publication(PublicationChange::SetViaRoot { .. }) => {
            "alter_publication_set_via_root"
        }
        Change::Publication(PublicationChange::CommentOn { .. }) => "comment_on_publication",
        Change::Statistic(StatisticChange::Create(_)) => "create_statistic",
        Change::Statistic(StatisticChange::Drop { .. }) => "drop_statistic",
        Change::Statistic(StatisticChange::Replace { .. }) => "replace_statistic",
        Change::Statistic(StatisticChange::AlterSetTarget { .. }) => "alter_statistic_set_target",
        Change::Statistic(StatisticChange::CommentOn { .. }) => "comment_on_statistic",
        Change::Subscription(SubscriptionChange::Create(_)) => "create_subscription",
        Change::Subscription(SubscriptionChange::Drop { .. }) => "drop_subscription",
        Change::Subscription(SubscriptionChange::AlterConnection { .. }) => {
            "alter_subscription_connection"
        }
        Change::Subscription(SubscriptionChange::AddPublication { .. }) => {
            "alter_subscription_add_publication"
        }
        Change::Subscription(SubscriptionChange::DropPublication { .. }) => {
            "alter_subscription_drop_publication"
        }
        Change::Subscription(SubscriptionChange::SetOptions { .. }) => {
            "alter_subscription_set_options"
        }
        Change::Subscription(SubscriptionChange::CommentOn { .. }) => "comment_on_subscription",
        Change::Collation(CollationChange::Create(_)) => "create_collation",
        Change::Collation(CollationChange::Drop { .. }) => "drop_collation",
        Change::Collation(CollationChange::Rename { .. }) => "rename_collation",
        Change::Collation(CollationChange::Replace { .. }) => "replace_collation",
        Change::Collation(CollationChange::CommentOn { .. }) => "comment_on_collation",
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::Create(_)) => {
            "create_event_trigger"
        }
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::Replace {
            ..
        }) => "replace_event_trigger",
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::Drop { .. }) => {
            "drop_event_trigger"
        }
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::AlterEnable {
            ..
        }) => "alter_event_trigger_enable",
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::AlterOwner {
            ..
        }) => "alter_event_trigger_owner",
        Change::EventTrigger(pgevolve_core::diff::change::EventTriggerChange::CommentOn {
            ..
        }) => "comment_on_event_trigger",
        Change::Aggregate(pgevolve_core::diff::change::AggregateChange::Create(_)) => {
            "create_aggregate"
        }
        Change::Aggregate(pgevolve_core::diff::change::AggregateChange::Replace { .. }) => {
            "replace_aggregate"
        }
        Change::Aggregate(pgevolve_core::diff::change::AggregateChange::Drop { .. }) => {
            "drop_aggregate"
        }
        Change::Aggregate(pgevolve_core::diff::change::AggregateChange::AlterOwner { .. }) => {
            "alter_aggregate_owner"
        }
        Change::Aggregate(pgevolve_core::diff::change::AggregateChange::CommentOn { .. }) => {
            "comment_on_aggregate"
        }
        Change::Cast(pgevolve_core::diff::change::CastChange::Create(_)) => "create_cast",
        Change::Cast(pgevolve_core::diff::change::CastChange::Replace { .. }) => "replace_cast",
        Change::Cast(pgevolve_core::diff::change::CastChange::Drop { .. }) => "drop_cast",
        Change::Cast(pgevolve_core::diff::change::CastChange::CommentOn { .. }) => {
            "comment_on_cast"
        }
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::Create(_)) => {
            "create_ts_dictionary"
        }
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::Replace {
            ..
        }) => "replace_ts_dictionary",
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::Drop { .. }) => {
            "drop_ts_dictionary"
        }
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::AlterOptions {
            ..
        }) => "alter_ts_dictionary_options",
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::AlterOwner {
            ..
        }) => "alter_ts_dictionary_owner",
        Change::TsDictionary(pgevolve_core::diff::change::TsDictionaryChange::CommentOn {
            ..
        }) => "comment_on_ts_dictionary",
        Change::TsConfiguration(pgevolve_core::diff::change::TsConfigurationChange::Create(_)) => {
            "create_ts_configuration"
        }
        Change::TsConfiguration(pgevolve_core::diff::change::TsConfigurationChange::Replace {
            ..
        }) => "replace_ts_configuration",
        Change::TsConfiguration(pgevolve_core::diff::change::TsConfigurationChange::Drop {
            ..
        }) => "drop_ts_configuration",
        Change::TsConfiguration(
            pgevolve_core::diff::change::TsConfigurationChange::AddMapping { .. },
        ) => "alter_ts_configuration_add_mapping",
        Change::TsConfiguration(
            pgevolve_core::diff::change::TsConfigurationChange::AlterMapping { .. },
        ) => "alter_ts_configuration_alter_mapping",
        Change::TsConfiguration(
            pgevolve_core::diff::change::TsConfigurationChange::DropMapping { .. },
        ) => "alter_ts_configuration_drop_mapping",
        Change::TsConfiguration(
            pgevolve_core::diff::change::TsConfigurationChange::AlterOwner { .. },
        ) => "alter_ts_configuration_owner",
        Change::TsConfiguration(
            pgevolve_core::diff::change::TsConfigurationChange::CommentOn { .. },
        ) => "comment_on_ts_configuration",
    }
}
