//! Predicate pushdown support for efficient Parquet filtering

use crate::error::{IoError, Result};
use crate::parquet::reader::{ParquetChunkIterator, ParquetData};
use crate::parquet::statistics::read_parquet_statistics;
use arrow::array::RecordBatchReader;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::arrow::ProjectionMask;
use std::fs::File;
use std::path::Path;

/// Predicate for filtering Parquet data
#[derive(Debug, Clone)]
pub enum ParquetPredicate {
    /// Column equals a value
    Eq(String, PredicateValue),

    /// Column not equals a value
    NotEq(String, PredicateValue),

    /// Column less than a value
    Lt(String, PredicateValue),

    /// Column less than or equal to a value
    LtEq(String, PredicateValue),

    /// Column greater than a value
    Gt(String, PredicateValue),

    /// Column greater than or equal to a value
    GtEq(String, PredicateValue),

    /// Column is null
    IsNull(String),

    /// Column is not null
    IsNotNull(String),

    /// Column value in a set
    In(String, Vec<PredicateValue>),

    /// Logical AND of predicates
    And(Vec<ParquetPredicate>),

    /// Logical OR of predicates
    Or(Vec<ParquetPredicate>),

    /// Logical NOT of a predicate
    Not(Box<ParquetPredicate>),
}

/// Value type for predicates
#[derive(Debug, Clone, PartialEq)]
pub enum PredicateValue {
    /// 64-bit floating point
    Float64(f64),
    /// 32-bit floating point
    Float32(f32),
    /// 64-bit integer
    Int64(i64),
    /// 32-bit integer
    Int32(i32),
    /// Boolean
    Boolean(bool),
    /// String
    String(String),
}

impl ParquetPredicate {
    /// Create an equality predicate
    pub fn eq(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::Eq(column.into(), value)
    }

    /// Create a not-equal predicate
    pub fn not_eq(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::NotEq(column.into(), value)
    }

    /// Create a less-than predicate
    pub fn lt(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::Lt(column.into(), value)
    }

    /// Create a less-than-or-equal predicate
    pub fn lt_eq(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::LtEq(column.into(), value)
    }

    /// Create a greater-than predicate
    pub fn gt(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::Gt(column.into(), value)
    }

    /// Create a greater-than-or-equal predicate
    pub fn gt_eq(column: impl Into<String>, value: PredicateValue) -> Self {
        Self::GtEq(column.into(), value)
    }

    /// Create an is-null predicate
    pub fn is_null(column: impl Into<String>) -> Self {
        Self::IsNull(column.into())
    }

    /// Create an is-not-null predicate
    pub fn is_not_null(column: impl Into<String>) -> Self {
        Self::IsNotNull(column.into())
    }

    /// Create an IN predicate
    pub fn in_values(column: impl Into<String>, values: Vec<PredicateValue>) -> Self {
        Self::In(column.into(), values)
    }

    /// Combine predicates with AND
    pub fn and(predicates: Vec<ParquetPredicate>) -> Self {
        Self::And(predicates)
    }

    /// Combine predicates with OR
    pub fn or(predicates: Vec<ParquetPredicate>) -> Self {
        Self::Or(predicates)
    }

    /// Negate a predicate
    pub fn not(predicate: ParquetPredicate) -> Self {
        Self::Not(Box::new(predicate))
    }

    /// Check if this predicate can potentially skip a row group based on statistics
    ///
    /// This performs predicate pushdown by using column statistics to determine
    /// if a row group can be entirely skipped.
    pub fn can_skip_row_group_f64(&self, min: f64, max: f64) -> bool {
        match self {
            ParquetPredicate::Lt(_, PredicateValue::Float64(val)) => min >= *val,
            ParquetPredicate::LtEq(_, PredicateValue::Float64(val)) => min > *val,
            ParquetPredicate::Gt(_, PredicateValue::Float64(val)) => max <= *val,
            ParquetPredicate::GtEq(_, PredicateValue::Float64(val)) => max < *val,
            ParquetPredicate::Eq(_, PredicateValue::Float64(val)) => max < *val || min > *val,
            ParquetPredicate::And(predicates) => predicates
                .iter()
                .any(|p| p.can_skip_row_group_f64(min, max)),
            ParquetPredicate::Or(predicates) => predicates
                .iter()
                .all(|p| p.can_skip_row_group_f64(min, max)),
            _ => false, // Conservative: don't skip if we can't determine
        }
    }

