#![warn(missing_docs)]

//! # Schema Module
//!
//! This module provides the core schema definition functionality for Lume.
//! It includes the schema trait, column definitions, and the powerful `define_schema!` macro.
//!
//! ## Key Components
//!
//! - [`Schema`] trait: Core trait for all database schemas
//! - [`Column<T>`]: Type-safe column definition with constraints
//! - [`Value`]: Enum for storing and converting between database values
//! - [`define_schema!`]: Macro for ergonomic schema definition
//!
//! ## Example
//!
//! ```no_run,ignore
//! use lume::define_schema;
//!
//! define_schema! {
//!     User {
//!         id: i32 [primary_key().not_null()],
//!         name: String [not_null()],
//!         email: String [unique()],
//!         age: i32,
//!         active: bool [default_value(true)],
//!     }
//! }
//! ```

mod column;
mod constraints;
mod default;
mod macros;
mod validators;
mod value;

use std::collections::HashMap;
use std::marker::PhantomData;

pub use crate::schema::constraints::ColumnConstraint;
pub use crate::schema::constraints::GeneratedColumn;
pub use crate::schema::default::DefaultToSql;
pub use crate::schema::default::DefaultValueEnum;
pub use crate::schema::validators::ColumnValidators;
use crate::table::TableDefinition;
pub use column::Column;
use std::fmt::Debug;
pub use value::Value;
pub use value::convert_to_value;

/// Core trait that all database schemas must implement.
///
/// This trait provides the interface for schema registration, column retrieval,
/// and table metadata. It's automatically implemented by the `define_schema!` macro.
///
/// # Example
///
/// ```rust
/// use lume::define_schema;
/// use lume::schema::{Schema, ColumnInfo};
///
/// define_schema! {
///     Product {
///         id: i32 [primary_key()],
///         name: String [not_null()],
///     }
/// }
///
/// // The Schema trait is automatically implemented
/// assert_eq!(Product::table_name(), "Product");
/// let columns = Product::get_all_columns();
/// ```
pub trait Schema {
    /// Returns the table name for this schema.
    ///
    /// This is used for SQL generation and table registry.
    fn table_name() -> &'static str;

    /// Returns metadata for all columns in this schema.
    ///
    /// This includes column names, types, constraints, and other metadata
    /// needed for SQL generation and type checking.
    fn get_all_columns() -> Vec<ColumnInfo<'static>>;

    /// Ensures the schema is registered in the table registry.
    ///
    /// This method is idempotent and can be called multiple times safely.
    /// It's automatically called when using the generated schema methods.
    fn ensure_registered();

    /// Returns a map of column names to their corresponding values for this schema instance.
    ///
    /// This method is used to extract the values of all fields in the schema as a
    /// `HashMap<String, Value>`, where each key is the column name and each value is
    /// the associated database value. This is primarily used for insert and update
    /// operations to serialize the struct into a form suitable for database interaction.
    fn values(&self) -> HashMap<String, Value>;
}

/// Trait for extracting updated column values for SQL UPDATE operations.
///
/// The `UpdateTrait` trait is implemented by update structs generated by the
/// `define_schema!` macro. It provides a method to retrieve a vector of
/// column-value pairs representing the fields to be updated in the database.
pub trait UpdateTrait {
    /// Returns a vector of (column name, value) pairs to be updated.
    ///
    /// Only fields that are set (e.g., `Some(value)`) should be included.
    fn get_updated(self) -> Vec<(&'static str, Value)>;
}

/// Trait for specifying column selection in queries.
///
/// The `Select` trait is used to define which columns should be included
/// in a SQL SELECT statement. It is typically implemented by selection
/// structs generated by the `define_schema!` macro, and is used by the
/// query builder to determine which columns to fetch from the database.
///
/// # Example
///
/// ```rust
/// use lume::define_schema;
/// use lume::schema::Select;
/// use lume::schema::ColumnInfo;
/// use lume::schema::Schema;
///
/// define_schema! {
///     User {
///         id: i32 [primary_key()],
///         name: String [not_null()],
///         age: i32,
///     }
/// }
///
/// // The macro generates a selection struct (e.g., SelectUser) that implements Select.
/// // You can use it to specify which columns to select in a query.
/// // let selection = SelectUser::default();
/// // let columns = selection.get_selected();
/// ```
pub trait Select {
    /// Returns the default selection schema.
    ///
    /// This is typically all columns, but can be customized.
    fn default() -> Self;

