//! DDL parsing and export for altimate-core schemas.
//!
//! Parses `CREATE TABLE` statements from DDL strings and converts them
//! into [`SchemaDefinition`] objects. Also supports exporting schemas
//! back to DDL format.

use std::collections::HashMap;
use std::path::Path;

use sqlparser::ast::{
    ColumnOption, DataType as SqlDataType, ExactNumberInfo, ObjectNamePart, Statement,
    TableConstraint,
};
use sqlparser::dialect::{
    BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, MySqlDialect, PostgreSqlDialect,
    RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect,
};
use sqlparser::parser::Parser;

use super::loader::SchemaLoadError;
use super::types::{
    ColumnDef, ForeignKeyDef, ForeignKeyRef, SchemaDefinition, SqlDialect, TableDef,
};

/// Parse a DDL string into a [`SchemaDefinition`].
///
/// Iterates over all statements in the DDL, processing only `CREATE TABLE`
/// statements. All other statements (INSERT, CREATE INDEX, ALTER, etc.)
/// are silently skipped.
pub fn parse_ddl(ddl_str: &str, dialect: SqlDialect) -> Result<SchemaDefinition, SchemaLoadError> {
    let parser_dialect: Box<dyn sqlparser::dialect::Dialect> = match dialect {
        SqlDialect::Generic | SqlDialect::DataFusion => Box::new(GenericDialect),
        SqlDialect::Snowflake => Box::new(SnowflakeDialect),
        SqlDialect::Postgres | SqlDialect::CockroachDB | SqlDialect::RisingWave => {
            Box::new(PostgreSqlDialect {})
        }
        SqlDialect::BigQuery => Box::new(BigQueryDialect),
        SqlDialect::DuckDB => Box::new(DuckDbDialect),
        SqlDialect::MySQL
        | SqlDialect::SingleStore
        | SqlDialect::TiDB
        | SqlDialect::Doris
        | SqlDialect::StarRocks => Box::new(MySqlDialect {}),
        SqlDialect::Hive | SqlDialect::Spark => Box::new(HiveDialect {}),
        SqlDialect::SQLite => Box::new(SQLiteDialect {}),
        SqlDialect::Redshift => Box::new(RedshiftSqlDialect {}),
        _ => Box::new(GenericDialect),
    };

    let statements = Parser::parse_sql(parser_dialect.as_ref(), ddl_str)
        .map_err(|e| SchemaLoadError::DdlParseError(e.to_string()))?;

    let mut tables = HashMap::new();

    for stmt in statements {
        if let Statement::CreateTable(create_table) = stmt {
            let table_name = extract_table_name(&create_table.name);

            let mut columns = Vec::new();
            let mut pk_columns: Vec<String> = Vec::new();

            // Extract columns and column-level constraints
            for col in &create_table.columns {
                let col_name = col.name.value.clone();
                let data_type = convert_sql_type(&col.data_type);
                let mut nullable = true;

                for opt_def in &col.options {
                    match &opt_def.option {
                        ColumnOption::NotNull => {
                            nullable = false;
                        }
                        ColumnOption::Null => {
                            nullable = true;
                        }
                        ColumnOption::Unique { is_primary, .. } if *is_primary => {
                            pk_columns.push(col_name.clone());
                            nullable = false;
                        }
                        _ => {}
                    }
                }

                columns.push(ColumnDef {
                    name: col_name,
                    data_type,
                    nullable,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],
                    statistics: None,
                });
            }

            // Extract table-level constraints
            let mut foreign_keys = Vec::new();

            for constraint in &create_table.constraints {
                match constraint {
                    TableConstraint::PrimaryKey {
                        columns: pk_cols, ..
                    } => {
                        pk_columns = pk_cols.iter().map(|c| c.value.clone()).collect();
                    }
                    TableConstraint::ForeignKey {
                        columns: fk_cols,
                        foreign_table,
                        referred_columns,
                        ..
                    } => {
                        foreign_keys.push(ForeignKeyDef {
                            columns: fk_cols.iter().map(|c| c.value.clone()).collect(),
                            references: ForeignKeyRef {
                                table: extract_table_name(foreign_table),
                                columns: referred_columns.iter().map(|c| c.value.clone()).collect(),
                            },
                        });
                    }
                    _ => {}
                }
            }

            let primary_key = if pk_columns.is_empty() {
                None
            } else {
                Some(pk_columns)
            };

            tables.insert(
                table_name,
                TableDef {
                    description: None,
                    database: None,
                    schema: None,
                    columns,
                    primary_key,
                    foreign_keys,
                    statistics: None,
                    clustering_keys: vec![],
                },
            );
        }
        // Skip all non-CREATE TABLE statements
    }

    if tables.is_empty() {
        return Err(SchemaLoadError::DdlParseError(
            "No CREATE TABLE statements found in DDL".to_string(),
        ));
    }

    Ok(SchemaDefinition {
        version: "1".to_string(),
        dialect,
        database: None,
        schema_name: None,
        tables,
        glossary: None,
    })
}

