//! Schema-only DataFusion catalog provider.
//!
//! This is the core innovation of altimate-core: a DataFusion CatalogProvider that
//! holds ONLY schema metadata (table names, column names, types). No data.
//! No storage. This allows DataFusion to parse SQL, build a LogicalPlan,
//! and validate types — all without executing anything or connecting to a database.

use super::types::{ColumnDef, SchemaDefinition};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion::catalog::{CatalogProvider, SchemaProvider, Session, TableProvider};
use datafusion::common::DataFusionError;
use datafusion::datasource::TableType;
use datafusion::physical_plan::ExecutionPlan;
use datafusion::prelude::Expr;
use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;

/// The default catalog name used when registering tables.
pub const DEFAULT_CATALOG: &str = "altimate-core";
/// The default schema name used when registering tables.
pub const DEFAULT_SCHEMA: &str = "public";

// ─── Catalog Provider ────────────────────────────────────────────────

/// A CatalogProvider that holds only schema information.
/// No data, no storage — just table/column definitions.
pub struct SchemaOnlyCatalog {
    pub(crate) schemas: HashMap<String, Arc<dyn SchemaProvider>>,
}

impl fmt::Debug for SchemaOnlyCatalog {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SchemaOnlyCatalog")
            .field("schemas", &self.schemas.keys().collect::<Vec<_>>())
            .finish()
    }
}

impl SchemaOnlyCatalog {
    /// Create a new schema-only catalog with a single schema provider
    /// registered under the default schema name.
    pub fn new(schema_provider: Arc<dyn SchemaProvider>) -> Self {
        let mut schemas = HashMap::new();
        schemas.insert(DEFAULT_SCHEMA.to_string(), schema_provider);
        Self { schemas }
    }
}

impl CatalogProvider for SchemaOnlyCatalog {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn schema_names(&self) -> Vec<String> {
        self.schemas.keys().cloned().collect()
    }

    fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
        self.schemas.get(name).cloned()
    }
}

// ─── Schema Provider ─────────────────────────────────────────────────

/// A SchemaProvider that holds schema-only table providers.
pub struct SchemaOnlySchemaProvider {
    tables: HashMap<String, Arc<dyn TableProvider>>,
}

impl fmt::Debug for SchemaOnlySchemaProvider {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SchemaOnlySchemaProvider")
            .field("tables", &self.tables.keys().collect::<Vec<_>>())
            .finish()
    }
}

impl SchemaOnlySchemaProvider {
    /// Create a new schema-only schema provider with the given table providers.
    pub fn new(tables: HashMap<String, Arc<dyn TableProvider>>) -> Self {
        Self { tables }
    }
}

#[async_trait::async_trait]
impl SchemaProvider for SchemaOnlySchemaProvider {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn table_names(&self) -> Vec<String> {
        self.tables.keys().cloned().collect()
    }

    async fn table(&self, name: &str) -> Result<Option<Arc<dyn TableProvider>>, DataFusionError> {
        Ok(self.tables.get(name).cloned())
    }

    fn table_exist(&self, name: &str) -> bool {
        self.tables.contains_key(name)
    }
}

// ─── Table Provider ──────────────────────────────────────────────────

/// A TableProvider with schema but NO data.
///
/// DataFusion can parse SQL, build a LogicalPlan, and type-check against
/// this provider — without executing anything. Calling `scan()` will error.
pub struct SchemaOnlyTableProvider {
    table_name: String,
    schema: SchemaRef,
}

impl fmt::Debug for SchemaOnlyTableProvider {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SchemaOnlyTableProvider")
            .field("table_name", &self.table_name)
            .finish()
    }
}

impl SchemaOnlyTableProvider {
    /// Create a new schema-only table provider with the given name and Arrow schema.
    pub fn new(table_name: String, schema: SchemaRef) -> Self {
        Self { table_name, schema }
    }
}

#[async_trait::async_trait]
impl TableProvider for SchemaOnlyTableProvider {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn schema(&self) -> SchemaRef {
        self.schema.clone()
    }