    /// Returns a vector of column names to be selected.
    ///
    /// This determines which columns will be included in the SQL SELECT clause.
    fn get_selected(self) -> Vec<&'static str>;
}

/// Metadata information for a database column.
///
/// This struct contains all the necessary information about a column
/// for SQL generation, type checking, and constraint validation.
///
/// # Fields
///
/// - `name`: The column name in the database
/// - `data_type`: The SQL data type (e.g., "INTEGER", "VARCHAR(255)")
/// - `nullable`: Whether the column allows NULL values
/// - `unique`: Whether the column has a UNIQUE constraint
/// - `primary_key`: Whether the column is a primary key
/// - `indexed`: Whether the column has an index
/// - `has_default`: Whether the column has a default value
/// - `default_sql`: The SQL representation of the default value
#[derive(Debug, Clone)]
pub struct ColumnInfo<'a> {
    /// The column name in the database
    pub name: &'static str,
    /// The SQL data type (e.g., "INTEGER", "VARCHAR(255)")
    pub data_type: &'static str,
    /// Whether the column allows NULL values
    pub has_default: bool,
    /// The SQL representation of the default value
    pub default_sql: Option<DefaultValueEnum<String>>,
    /// Optional column comment (MySQL COMMENT)
    pub comment: Option<&'static str>,
    /// Optional character set (MySQL CHARACTER SET)
    pub charset: Option<&'static str>,
    /// Optional collation (MySQL COLLATE)
    pub collate: Option<&'static str>,
    /// Validators applied to this column's values at runtime.
    pub validators: &'a Vec<ColumnValidators>,
    /// Constraints applied to this column (e.g., NOT NULL, UNIQUE, PRIMARY KEY).
    pub constraints: &'a Vec<ColumnConstraint>,
}

/// Converts a Rust type to its corresponding SQL type string.
///
/// This function provides the mapping between Rust types and SQL column types
/// used in database schema generation.
///
/// # Supported Types
///
/// - `String` → `"VARCHAR(255)"`
/// - `i8` → `"TINYINT"`
/// - `i16` → `"SMALLINT"`
/// - `i32` → `"INTEGER"`
/// - `i64` → `"BIGINT"`
/// - `u8` → `"TINYINT UNSIGNED"`
/// - `u16` → `"SMALLINT UNSIGNED"`
/// - `u32` → `"INTEGER UNSIGNED"`
/// - `u64` → `"BIGINT UNSIGNED"`
/// - `f32` → `"FLOAT"`
/// - `f64` → `"DOUBLE"`
/// - `bool` → `"BOOLEAN"`
/// - All other types → `"TEXT"` (fallback)
///
/// # Example
///
/// ```rust
/// use lume::schema::type_to_sql_string;
///
/// assert_eq!(type_to_sql_string::<String>(), "VARCHAR(255)");
/// assert_eq!(type_to_sql_string::<i32>(), "INT");
/// assert_eq!(type_to_sql_string::<i64>(), "BIGINT");
/// assert_eq!(type_to_sql_string::<u64>(), "BIGINT UNSIGNED");
/// assert_eq!(type_to_sql_string::<bool>(), "BOOLEAN");
/// ```
pub fn type_to_sql_string<T: 'static>() -> &'static str {
    use std::any::TypeId;

    let type_id = TypeId::of::<T>();

    if type_id == TypeId::of::<String>() {
        "VARCHAR(255)"
    } else if type_id == TypeId::of::<i8>() {
        "TINYINT"
    } else if type_id == TypeId::of::<i16>() {
        "SMALLINT"
    } else if type_id == TypeId::of::<i32>() {
        "INT"
    } else if type_id == TypeId::of::<i64>() {
        "BIGINT"
    } else if type_id == TypeId::of::<u8>() {
        "TINYINT UNSIGNED"
    } else if type_id == TypeId::of::<u16>() {
        "SMALLINT UNSIGNED"
    } else if type_id == TypeId::of::<u32>() {
        "INT UNSIGNED"
    } else if type_id == TypeId::of::<u64>() {
        "BIGINT UNSIGNED"
    } else if type_id == TypeId::of::<f32>() {
        "FLOAT"
    } else if type_id == TypeId::of::<f64>() {
        "DOUBLE"
    } else if type_id == TypeId::of::<bool>() {
        "BOOLEAN"
    } else if type_id == TypeId::of::<time::Date>() {
        "DATE"
    } else if type_id == TypeId::of::<time::OffsetDateTime>() {
        "DATETIME"
    } else {
        "VARCHAR(255)" // fallback
    }
}

/// A wrapper around a schema type that implements [`TableDefinition`].
///
/// This struct is used internally to bridge between the [`Schema`] trait
/// and the [`TableDefinition`] trait for table registry and SQL generation.
///
/// # Type Parameters
///
/// - `T`: The schema type that implements [`Schema`]
#[derive(Debug)]
pub(crate) struct SchemaWrapper<T: Schema + Debug> {
    _phantom: PhantomData<T>,
}

// Implement Clone for SchemaWrapper<T>
impl<T: Schema + Debug> Clone for SchemaWrapper<T> {
    fn clone(&self) -> Self {
        Self {
            _phantom: PhantomData,
        }
    }
}