/// Parse a DDL file into a [`SchemaDefinition`].
///
/// Reads the file content and delegates to [`parse_ddl`].
pub fn parse_ddl_file(
    path: &Path,
    dialect: SqlDialect,
) -> Result<SchemaDefinition, SchemaLoadError> {
    let content = std::fs::read_to_string(path)?;
    parse_ddl(&content, dialect)
}

/// Convert a sqlparser [`SqlDataType`] to our string representation.
///
/// Maps common SQL data types to normalized uppercase strings.
/// Preserves precision/scale for DECIMAL types.
pub fn convert_sql_type(sql_type: &SqlDataType) -> String {
    match sql_type {
        // Integer types
        SqlDataType::Int(_) | SqlDataType::Int4(_) => "INT".to_string(),
        SqlDataType::Integer(_) => "INT".to_string(),
        SqlDataType::SmallInt(_) | SqlDataType::Int2(_) => "SMALLINT".to_string(),
        SqlDataType::BigInt(_) | SqlDataType::Int8(_) => "BIGINT".to_string(),
        SqlDataType::TinyInt(_) => "TINYINT".to_string(),

        // String types
        SqlDataType::Varchar(_)
        | SqlDataType::CharacterVarying(_)
        | SqlDataType::CharVarying(_) => "VARCHAR".to_string(),
        SqlDataType::Text => "VARCHAR".to_string(),
        SqlDataType::String(_) => "VARCHAR".to_string(),
        SqlDataType::Char(_) | SqlDataType::Character(_) => "CHAR".to_string(),

        // Float types
        SqlDataType::Float(_) | SqlDataType::Float4 => "FLOAT".to_string(),
        SqlDataType::Real => "FLOAT".to_string(),
        SqlDataType::Double(_) | SqlDataType::DoublePrecision | SqlDataType::Float8 => {
            "DOUBLE".to_string()
        }

        // Decimal types
        SqlDataType::Decimal(info) | SqlDataType::Numeric(info) | SqlDataType::Dec(info) => {
            format_decimal_type(info)
        }

        // Boolean
        SqlDataType::Boolean | SqlDataType::Bool => "BOOLEAN".to_string(),

        // Date/Time types
        SqlDataType::Timestamp(_, _) => "TIMESTAMP".to_string(),
        SqlDataType::Date => "DATE".to_string(),
        SqlDataType::Time(_, _) => "TIME".to_string(),
        SqlDataType::Datetime(_) => "DATETIME".to_string(),

        // JSON types
        SqlDataType::JSON | SqlDataType::JSONB => "JSON".to_string(),

        // Binary types
        SqlDataType::Binary(_) | SqlDataType::Varbinary(_) | SqlDataType::Blob(_) => {
            "BINARY".to_string()
        }
        SqlDataType::Bytea => "BINARY".to_string(),

        // Custom types (handle Snowflake/Postgres specials)
        SqlDataType::Custom(name, _modifiers) => {
            let type_name = name.to_string().to_uppercase();
            match type_name.as_str() {
                "NUMBER" => "DECIMAL".to_string(),
                "VARIANT" | "ARRAY" | "OBJECT" => "JSON".to_string(),
                "SERIAL" | "BIGSERIAL" | "SMALLSERIAL" => "INT".to_string(),
                other => other.to_string(),
            }
        }

        // Fallback: use Display format
        other => other.to_string().to_uppercase(),
    }
}