    fn table_type(&self) -> TableType {
        TableType::Base
    }

    async fn scan(
        &self,
        _state: &dyn Session,
        _projection: Option<&Vec<usize>>,
        _filters: &[Expr],
        _limit: Option<usize>,
    ) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
        Err(DataFusionError::NotImplemented(format!(
            "SchemaOnlyTableProvider '{}' does not support execution — \
             it exists only for SQL validation",
            self.table_name
        )))
    }
}

// ─── Type Conversion ─────────────────────────────────────────────────

/// Parse a DECIMAL/NUMERIC type string with optional precision and scale.
/// Returns Decimal128 with the specified precision/scale, or defaults to (38, 10).
fn parse_decimal_type(sql_type: &str) -> DataType {
    let upper = sql_type.to_uppercase();
    if let Some(start) = upper.find('(')
        && let Some(end) = upper.find(')')
    {
        let params = &upper[start + 1..end];
        let parts: Vec<&str> = params.split(',').map(|s| s.trim()).collect();
        if let Some(precision) = parts.first().and_then(|p| p.parse::<u8>().ok()) {
            let scale = parts.get(1).and_then(|s| s.parse::<i8>().ok()).unwrap_or(0);
            return DataType::Decimal128(precision, scale);
        }
    }
    // Default DECIMAL without precision
    DataType::Decimal128(38, 10)
}

/// Convert a SQL type string (e.g., "INT", "VARCHAR", "DECIMAL(10,2)")
/// to an Arrow DataType.
pub fn sql_type_to_arrow(sql_type: &str) -> DataType {
    let upper = sql_type.to_uppercase();
    let base = upper.split('(').next().unwrap_or(&upper).trim();

    match base {
        "INT" | "INTEGER" | "INT4" => DataType::Int32,
        "BIGINT" | "INT8" => DataType::Int64,
        "SMALLINT" | "INT2" => DataType::Int16,
        "TINYINT" => DataType::Int8,
        "FLOAT" | "FLOAT4" | "REAL" => DataType::Float32,
        "DOUBLE" | "FLOAT8" | "DOUBLE PRECISION" => DataType::Float64,
        "DECIMAL" | "NUMERIC" | "NUMBER" => parse_decimal_type(sql_type),
        "VARCHAR" | "TEXT" | "STRING" | "CHAR" | "CHARACTER VARYING" => DataType::Utf8,
        "BOOLEAN" | "BOOL" => DataType::Boolean,
        "DATE" => DataType::Date32,
        "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => {
            DataType::Timestamp(arrow::datatypes::TimeUnit::Microsecond, None)
        }
        "TIMESTAMP_TZ" | "TIMESTAMPTZ" => {
            DataType::Timestamp(arrow::datatypes::TimeUnit::Microsecond, Some("UTC".into()))
        }
        "TIME" => DataType::Time64(arrow::datatypes::TimeUnit::Microsecond),
        "BINARY" | "VARBINARY" | "BYTEA" => DataType::Binary,
        "JSON" | "JSONB" | "VARIANT" | "OBJECT" => DataType::Utf8, // Stored as string
        "ARRAY" => DataType::Utf8,                                 // TODO: typed arrays
        _ => DataType::Utf8,                                       // Default fallback
    }
}

/// Convert a ColumnDef to an Arrow Field.
pub fn column_to_field(col: &ColumnDef) -> Field {
    let data_type = sql_type_to_arrow(&col.data_type);
    Field::new(&col.name, data_type, col.nullable)
}

// ─── Registration ────────────────────────────────────────────────────

