//! Migration safety analyzer.
//!
//! Parses migration SQL (ALTER TABLE, DROP TABLE, etc.) and evaluates
//! the safety risk of each operation.

use super::types::*;
use crate::schema::types::SchemaDefinition;

use sqlparser::ast::{
    AlterColumnOperation, AlterTableOperation, ColumnOption, DataType, ObjectType, Statement,
    TableObject,
};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;

/// Analyze a migration SQL for safety risks.
///
/// Parses the SQL using sqlparser and classifies each statement by risk level.
/// Returns a `MigrationResult` with individual findings and an overall risk assessment.
pub fn analyze_migration(sql: &str, _schema: &SchemaDefinition) -> MigrationResult {
    let dialect = GenericDialect {};
    let statements = match Parser::parse_sql(&dialect, sql) {
        Ok(stmts) => stmts,
        Err(e) => {
            return MigrationResult {
                sql: sql.to_string(),
                overall_risk: MigrationRisk::Destructive,
                findings: vec![MigrationFinding {
                    risk: MigrationRisk::Destructive,
                    operation: "PARSE ERROR".to_string(),
                    message: format!("Failed to parse migration SQL: {e}"),
                    mitigation: Some("Fix the SQL syntax before analyzing".to_string()),
                    rollback_sql: None,
                }],
                safe: false,
                rollback_sql: None,
            };
        }
    };

    let mut findings = Vec::new();
    let mut rollback_parts = Vec::new();

    for stmt in &statements {
        analyze_statement(stmt, &mut findings, &mut rollback_parts);
    }

    let overall_risk = findings
        .iter()
        .map(|f| f.risk)
        .max()
        .unwrap_or(MigrationRisk::Safe);

    let safe = overall_risk <= MigrationRisk::Caution;

    let rollback_sql = if rollback_parts.is_empty() {
        None
    } else {
        Some(rollback_parts.join("\n"))
    };

    MigrationResult {
        sql: sql.to_string(),
        overall_risk,
        findings,
        safe,
        rollback_sql,
    }
}