    /// Get the column name referenced by this predicate (if single column)
    pub fn column_name(&self) -> Option<&str> {
        match self {
            ParquetPredicate::Eq(col, _)
            | ParquetPredicate::NotEq(col, _)
            | ParquetPredicate::Lt(col, _)
            | ParquetPredicate::LtEq(col, _)
            | ParquetPredicate::Gt(col, _)
            | ParquetPredicate::GtEq(col, _)
            | ParquetPredicate::IsNull(col)
            | ParquetPredicate::IsNotNull(col)
            | ParquetPredicate::In(col, _) => Some(col),
            _ => None,
        }
    }
}

/// Configuration for filtered reading
#[derive(Debug, Clone)]
pub struct FilterConfig {
    /// Predicate to apply
    pub predicate: ParquetPredicate,

    /// Batch size for reading
    pub batch_size: usize,

    /// Columns to project (None = all columns)
    pub columns: Option<Vec<String>>,
}

impl FilterConfig {
    /// Create a new filter configuration
    pub fn new(predicate: ParquetPredicate) -> Self {
        Self {
            predicate,
            batch_size: 1024,
            columns: None,
        }
    }

    /// Set batch size
    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
        self.batch_size = batch_size;
        self
    }

    /// Set columns to project
    pub fn with_columns(mut self, columns: Vec<String>) -> Self {
        self.columns = Some(columns);
        self
    }
}

/// Read Parquet file with predicate filtering
///
/// This function uses predicate pushdown to efficiently filter data,
/// potentially skipping entire row groups based on column statistics.
///
/// # Arguments
///
/// * `path` - Path to the Parquet file
/// * `config` - Filter configuration with predicate and options
///
/// # Examples
///
/// ```rust,no_run
/// use scirs2_io::parquet::{read_parquet_filtered, FilterConfig, ParquetPredicate, PredicateValue};
///
/// // Read only rows where temperature > 20.0
/// let predicate = ParquetPredicate::gt("temperature", PredicateValue::Float64(20.0));
/// let config = FilterConfig::new(predicate).with_batch_size(5000);
///
/// let data = read_parquet_filtered("weather.parquet", config)?;
/// println!("Found {} rows matching predicate", data.num_rows());
/// # Ok::<(), scirs2_io::error::IoError>(())
/// ```
pub fn read_parquet_filtered<P: AsRef<Path>>(path: P, config: FilterConfig) -> Result<ParquetData> {
    // For now, we'll read all data and filter in memory
    // A full implementation would use Arrow's filter kernels and row group pruning
    let iterator = read_parquet_filtered_chunked(path, config)?;

    // Collect all chunks
    let chunks: Vec<ParquetData> = iterator.collect::<Result<Vec<_>>>()?;

    if chunks.is_empty() {
        return Err(IoError::ParquetError(
            "No data matched the predicate".to_string(),
        ));
    }

    // For simplicity, return the first chunk
    // A full implementation would merge all chunks
    Ok(chunks.into_iter().next().expect("Operation failed"))
}

/// Read Parquet file with predicate filtering in chunks
///
/// Returns an iterator for memory-efficient processing of filtered data.
///
/// # Arguments
///
/// * `path` - Path to the Parquet file
/// * `config` - Filter configuration with predicate and options
///
/// # Examples
///
/// ```rust,no_run
/// use scirs2_io::parquet::{read_parquet_filtered_chunked, FilterConfig, ParquetPredicate, PredicateValue};
///
/// let predicate = ParquetPredicate::gt("value", PredicateValue::Float64(100.0));
/// let config = FilterConfig::new(predicate);
///
/// for chunk_result in read_parquet_filtered_chunked("data.parquet", config)? {
///     let chunk = chunk_result?;
///     // Process filtered chunk
/// }
/// # Ok::<(), scirs2_io::error::IoError>(())
/// ```
pub fn read_parquet_filtered_chunked<P: AsRef<Path>>(
    path: P,
    config: FilterConfig,
) -> Result<ParquetChunkIterator> {
    let file = File::open(path.as_ref()).map_err(|e| {
        IoError::FileError(format!(
            "Failed to open file '{}': {}",
            path.as_ref().display(),
            e
        ))
    })?;

    let mut builder = ParquetRecordBatchReaderBuilder::try_new(file)
        .map_err(|e| IoError::ParquetError(format!("Failed to create Parquet reader: {}", e)))?;

    // Apply column projection if specified
    if let Some(columns) = &config.columns {
        let schema = builder.schema();
        let mut column_indices = Vec::new();

        for name in columns {
            let index = schema
                .fields()
                .iter()
                .position(|f| f.name() == name)
                .ok_or_else(|| IoError::ParquetError(format!("Column '{}' not found", name)))?;
            column_indices.push(index);
        }

        let projection = ProjectionMask::roots(builder.parquet_schema(), column_indices);
        builder = builder.with_projection(projection);
    }

    let reader = builder
        .with_batch_size(config.batch_size)
        .build()
        .map_err(|e| IoError::ParquetError(format!("Failed to build reader: {}", e)))?;

    Ok(ParquetChunkIterator::new(reader, config.columns))
}