/// Export a [`SchemaDefinition`] to a DDL string of `CREATE TABLE` statements.
///
/// Produces standard SQL DDL with PRIMARY KEY and FOREIGN KEY constraints.
pub fn to_ddl(schema: &SchemaDefinition) -> String {
    let mut output = String::new();

    // Sort tables for deterministic output
    let mut table_names: Vec<&String> = schema.tables.keys().collect();
    table_names.sort();

    for (i, table_name) in table_names.iter().enumerate() {
        let table_def = &schema.tables[*table_name];

        if i > 0 {
            output.push('\n');
        }

        output.push_str(&format!("CREATE TABLE {table_name} (\n"));

        // Columns
        let mut parts: Vec<String> = Vec::new();
        for col in &table_def.columns {
            let mut col_str = format!("    {} {}", col.name, col.data_type);
            if !col.nullable {
                col_str.push_str(" NOT NULL");
            }
            parts.push(col_str);
        }

        // Primary key constraint
        if let Some(pk_cols) = &table_def.primary_key {
            parts.push(format!("    PRIMARY KEY ({})", pk_cols.join(", ")));
        }

        // Foreign key constraints
        for fk in &table_def.foreign_keys {
            parts.push(format!(
                "    FOREIGN KEY ({}) REFERENCES {}({})",
                fk.columns.join(", "),
                fk.references.table,
                fk.references.columns.join(", "),
            ));
        }

        output.push_str(&parts.join(",\n"));
        output.push_str("\n);\n");
    }

    output
}

/// Extract the table name from a sqlparser [`ObjectName`].
///
/// For multi-part names (e.g. `schema.table`), returns only the last part.
pub(crate) fn extract_table_name(name: &sqlparser::ast::ObjectName) -> String {
    name.0
        .last()
        .map(|part| match part {
            ObjectNamePart::Identifier(ident) => ident.value.clone(),
        })
        .unwrap_or_default()
}