/// Analyze a single SQL statement and append findings.
fn analyze_statement(
    stmt: &Statement,
    findings: &mut Vec<MigrationFinding>,
    rollback_parts: &mut Vec<String>,
) {
    match stmt {
        Statement::AlterTable {
            name, operations, ..
        } => {
            let table_name = extract_object_name(name);
            for op in operations {
                analyze_alter_operation(&table_name, op, findings, rollback_parts);
            }
        }

        Statement::Drop {
            object_type, names, ..
        } => match object_type {
            ObjectType::Table => {
                for name in names {
                    let tbl = extract_object_name(name);
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Destructive,
                        operation: format!("DROP TABLE {tbl}"),
                        message: format!("Dropping table '{tbl}' will permanently delete all data"),
                        mitigation: Some(
                            "Back up the table data before dropping. Consider renaming instead."
                                .to_string(),
                        ),
                        rollback_sql: None,
                    });
                    rollback_parts.push(format!("-- DROP TABLE {tbl}: irreversible, data is lost"));
                }
            }
            ObjectType::Index => {
                for name in names {
                    let idx = extract_object_name(name);
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Caution,
                        operation: format!("DROP INDEX {idx}"),
                        message: format!("Dropping index '{idx}' may impact query performance"),
                        mitigation: Some(
                            "Verify no critical queries depend on this index".to_string(),
                        ),
                        rollback_sql: Some(format!(
                            "-- Recreate index: CREATE INDEX {idx} ON <table>(<columns>);"
                        )),
                    });
                    rollback_parts.push(format!(
                        "-- DROP INDEX {idx}: recreate with CREATE INDEX {idx} ON <table>(<columns>);"
                    ));
                }
            }
            _ => {
                for name in names {
                    let obj = extract_object_name(name);
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Caution,
                        operation: format!("DROP {object_type} {obj}"),
                        message: format!("Dropping {object_type} '{obj}'"),
                        mitigation: None,
                        rollback_sql: None,
                    });
                }
            }
        },

        Statement::Truncate { table_names, .. } => {
            for target in table_names {
                let tbl = extract_object_name(&target.name);
                findings.push(MigrationFinding {
                    risk: MigrationRisk::Destructive,
                    operation: format!("TRUNCATE TABLE {tbl}"),
                    message: format!("Truncating table '{tbl}' will permanently delete all rows"),
                    mitigation: Some("Back up the table data before truncating".to_string()),
                    rollback_sql: None,
                });
                rollback_parts.push(format!(
                    "-- TRUNCATE TABLE {tbl}: irreversible, data is lost"
                ));
            }
        }

        Statement::CreateTable(create_table) => {
            let tbl = extract_object_name(&create_table.name);
            findings.push(MigrationFinding {
                risk: MigrationRisk::Safe,
                operation: format!("CREATE TABLE {tbl}"),
                message: format!("Creating new table '{tbl}'"),
                mitigation: None,
                rollback_sql: Some(format!("DROP TABLE IF EXISTS {tbl};")),
            });
            rollback_parts.push(format!("DROP TABLE IF EXISTS {tbl};"));
        }

        Statement::CreateIndex(create_index) => {
            let idx_name = create_index
                .name
                .as_ref()
                .map(extract_object_name)
                .unwrap_or_else(|| "<unnamed>".to_string());
            let tbl = extract_object_name(&create_index.table_name);
            findings.push(MigrationFinding {
                risk: MigrationRisk::Safe,
                operation: format!("CREATE INDEX {idx_name} ON {tbl}"),
                message: format!("Creating index '{idx_name}' on table '{tbl}'"),
                mitigation: None,
                rollback_sql: Some(format!("DROP INDEX IF EXISTS {idx_name};")),
            });
            rollback_parts.push(format!("DROP INDEX IF EXISTS {idx_name};"));
        }

        Statement::RenameTable(renames) => {
            for rename in renames {
                let old = extract_object_name(&rename.old_name);
                let new = extract_object_name(&rename.new_name);
                findings.push(MigrationFinding {
                    risk: MigrationRisk::Dangerous,
                    operation: format!("RENAME TABLE {old} TO {new}"),
                    message: format!(
                        "Renaming table '{old}' to '{new}' will break existing queries referencing '{old}'"
                    ),
                    mitigation: Some(
                        "Update all queries and application code that reference the old table name"
                            .to_string(),
                    ),
                    rollback_sql: Some(format!("ALTER TABLE {new} RENAME TO {old};")),
                });
                rollback_parts.push(format!("ALTER TABLE {new} RENAME TO {old};"));
            }
        }

        Statement::Insert(insert) => {
            let tbl = match &insert.table {
                TableObject::TableName(name) => extract_object_name(name),
                TableObject::TableFunction(_) => "<function>".to_string(),
            };
            findings.push(MigrationFinding {
                risk: MigrationRisk::Safe,
                operation: format!("INSERT INTO {tbl}"),
                message: format!("Inserting data into '{tbl}'"),
                mitigation: None,
                rollback_sql: None,
            });
        }

        _ => {
            findings.push(MigrationFinding {
                risk: MigrationRisk::Caution,
                operation: stmt_type_name(stmt),
                message: format!("Unrecognized migration statement: {}", stmt_type_name(stmt)),
                mitigation: Some("Review this statement manually".to_string()),
                rollback_sql: None,
            });
        }
    }
}