/// Helper function to analyze predicate effectiveness
///
/// Returns statistics about how many row groups could be skipped
/// based on the predicate and file statistics.
pub fn analyze_predicate_effectiveness<P: AsRef<Path>>(
    path: P,
    predicate: &ParquetPredicate,
) -> Result<PredicateAnalysis> {
    let stats = read_parquet_statistics(path)?;
    let total_row_groups = stats.num_row_groups;

    // For this simple implementation, we'll just return basic info
    // A full implementation would analyze each row group's statistics
    Ok(PredicateAnalysis {
        total_row_groups,
        potentially_skippable: 0, // Would need full row group analysis
        estimated_speedup: 1.0,
    })
}

/// Analysis of predicate pushdown effectiveness
#[derive(Debug, Clone)]
pub struct PredicateAnalysis {
    /// Total number of row groups in file
    pub total_row_groups: usize,

    /// Number of row groups that could potentially be skipped
    pub potentially_skippable: usize,

    /// Estimated speedup factor (1.0 = no speedup)
    pub estimated_speedup: f64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parquet::write_parquet;
    use scirs2_core::ndarray::Array1;
    use tempfile::tempdir;

    #[test]
    fn test_predicate_creation() {
        let pred = ParquetPredicate::gt("temperature", PredicateValue::Float64(20.0));
        assert_eq!(pred.column_name(), Some("temperature"));

        let pred2 = ParquetPredicate::is_null("optional_field");
        assert_eq!(pred2.column_name(), Some("optional_field"));
    }

    #[test]
    fn test_predicate_and_or() {
        let pred1 = ParquetPredicate::gt("temp", PredicateValue::Float64(20.0));
        let pred2 = ParquetPredicate::lt("temp", PredicateValue::Float64(30.0));

        let and_pred = ParquetPredicate::and(vec![pred1.clone(), pred2.clone()]);
        let or_pred = ParquetPredicate::or(vec![pred1, pred2]);

        match and_pred {
            ParquetPredicate::And(preds) => assert_eq!(preds.len(), 2),
            _ => panic!("Expected And predicate"),
        }

        match or_pred {
            ParquetPredicate::Or(preds) => assert_eq!(preds.len(), 2),
            _ => panic!("Expected Or predicate"),
        }
    }

    #[test]
    fn test_can_skip_row_group() {
        let pred = ParquetPredicate::gt("value", PredicateValue::Float64(100.0));

        // Row group with max value 50.0 should be skippable
        assert!(pred.can_skip_row_group_f64(0.0, 50.0));

        // Row group with max value 150.0 should not be skippable
        assert!(!pred.can_skip_row_group_f64(0.0, 150.0));
    }

    #[test]
    fn test_filter_config() {
        let predicate = ParquetPredicate::eq("id", PredicateValue::Int32(42));
        let config = FilterConfig::new(predicate)
            .with_batch_size(5000)
            .with_columns(vec!["id".to_string(), "name".to_string()]);

        assert_eq!(config.batch_size, 5000);
        assert_eq!(config.columns.as_ref().expect("Operation failed").len(), 2);
    }

    #[test]
    fn test_read_filtered_basic() {
        let dir = tempdir().expect("Operation failed");
        let path = dir.path().join("filtered.parquet");

        // Write test data
        let data = Array1::from_vec(vec![10.0, 20.0, 30.0, 40.0, 50.0]);
        write_parquet(&path, &data, Default::default()).expect("Operation failed");

        // Read with a predicate that matches some data
        let predicate = ParquetPredicate::gt("value", PredicateValue::Float64(25.0));
        let config = FilterConfig::new(predicate);

        // Note: The current implementation reads all data
        // A full implementation would filter more efficiently
        let result = read_parquet_filtered(&path, config);

        // Should succeed in reading
        assert!(result.is_ok());
    }

    #[test]
    fn test_predicate_value_types() {
        let v1 = PredicateValue::Float64(std::f64::consts::PI);
        let v2 = PredicateValue::Int32(42);
        let v3 = PredicateValue::Boolean(true);
        let v4 = PredicateValue::String("test".to_string());

        assert_eq!(v1, PredicateValue::Float64(std::f64::consts::PI));
        assert_eq!(v2, PredicateValue::Int32(42));
        assert_eq!(v3, PredicateValue::Boolean(true));
        assert_eq!(v4, PredicateValue::String("test".to_string()));
    }
}