/// Register a SchemaDefinition as a DataFusion catalog on the given SessionContext.
///
/// Uses the schema's `database` and `schema_name` fields to determine the
/// catalog and schema names. Falls back to [`DEFAULT_CATALOG`] and
/// [`DEFAULT_SCHEMA`] when not specified, preserving backward compatibility.
///
/// After registration, `ctx.state().statement_to_plan(stmt)` will validate
/// SQL against the schema without needing any data.
pub fn register_schema_catalog(
    ctx: &datafusion::execution::context::SessionContext,
    schema_def: &SchemaDefinition,
) {
    // Lowercase defaults to match DataFusion's GenericDialect identifier normalization.
    let default_catalog = schema_def
        .database
        .as_deref()
        .unwrap_or(DEFAULT_CATALOG)
        .to_lowercase();
    let default_schema = schema_def
        .schema_name
        .as_deref()
        .unwrap_or(DEFAULT_SCHEMA)
        .to_lowercase();

    // Group tables by (catalog, schema). Each table can override both via
    // per-table `database`/`schema` fields, falling back to the top-level defaults.
    // This enables a single YAML to describe tables across multiple catalogs/schemas.
    #[allow(clippy::type_complexity)]
    let mut catalog_schema_tables: HashMap<
        String,
        HashMap<String, HashMap<String, Arc<dyn TableProvider>>>,
    > = HashMap::new();

    for (table_name, table_def) in &schema_def.tables {
        let target_catalog = table_def
            .database
            .as_deref()
            .map(|s| s.to_lowercase())
            .unwrap_or_else(|| default_catalog.clone());
        let target_schema = table_def
            .schema
            .as_deref()
            .map(|s| s.to_lowercase())
            .unwrap_or_else(|| default_schema.clone());

        let fields: Vec<Field> = table_def.columns.iter().map(column_to_field).collect();
        let arrow_schema = Arc::new(Schema::new(fields));
        let lower_name = table_name.to_lowercase();
        let provider = SchemaOnlyTableProvider::new(lower_name.clone(), arrow_schema);

        catalog_schema_tables
            .entry(target_catalog)
            .or_default()
            .entry(target_schema)
            .or_default()
            .insert(lower_name, Arc::new(provider) as Arc<dyn TableProvider>);
    }

    // Register each catalog with its schemas.
    for (cat_name, schema_map) in catalog_schema_tables {
        let mut schemas: HashMap<String, Arc<dyn SchemaProvider>> = HashMap::new();
        for (schema_key, tables) in schema_map {
            schemas.insert(
                schema_key,
                Arc::new(SchemaOnlySchemaProvider::new(tables)) as Arc<dyn SchemaProvider>,
            );
        }
        let catalog = Arc::new(SchemaOnlyCatalog { schemas });
        ctx.register_catalog(&cat_name, catalog);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::types::TableDef;
    use datafusion::execution::context::SessionContext;

    fn test_schema() -> SchemaDefinition {
        let mut tables = HashMap::new();
        tables.insert(
            "users".to_string(),
            TableDef {
                description: None,
                database: None,
                schema: None,
                columns: vec![
                    ColumnDef {
                        name: "id".to_string(),
                        data_type: "INT".to_string(),
                        nullable: false,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                    ColumnDef {
                        name: "name".to_string(),
                        data_type: "VARCHAR".to_string(),
                        nullable: true,
                        description: None,
                        tags: vec![],
                        synonyms: vec![],

                        statistics: None,
                    },
                ],
                primary_key: Some(vec!["id".to_string()]),
                foreign_keys: vec![],
                statistics: None,
                clustering_keys: vec![],
            },
        );
        SchemaDefinition {
            version: "1".to_string(),
            dialect: crate::schema::types::SqlDialect::Generic,
            database: None,
            schema_name: None,
            tables,
            glossary: None,
        }
    }

    #[test]
    fn test_sql_type_to_arrow() {
        assert_eq!(sql_type_to_arrow("INT"), DataType::Int32);
        assert_eq!(sql_type_to_arrow("VARCHAR"), DataType::Utf8);
        assert_eq!(sql_type_to_arrow("BOOLEAN"), DataType::Boolean);
        assert_eq!(sql_type_to_arrow("bigint"), DataType::Int64);
    }

    #[tokio::test]
    async fn test_register_catalog() {
        let schema = test_schema();
        let ctx = SessionContext::new();
        register_schema_catalog(&ctx, &schema);

        // Verify catalog is registered
        let catalog = ctx.catalog(DEFAULT_CATALOG);
        assert!(catalog.is_some());
    }
}