/// Analyze an ALTER TABLE operation.
fn analyze_alter_operation(
    table_name: &str,
    op: &AlterTableOperation,
    findings: &mut Vec<MigrationFinding>,
    rollback_parts: &mut Vec<String>,
) {
    match op {
        AlterTableOperation::DropColumn { column_name, .. } => {
            let col = &column_name.value;
            findings.push(MigrationFinding {
                risk: MigrationRisk::Destructive,
                operation: format!("DROP COLUMN {table_name}.{col}"),
                message: format!(
                    "Dropping column '{col}' from table '{table_name}' will permanently delete column data"
                ),
                mitigation: Some(
                    "Back up column data before dropping. Consider marking as deprecated instead."
                        .to_string(),
                ),
                rollback_sql: None,
            });
            rollback_parts.push(format!(
                "-- DROP COLUMN {table_name}.{col}: irreversible, column data is lost"
            ));
        }

        AlterTableOperation::AddColumn { column_def, .. } => {
            let col = &column_def.name.value;
            let data_type = &column_def.data_type;
            let has_not_null = column_def
                .options
                .iter()
                .any(|o| matches!(o.option, ColumnOption::NotNull));
            let has_default = column_def
                .options
                .iter()
                .any(|o| matches!(o.option, ColumnOption::Default(_)));

            let (risk, message, mitigation) = if has_not_null && !has_default {
                (
                    MigrationRisk::Dangerous,
                    format!(
                        "Adding NOT NULL column '{col}' to '{table_name}' without a DEFAULT will fail if the table has existing rows"
                    ),
                    Some("Add a DEFAULT value or make the column nullable".to_string()),
                )
            } else {
                (
                    MigrationRisk::Safe,
                    format!("Adding column '{col}' ({data_type}) to table '{table_name}'"),
                    None,
                )
            };

            findings.push(MigrationFinding {
                risk,
                operation: format!("ADD COLUMN {table_name}.{col}"),
                message,
                mitigation,
                rollback_sql: Some(format!("ALTER TABLE {table_name} DROP COLUMN {col};")),
            });
            rollback_parts.push(format!("ALTER TABLE {table_name} DROP COLUMN {col};"));
        }

        AlterTableOperation::AlterColumn { column_name, op } => {
            let col = &column_name.value;
            match op {
                AlterColumnOperation::SetNotNull => {
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Dangerous,
                        operation: format!("SET NOT NULL {table_name}.{col}"),
                        message: format!(
                            "Setting column '{col}' in '{table_name}' to NOT NULL will fail if any existing rows contain NULL"
                        ),
                        mitigation: Some(
                            "Ensure all existing rows have non-NULL values for this column first"
                                .to_string(),
                        ),
                        rollback_sql: Some(format!(
                            "ALTER TABLE {table_name} ALTER COLUMN {col} DROP NOT NULL;"
                        )),
                    });
                    rollback_parts.push(format!(
                        "ALTER TABLE {table_name} ALTER COLUMN {col} DROP NOT NULL;"
                    ));
                }

                AlterColumnOperation::DropNotNull => {
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Safe,
                        operation: format!("DROP NOT NULL {table_name}.{col}"),
                        message: format!(
                            "Removing NOT NULL constraint from column '{col}' in '{table_name}'"
                        ),
                        mitigation: None,
                        rollback_sql: Some(format!(
                            "ALTER TABLE {table_name} ALTER COLUMN {col} SET NOT NULL;"
                        )),
                    });
                    rollback_parts.push(format!(
                        "ALTER TABLE {table_name} ALTER COLUMN {col} SET NOT NULL;"
                    ));
                }

                AlterColumnOperation::SetDataType {
                    data_type: new_type,
                    ..
                } => {
                    let risk = classify_type_change(new_type);
                    let risk_label = match risk {
                        MigrationRisk::Safe => "safe (widening)",
                        MigrationRisk::Dangerous => "dangerous (possible data loss)",
                        MigrationRisk::Destructive => "destructive (incompatible types)",
                        MigrationRisk::Caution => "caution",
                    };
                    findings.push(MigrationFinding {
                        risk,
                        operation: format!("ALTER TYPE {table_name}.{col} → {new_type}"),
                        message: format!(
                            "Changing column type of '{col}' in '{table_name}' to {new_type} — {risk_label}"
                        ),
                        mitigation: match risk {
                            MigrationRisk::Safe => None,
                            MigrationRisk::Caution => Some(
                                "Review the type change for potential issues".to_string(),
                            ),
                            MigrationRisk::Dangerous => Some(
                                "Data may be truncated or lost. Test with production data first."
                                    .to_string(),
                            ),
                            MigrationRisk::Destructive => Some(
                                "Types are incompatible. This migration will likely fail or corrupt data."
                                    .to_string(),
                            ),
                        },
                        rollback_sql: Some(format!(
                            "ALTER TABLE {table_name} ALTER COLUMN {col} SET DATA TYPE <original_type>;"
                        )),
                    });
                    rollback_parts.push(format!(
                        "ALTER TABLE {table_name} ALTER COLUMN {col} SET DATA TYPE <original_type>;"
                    ));
                }

                AlterColumnOperation::SetDefault { .. } => {
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Safe,
                        operation: format!("SET DEFAULT {table_name}.{col}"),
                        message: format!(
                            "Setting default value for column '{col}' in '{table_name}'"
                        ),
                        mitigation: None,
                        rollback_sql: Some(format!(
                            "ALTER TABLE {table_name} ALTER COLUMN {col} DROP DEFAULT;"
                        )),
                    });
                    rollback_parts.push(format!(
                        "ALTER TABLE {table_name} ALTER COLUMN {col} DROP DEFAULT;"
                    ));
                }

                AlterColumnOperation::DropDefault => {
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Caution,
                        operation: format!("DROP DEFAULT {table_name}.{col}"),
                        message: format!(
                            "Removing default value for column '{col}' in '{table_name}'"
                        ),
                        mitigation: Some(
                            "Ensure application code provides explicit values for this column"
                                .to_string(),
                        ),
                        rollback_sql: Some(format!(
                            "ALTER TABLE {table_name} ALTER COLUMN {col} SET DEFAULT <original_default>;"
                        )),
                    });
                    rollback_parts.push(format!(
                        "ALTER TABLE {table_name} ALTER COLUMN {col} SET DEFAULT <original_default>;"
                    ));
                }

                _ => {
                    findings.push(MigrationFinding {
                        risk: MigrationRisk::Caution,
                        operation: format!("ALTER COLUMN {table_name}.{col}"),
                        message: format!("Altering column '{col}' in '{table_name}'"),
                        mitigation: Some("Review this column alteration manually".to_string()),
                        rollback_sql: None,
                    });
                }
            }
        }

        AlterTableOperation::RenameColumn {
            old_column_name,
            new_column_name,
        } => {
            let old = &old_column_name.value;
            let new = &new_column_name.value;
            findings.push(MigrationFinding {
                risk: MigrationRisk::Dangerous,
                operation: format!("RENAME COLUMN {table_name}.{old} → {new}"),
                message: format!(
                    "Renaming column '{old}' to '{new}' in '{table_name}' will break existing queries referencing '{old}'"
                ),
                mitigation: Some(
                    "Update all queries and application code that reference the old column name"
                        .to_string(),
                ),
                rollback_sql: Some(format!(
                    "ALTER TABLE {table_name} RENAME COLUMN {new} TO {old};"
                )),
            });
            rollback_parts.push(format!(
                "ALTER TABLE {table_name} RENAME COLUMN {new} TO {old};"
            ));
        }

        AlterTableOperation::RenameTable {
            table_name: new_name,
        } => {
            let new = extract_object_name(new_name);
            findings.push(MigrationFinding {
                risk: MigrationRisk::Dangerous,
                operation: format!("RENAME TABLE {table_name} → {new}"),
                message: format!(
                    "Renaming table '{table_name}' to '{new}' will break existing queries"
                ),
                mitigation: Some(
                    "Update all queries and application code that reference the old table name"
                        .to_string(),
                ),
                rollback_sql: Some(format!("ALTER TABLE {new} RENAME TO {table_name};")),
            });
            rollback_parts.push(format!("ALTER TABLE {new} RENAME TO {table_name};"));
        }

        AlterTableOperation::DropConstraint { name, .. } => {
            let constraint = &name.value;
            findings.push(MigrationFinding {
                risk: MigrationRisk::Caution,
                operation: format!("DROP CONSTRAINT {table_name}.{constraint}"),
                message: format!(
                    "Dropping constraint '{constraint}' from table '{table_name}'"
                ),
                mitigation: Some(
                    "Ensure data integrity is maintained by other means".to_string(),
                ),
                rollback_sql: Some(format!(
                    "-- Recreate constraint: ALTER TABLE {table_name} ADD CONSTRAINT {constraint} ...;"
                )),
            });
            rollback_parts.push(format!(
                "-- Recreate constraint: ALTER TABLE {table_name} ADD CONSTRAINT {constraint} ...;"
            ));
        }

        AlterTableOperation::AddConstraint(_) => {
            findings.push(MigrationFinding {
                risk: MigrationRisk::Safe,
                operation: format!("ADD CONSTRAINT on {table_name}"),
                message: format!("Adding constraint to table '{table_name}'"),
                mitigation: None,
                rollback_sql: None,
            });
        }

        _ => {
            findings.push(MigrationFinding {
                risk: MigrationRisk::Caution,
                operation: format!("ALTER TABLE {table_name} (unrecognized operation)"),
                message: format!("Unrecognized ALTER TABLE operation on '{table_name}'"),
                mitigation: Some("Review this operation manually".to_string()),
                rollback_sql: None,
            });
        }
    }
}