/// Format a DECIMAL type with optional precision and scale.
fn format_decimal_type(info: &ExactNumberInfo) -> String {
    match info {
        ExactNumberInfo::None => "DECIMAL".to_string(),
        ExactNumberInfo::Precision(p) => format!("DECIMAL({p})"),
        ExactNumberInfo::PrecisionAndScale(p, s) => format!("DECIMAL({p},{s})"),
    }
}

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

    #[test]
    fn test_parse_simple_ddl() {
        let ddl = "CREATE TABLE users (id INT NOT NULL, name VARCHAR);";
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        assert_eq!(schema.tables.len(), 1);
        assert!(schema.tables.contains_key("users"));
        let table = &schema.tables["users"];
        assert_eq!(table.columns.len(), 2);
        assert_eq!(table.columns[0].name, "id");
        assert!(!table.columns[0].nullable);
        assert_eq!(table.columns[1].name, "name");
        assert!(table.columns[1].nullable);
    }

    #[test]
    fn test_parse_multi_table() {
        let ddl = r#"
            CREATE TABLE users (id INT NOT NULL, name VARCHAR);
            CREATE TABLE orders (id INT NOT NULL, user_id INT NOT NULL);
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        assert_eq!(schema.tables.len(), 2);
        assert!(schema.tables.contains_key("users"));
        assert!(schema.tables.contains_key("orders"));
    }

    #[test]
    fn test_pk_extraction() {
        let ddl = r#"
            CREATE TABLE users (
                id INT NOT NULL,
                name VARCHAR,
                PRIMARY KEY (id)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["users"];
        assert_eq!(table.primary_key, Some(vec!["id".to_string()]));
    }

    #[test]
    fn test_fk_extraction() {
        let ddl = r#"
            CREATE TABLE users (id INT NOT NULL);
            CREATE TABLE orders (
                id INT NOT NULL,
                user_id INT NOT NULL,
                FOREIGN KEY (user_id) REFERENCES users(id)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let orders = &schema.tables["orders"];
        assert_eq!(orders.foreign_keys.len(), 1);
        assert_eq!(orders.foreign_keys[0].columns, vec!["user_id".to_string()]);
        assert_eq!(orders.foreign_keys[0].references.table, "users");
        assert_eq!(
            orders.foreign_keys[0].references.columns,
            vec!["id".to_string()]
        );
    }

    #[test]
    fn test_type_mapping_standard() {
        let ddl = r#"
            CREATE TABLE types_test (
                a INT,
                b BIGINT,
                c SMALLINT,
                d VARCHAR,
                e TEXT,
                f FLOAT,
                g DOUBLE PRECISION,
                h DECIMAL(10,2),
                i BOOLEAN,
                j TIMESTAMP,
                k DATE,
                l BYTEA
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["types_test"];
        assert_eq!(table.columns[0].data_type, "INT");
        assert_eq!(table.columns[1].data_type, "BIGINT");
        assert_eq!(table.columns[2].data_type, "SMALLINT");
        assert_eq!(table.columns[3].data_type, "VARCHAR");
        assert_eq!(table.columns[4].data_type, "VARCHAR");
        assert_eq!(table.columns[5].data_type, "FLOAT");
        assert_eq!(table.columns[6].data_type, "DOUBLE");
        assert_eq!(table.columns[7].data_type, "DECIMAL(10,2)");
        assert_eq!(table.columns[8].data_type, "BOOLEAN");
        assert_eq!(table.columns[9].data_type, "TIMESTAMP");
        assert_eq!(table.columns[10].data_type, "DATE");
        assert_eq!(table.columns[11].data_type, "BINARY");
    }

    #[test]
    fn test_not_null_detection() {
        let ddl = r#"
            CREATE TABLE t (
                a INT NOT NULL,
                b INT NULL,
                c INT
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["t"];
        assert!(
            !table.columns[0].nullable,
            "NOT NULL column should not be nullable"
        );
        assert!(table.columns[1].nullable, "NULL column should be nullable");
        assert!(table.columns[2].nullable, "default should be nullable");
    }

    #[test]
    fn test_mixed_ddl_skips_non_create() {
        let ddl = r#"
            CREATE TABLE users (id INT NOT NULL, name VARCHAR);
            INSERT INTO users VALUES (1, 'Alice');
            CREATE INDEX idx_users_name ON users(name);
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        assert_eq!(schema.tables.len(), 1);
        assert!(schema.tables.contains_key("users"));
    }

    #[test]
    fn test_invalid_ddl_error() {
        let ddl = "THIS IS NOT VALID SQL AT ALL;;;";
        let result = parse_ddl(ddl, SqlDialect::Generic);
        assert!(result.is_err());
    }

    #[test]
    fn test_no_create_table_error() {
        let ddl = "CREATE INDEX idx ON users(name);";
        let result = parse_ddl(ddl, SqlDialect::Generic);
        assert!(result.is_err());
    }

    #[test]
    fn test_to_ddl_roundtrip() {
        let ddl = r#"
            CREATE TABLE users (
                id INT NOT NULL,
                email VARCHAR NOT NULL,
                name VARCHAR,
                PRIMARY KEY (id)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let exported = to_ddl(&schema);

        // Parse the exported DDL and verify it produces the same schema
        let roundtrip = parse_ddl(&exported, SqlDialect::Generic).unwrap();
        assert_eq!(roundtrip.tables.len(), schema.tables.len());

        let orig_table = &schema.tables["users"];
        let rt_table = &roundtrip.tables["users"];
        assert_eq!(orig_table.columns.len(), rt_table.columns.len());
        assert_eq!(orig_table.primary_key, rt_table.primary_key);

        for (orig_col, rt_col) in orig_table.columns.iter().zip(rt_table.columns.iter()) {
            assert_eq!(orig_col.name, rt_col.name);
            assert_eq!(orig_col.data_type, rt_col.data_type);
            assert_eq!(orig_col.nullable, rt_col.nullable);
        }
    }

    #[test]
    fn test_parse_ddl_file() {
        let dir = tempfile::tempdir().unwrap();
        let file_path = dir.path().join("test.sql");
        std::fs::write(
            &file_path,
            "CREATE TABLE t (id INT NOT NULL, name VARCHAR);",
        )
        .unwrap();

        let schema = parse_ddl_file(&file_path, SqlDialect::Generic).unwrap();
        assert_eq!(schema.tables.len(), 1);
        assert!(schema.tables.contains_key("t"));
    }

    #[test]
    fn test_column_level_primary_key() {
        let ddl = "CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR);";
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["t"];
        assert_eq!(table.primary_key, Some(vec!["id".to_string()]));
        // PK columns should be NOT NULL
        assert!(!table.columns[0].nullable);
    }

    #[test]
    fn test_composite_primary_key() {
        let ddl = r#"
            CREATE TABLE t (
                a INT NOT NULL,
                b INT NOT NULL,
                c VARCHAR,
                PRIMARY KEY (a, b)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["t"];
        assert_eq!(
            table.primary_key,
            Some(vec!["a".to_string(), "b".to_string()])
        );
    }

    #[test]
    fn test_multiple_foreign_keys() {
        let ddl = r#"
            CREATE TABLE users (id INT NOT NULL);
            CREATE TABLE products (id INT NOT NULL);
            CREATE TABLE order_items (
                id INT NOT NULL,
                order_id INT NOT NULL,
                product_id INT NOT NULL,
                FOREIGN KEY (order_id) REFERENCES users(id),
                FOREIGN KEY (product_id) REFERENCES products(id)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["order_items"];
        assert_eq!(table.foreign_keys.len(), 2);
    }

    #[test]
    fn test_decimal_precision_variants() {
        let ddl = r#"
            CREATE TABLE t (
                a DECIMAL,
                b DECIMAL(10),
                c DECIMAL(10,2),
                d NUMERIC(5,3)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let table = &schema.tables["t"];
        assert_eq!(table.columns[0].data_type, "DECIMAL");
        assert_eq!(table.columns[1].data_type, "DECIMAL(10)");
        assert_eq!(table.columns[2].data_type, "DECIMAL(10,2)");
        assert_eq!(table.columns[3].data_type, "DECIMAL(5,3)");
    }

    #[test]
    fn test_postgres_dialect() {
        let ddl = r#"
            CREATE TABLE t (
                id SERIAL,
                data JSONB,
                raw BYTEA
            );
        "#;
        // SERIAL is parsed by PostgreSqlDialect as a custom type
        let schema = parse_ddl(ddl, SqlDialect::Postgres).unwrap();
        let table = &schema.tables["t"];
        // SERIAL might be parsed differently depending on dialect
        assert_eq!(table.columns.len(), 3);
    }

    #[test]
    fn test_to_ddl_with_foreign_keys() {
        let ddl = r#"
            CREATE TABLE users (
                id INT NOT NULL,
                PRIMARY KEY (id)
            );
            CREATE TABLE orders (
                id INT NOT NULL,
                user_id INT NOT NULL,
                PRIMARY KEY (id),
                FOREIGN KEY (user_id) REFERENCES users(id)
            );
        "#;
        let schema = parse_ddl(ddl, SqlDialect::Generic).unwrap();
        let exported = to_ddl(&schema);
        assert!(exported.contains("FOREIGN KEY (user_id) REFERENCES users(id)"));
        assert!(exported.contains("PRIMARY KEY (id)"));
    }
}