impl<T: Schema + Debug> SchemaWrapper<T> {
    /// Creates a new `SchemaWrapper` instance.
    pub(crate) fn new() -> Self {
        Self {
            _phantom: PhantomData,
        }
    }
}

impl<T: Schema + Debug + Sync + Send + 'static> TableDefinition for SchemaWrapper<T> {
    fn table_name(&self) -> &'static str {
        T::table_name()
    }

    fn get_columns(&self) -> Vec<ColumnInfo<'static>> {
        T::get_all_columns()
    }

    fn to_create_sql(&self) -> String {
        let table_name = self.table_name();
        let columns = self.get_columns();

        let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (\n", table_name);

        let column_definitions: Vec<String> = columns
            .iter()
            .map(|col| {
                let mut def = format!("    {} {}", col.name, col.data_type);
                let constraints = col.constraints;

                for constraint in constraints {
                    match constraint {
                        ColumnConstraint::NonNullable => {
                            def.push_str(" NOT NULL");
                        }
                        ColumnConstraint::Unique => {
                            def.push_str(" UNIQUE");
                        }
                        ColumnConstraint::PrimaryKey => {
                            def.push_str(" PRIMARY KEY");
                        }
                        ColumnConstraint::Indexed => {}
                        ColumnConstraint::AutoIncrement => {
                            if is_mysql_integer_type(col.data_type) {
                                def.push_str(" AUTO_INCREMENT");
                            }
                        }
                        ColumnConstraint::Invisible => {
                            def.push_str(" INVISIBLE");
                        }
                        ColumnConstraint::OnUpdateCurrentTimestamp => {
                            def.push_str(" ON UPDATE CURRENT_TIMESTAMP");
                        }
                        ColumnConstraint::Check(expression) => {
                            def.push_str(&format!(" CHECK ({})", expression));
                        }
                        ColumnConstraint::Generated(generated) => {
                            def.push_str(&format!(" GENERATED {}", generated));
                        }
                    }
                }

                if col.comment.is_some() {
                    let escaped = col.comment.unwrap().replace("'", "''");
                    def.push_str(&format!(" COMMENT '{}'", escaped));
                }

                if col.charset.is_some() {
                    def.push_str(&format!(" CHARACTER SET {}", col.charset.unwrap()));
                }

                if col.collate.is_some() {
                    def.push_str(&format!(" COLLATE {}", col.collate.unwrap()));
                }

                if col.has_default {
                    if let Some(ref default) = col.default_sql {
                        if let DefaultValueEnum::Value(default) = default {
                            // Add quotes for string default values if not already quoted
                            let needs_quotes = col.data_type == "TEXT"
                                || col.data_type.starts_with("VARCHAR")
                                || col.data_type == "CHAR"
                                || col.data_type == "STRING";
                            if needs_quotes
                                && !(default.starts_with('\'') && default.ends_with('\''))
                            {
                                def.push_str(&format!(
                                    " DEFAULT '{}'",
                                    default.replace('\'', "''")
                                ));
                            } else {
                                def.push_str(&format!(" DEFAULT {}", default));
                            }
                        } else if &DefaultValueEnum::CurrentTimestamp == default {
                            def.push_str(" DEFAULT CURRENT_TIMESTAMP");
                        } else if &DefaultValueEnum::Random == default {
                            def.push_str(" DEFAULT (UUID())");
                        }
                    }
                }

                def
            })
            .collect();

        sql.push_str(&column_definitions.join(",\n"));
        sql.push_str("\n);");

        // Add indexes
        let indexes: Vec<String> = columns
            .iter()
            .filter(|col| {
                col.constraints.contains(&ColumnConstraint::Indexed)
                    && !col.constraints.contains(&ColumnConstraint::PrimaryKey)
            })
            .map(|col| {
                format!(
                    "CREATE INDEX idx_{}_{} ON {} ({});",
                    table_name, col.name, table_name, col.name
                )
            })
            .collect();

        if !indexes.is_empty() {
            sql.push_str("\n\n");
            sql.push_str(&indexes.join("\n"));
        }

        sql
    }

    fn clone_box(&self) -> Box<dyn TableDefinition> {
        Box::new(self.clone())
    }
}

/// Returns true if a MySQL data type string represents an integer type.
fn is_mysql_integer_type(data_type: &str) -> bool {
    match data_type {
        // Signed
        "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "INTEGER" | "BIGINT" |
        // Unsigned variants may appear with a space and suffix
        "TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "MEDIUMINT UNSIGNED" |
        "INT UNSIGNED" | "INTEGER UNSIGNED" | "BIGINT UNSIGNED" => true,
        _ => false,
    }
}

/// Marker trait for user-defined types that should use generic `DefaultToSql`.
///
/// Users can implement this trait for their custom enums or types to enable
/// default value support in schema definitions. Types implementing this trait
/// must also implement `ToString` to provide SQL string representation.
pub trait CustomSqlType {}