/// Classify a type change by comparing the new data type.
///
/// Since we don't have the original type from the schema context within
/// the ALTER statement itself, we classify based on the target type's
/// general category. This is a best-effort heuristic.
fn classify_type_change(new_type: &DataType) -> MigrationRisk {
    match new_type {
        // Widening to larger numeric types is generally safe
        DataType::BigInt(_) | DataType::Int8(_) => MigrationRisk::Safe,
        // Widening to text is generally safe
        DataType::Text | DataType::String(_) => MigrationRisk::Safe,
        // Narrowing integer types could truncate
        DataType::SmallInt(_) | DataType::Int2(_) | DataType::TinyInt(_) => {
            MigrationRisk::Dangerous
        }
        // Boolean from numeric is incompatible
        DataType::Boolean | DataType::Bool => MigrationRisk::Dangerous,
        // General numeric types — caution
        DataType::Int(_)
        | DataType::Int4(_)
        | DataType::Integer(_)
        | DataType::Float(_)
        | DataType::Float4
        | DataType::Float8
        | DataType::Real
        | DataType::Double(_)
        | DataType::DoublePrecision => MigrationRisk::Caution,
        // Decimal changes — caution (could narrow or widen)
        DataType::Decimal(_) | DataType::Numeric(_) | DataType::Dec(_) => MigrationRisk::Caution,
        // Varchar changes — caution (could narrow)
        DataType::Varchar(_)
        | DataType::Char(_)
        | DataType::Character(_)
        | DataType::CharacterVarying(_)
        | DataType::CharVarying(_) => MigrationRisk::Caution,
        // Timestamp/date types — caution
        DataType::Timestamp(_, _)
        | DataType::Date
        | DataType::Time(_, _)
        | DataType::Datetime(_) => MigrationRisk::Caution,
        // Default: treat as caution
        _ => MigrationRisk::Caution,
    }
}

/// Extract a simple table/object name from an ObjectName.
fn extract_object_name(name: &sqlparser::ast::ObjectName) -> String {
    name.0
        .iter()
        .map(|part| match part {
            sqlparser::ast::ObjectNamePart::Identifier(ident) => ident.value.clone(),
        })
        .collect::<Vec<_>>()
        .join(".")
}

/// Get a human-readable name for a statement type.
fn stmt_type_name(stmt: &Statement) -> String {
    // Use the first few words of the Display output
    let s = format!("{stmt}");
    s.split_whitespace().take(3).collect::<Vec<_>>().join(" ")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    fn empty_schema() -> SchemaDefinition {
        SchemaDefinition {
            version: "1".to_string(),
            dialect: crate::schema::types::SqlDialect::Generic,
            tables: HashMap::new(),
            database: None,
            schema_name: None,
            glossary: None,
        }
    }

    #[test]
    fn test_drop_table_is_destructive() {
        let result = analyze_migration("DROP TABLE users;", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Destructive);
        assert!(!result.safe);
        assert_eq!(result.findings.len(), 1);
        assert!(result.findings[0].operation.contains("DROP TABLE"));
    }

    #[test]
    fn test_create_table_is_safe() {
        let result = analyze_migration(
            "CREATE TABLE users (id INT NOT NULL, name VARCHAR);",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Safe);
        assert!(result.safe);
        assert!(result.rollback_sql.is_some());
    }

    #[test]
    fn test_drop_column_is_destructive() {
        let result = analyze_migration("ALTER TABLE users DROP COLUMN email;", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Destructive);
        assert!(!result.safe);
    }

    #[test]
    fn test_add_column_with_default_is_safe() {
        let result = analyze_migration(
            "ALTER TABLE users ADD COLUMN age INT DEFAULT 0;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Safe);
        assert!(result.safe);
    }

    #[test]
    fn test_add_not_null_column_without_default_is_dangerous() {
        let result = analyze_migration(
            "ALTER TABLE users ADD COLUMN age INT NOT NULL;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Dangerous);
        assert!(!result.safe);
    }

    #[test]
    fn test_set_not_null_is_dangerous() {
        let result = analyze_migration(
            "ALTER TABLE users ALTER COLUMN email SET NOT NULL;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Dangerous);
        assert!(!result.safe);
    }

    #[test]
    fn test_rename_column_is_dangerous() {
        let result = analyze_migration(
            "ALTER TABLE users RENAME COLUMN email TO email_address;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Dangerous);
        assert!(!result.safe);
    }

    #[test]
    fn test_create_index_is_safe() {
        let result = analyze_migration("CREATE INDEX idx_email ON users(email);", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Safe);
        assert!(result.safe);
    }

    #[test]
    fn test_drop_index_is_caution() {
        let result = analyze_migration("DROP INDEX idx_email;", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Caution);
        assert!(result.safe);
    }

    #[test]
    fn test_drop_constraint_is_caution() {
        let result = analyze_migration(
            "ALTER TABLE orders DROP CONSTRAINT fk_user_id;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Caution);
        assert!(result.safe);
    }

    #[test]
    fn test_truncate_is_destructive() {
        let result = analyze_migration("TRUNCATE TABLE users;", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Destructive);
        assert!(!result.safe);
    }

    #[test]
    fn test_multiple_statements() {
        let sql = r#"
            CREATE TABLE new_table (id INT NOT NULL);
            ALTER TABLE users DROP COLUMN old_col;
            CREATE INDEX idx_name ON users(name);
        "#;
        let result = analyze_migration(sql, &empty_schema());
        // Highest risk is Destructive (from DROP COLUMN)
        assert_eq!(result.overall_risk, MigrationRisk::Destructive);
        assert!(!result.safe);
        assert_eq!(result.findings.len(), 3);
    }

    #[test]
    fn test_alter_column_type() {
        let result = analyze_migration(
            "ALTER TABLE users ALTER COLUMN age SET DATA TYPE BIGINT;",
            &empty_schema(),
        );
        assert_eq!(result.overall_risk, MigrationRisk::Safe);
        assert!(result.safe);
    }

    #[test]
    fn test_invalid_sql() {
        let result = analyze_migration("THIS IS NOT VALID SQL;;;", &empty_schema());
        assert_eq!(result.overall_risk, MigrationRisk::Destructive);
        assert!(!result.safe);
        assert!(result.findings[0].operation.contains("PARSE ERROR"));
    }

    #[test]
    fn test_rollback_sql_generated() {
        let result = analyze_migration(
            "ALTER TABLE users ADD COLUMN age INT DEFAULT 0;",
            &empty_schema(),
        );
        assert!(result.rollback_sql.is_some());
        let rollback = result.rollback_sql.as_ref().expect("should have rollback");
        assert!(rollback.contains("DROP COLUMN age"));
    }
}
