//! Format conversion utilities for open deviation bar data
//!
//! Provides bidirectional conversion between OpenDeviationBar and Polars DataFrame
//! with exception-only failure handling.

use opendeviationbar_core::{AggTrade, FixedPoint, OpenDeviationBar};
use polars::prelude::*;
use thiserror::Error;

/// Trait for converting between Rust types and Polars DataFrames
pub trait DataFrameConverter<T> {
    /// Convert to Polars DataFrame
    fn to_polars_dataframe(&self) -> PolarsResult<DataFrame>;

    /// Convert from Polars DataFrame
    fn from_polars_dataframe(df: DataFrame) -> Result<T, ConversionError>;
}

/// Conversion errors with rich context
#[derive(Debug, Error)]
pub enum ConversionError {
    #[error("Missing required column: {column}")]
    MissingColumn { column: String },

    #[error("Invalid data type for column '{column}': expected {expected}, got {actual}")]
    InvalidDataType {
        column: String,
        expected: String,
        actual: String,
    },

    #[error("Data validation failed: {message}")]
    ValidationFailed { message: String },

    #[error("Polars error: {0}")]
    PolarsError(#[from] PolarsError),

    #[error("FixedPoint conversion error: {0}")]
    FixedPointError(String),
}

/// Required columns for OpenDeviationBar DataFrame
pub const OPENDEVIATIONBAR_COLUMNS: &[&str] = &[
    "open_time",
    "close_time",
    "open",
    "high",
    "low",
    "close",
    "volume",
    "turnover",
    "individual_trade_count",
    "agg_record_count",
    "first_trade_id",
    "last_trade_id",
    "data_source",
    "buy_volume",
    "sell_volume",
    "buy_trade_count",
    "sell_trade_count",
    "vwap",
    "buy_turnover",
    "sell_turnover",
];

/// Required columns for AggTrade DataFrame
pub const AGGTRADE_COLUMNS: &[&str] = &[
    "agg_trade_id",
    "price",
    "volume",
    "first_trade_id",
    "last_trade_id",
    "timestamp",
    "is_buyer_maker",
];

impl DataFrameConverter<Vec<OpenDeviationBar>> for Vec<OpenDeviationBar> {
    fn to_polars_dataframe(&self) -> PolarsResult<DataFrame> {
        if self.is_empty() {
            return Err(PolarsError::NoData("Empty OpenDeviationBar vector".into()));
        }

        // Memory v2: Single-pass SoA conversion for cache efficiency.
        // Previous code iterated bars 20+ times (one per field), thrashing L1 cache.
        let n = self.len();
        let mut open_times = Vec::with_capacity(n);
        let mut close_times = Vec::with_capacity(n);
        let mut opens = Vec::with_capacity(n);
        let mut highs = Vec::with_capacity(n);
        let mut lows = Vec::with_capacity(n);
        let mut closes = Vec::with_capacity(n);
        let mut volumes = Vec::with_capacity(n);
        let mut turnovers = Vec::with_capacity(n);
        let mut trade_counts = Vec::with_capacity(n);
        let mut agg_record_counts = Vec::with_capacity(n);
        let mut first_ids = Vec::with_capacity(n);
        let mut last_ids = Vec::with_capacity(n);
        let mut buy_volumes = Vec::with_capacity(n);
        let mut sell_volumes = Vec::with_capacity(n);
        let mut buy_trade_counts = Vec::with_capacity(n);
        let mut sell_trade_counts = Vec::with_capacity(n);
        let mut vwaps = Vec::with_capacity(n);
        let mut buy_turnovers = Vec::with_capacity(n);
        let mut sell_turnovers = Vec::with_capacity(n);

        for bar in self {
            open_times.push(bar.open_time);
            close_times.push(bar.close_time);
            opens.push(bar.open.0);
            highs.push(bar.high.0);
            lows.push(bar.low.0);
            closes.push(bar.close.0);
            // Issue #88: i128 volume → i64 for CSV (lossy but CSV is legacy format)
            volumes.push(bar.volume as i64);
            turnovers.push(bar.turnover as i64);
            trade_counts.push(bar.individual_trade_count as i64);
            agg_record_counts.push(bar.agg_record_count as i64);
            first_ids.push(bar.first_trade_id);
            last_ids.push(bar.last_trade_id);
            buy_volumes.push(bar.buy_volume as i64);
            sell_volumes.push(bar.sell_volume as i64);
            buy_trade_counts.push(bar.buy_trade_count as i64);
            sell_trade_counts.push(bar.sell_trade_count as i64);
            vwaps.push(bar.vwap.0);
            buy_turnovers.push(bar.buy_turnover as i64);
            sell_turnovers.push(bar.sell_turnover as i64);
        }

        // Create DataFrame with all columns (full names matching OPENDEVIATIONBAR_COLUMNS)
        DataFrame::new(vec![
            Column::new("open_time".into(), &open_times),
            Column::new("close_time".into(), &close_times),
            Column::new("open".into(), &opens),
            Column::new("high".into(), &highs),
            Column::new("low".into(), &lows),
            Column::new("close".into(), &closes),
            Column::new("volume".into(), &volumes),
            Column::new("turnover".into(), &turnovers),
            Column::new("individual_trade_count".into(), &trade_counts),
            Column::new("agg_record_count".into(), &agg_record_counts),
            Column::new("first_trade_id".into(), &first_ids),
            Column::new("last_trade_id".into(), &last_ids),
            Column::new("data_source".into(), &vec!["BinanceFuturesUM"; n]),
            Column::new("buy_volume".into(), &buy_volumes),
            Column::new("sell_volume".into(), &sell_volumes),
            Column::new("buy_trade_count".into(), &buy_trade_counts),
            Column::new("sell_trade_count".into(), &sell_trade_counts),
            Column::new("vwap".into(), &vwaps),
            Column::new("buy_turnover".into(), &buy_turnovers),
            Column::new("sell_turnover".into(), &sell_turnovers),
        ])
    }

    fn from_polars_dataframe(df: DataFrame) -> Result<Vec<OpenDeviationBar>, ConversionError> {
        // Validate required columns are present
        validate_opendeviationbar_columns(&df)?;

        let height = df.height();
        if height == 0 {
            return Ok(Vec::new());
        }

        let mut open_deviation_bars = Vec::with_capacity(height);

        // Extract all columns efficiently (using full column names)
        let open_times = extract_i64_column(&df, "open_time")?;
        let close_times = extract_i64_column(&df, "close_time")?;
        let opens = extract_i64_column(&df, "open")?;
        let highs = extract_i64_column(&df, "high")?;
        let lows = extract_i64_column(&df, "low")?;
        let closes = extract_i64_column(&df, "close")?;
        let volumes = extract_i64_column(&df, "volume")?;
        let turnovers = extract_i64_column(&df, "turnover")?;
        let trade_counts = extract_i64_column(&df, "individual_trade_count")?;
        let agg_record_counts = extract_i64_column(&df, "agg_record_count")?;
        let first_ids = extract_i64_column(&df, "first_trade_id")?;
        let last_ids = extract_i64_column(&df, "last_trade_id")?;
        let buy_volumes = extract_i64_column(&df, "buy_volume")?;
        let sell_volumes = extract_i64_column(&df, "sell_volume")?;
        let buy_trade_counts = extract_i64_column(&df, "buy_trade_count")?;
        let sell_trade_counts = extract_i64_column(&df, "sell_trade_count")?;
        let vwaps = extract_i64_column(&df, "vwap")?;
        let buy_turnovers = extract_i64_column(&df, "buy_turnover")?;
        let sell_turnovers = extract_i64_column(&df, "sell_turnover")?;

        // Construct OpenDeviationBar structs
        for i in 0..height {
            let open_deviation_bar = OpenDeviationBar {
                open_time: open_times[i],
                close_time: close_times[i],
                open: FixedPoint(opens[i]),
                high: FixedPoint(highs[i]),
                low: FixedPoint(lows[i]),
                close: FixedPoint(closes[i]),
                volume: volumes[i] as i128, // Issue #88: i128 volume
                turnover: turnovers[i] as i128,
                individual_trade_count: trade_counts[i] as u32,
                agg_record_count: agg_record_counts[i] as u32,
                first_trade_id: first_ids[i],
                last_trade_id: last_ids[i],
                // Issue #72: Trade ID tracking (defaults for import)
                first_agg_trade_id: 0,
                last_agg_trade_id: 0,
                data_source: opendeviationbar_core::DataSource::default(),
                buy_volume: buy_volumes[i] as i128, // Issue #88: i128 volume
                sell_volume: sell_volumes[i] as i128, // Issue #88: i128 volume
                buy_trade_count: buy_trade_counts[i] as u32,
                sell_trade_count: sell_trade_counts[i] as u32,
                vwap: FixedPoint(vwaps[i]),
                buy_turnover: buy_turnovers[i] as i128,
                sell_turnover: sell_turnovers[i] as i128,
                // Microstructure features (Issue #25) - defaults for import
                duration_us: 0,
                ofi: 0.0,
                vwap_close_deviation: 0.0,
                price_impact: 0.0,
                kyle_lambda_proxy: 0.0,
                trade_intensity: 0.0,
                volume_per_trade: 0.0,
                aggression_ratio: 0.0,
                aggregation_density_f64: 0.0,
                turnover_imbalance: 0.0,
                // Inter-bar features (Issue #59) - defaults for import
                lookback_trade_count: None,
                lookback_ofi: None,
                lookback_duration_us: None,
                lookback_intensity: None,
                lookback_vwap_raw: None,
                lookback_vwap_position: None,
                lookback_count_imbalance: None,
                lookback_kyle_lambda: None,
                lookback_burstiness: None,
                lookback_volume_skew: None,
                lookback_volume_kurt: None,
                lookback_price_range: None,
                lookback_kaufman_er: None,
                lookback_garman_klass_vol: None,
                lookback_hurst: None,
                lookback_permutation_entropy: None,
                // Intra-bar features (Issue #59) - defaults for import
                intra_bull_epoch_density: None,
                intra_bear_epoch_density: None,
                intra_bull_excess_gain: None,
                intra_bear_excess_gain: None,
                intra_bull_cv: None,
                intra_bear_cv: None,
                intra_max_drawdown: None,
                intra_max_runup: None,
                intra_trade_count: None,
                intra_ofi: None,
                intra_duration_us: None,
                intra_intensity: None,
                intra_vwap_position: None,
                intra_count_imbalance: None,
                intra_kyle_lambda: None,
                intra_burstiness: None,
                intra_volume_skew: None,
                intra_volume_kurt: None,
                intra_kaufman_er: None,
                intra_garman_klass_vol: None,
                intra_hurst: None,
                intra_permutation_entropy: None,
            };

            // Validate open deviation bar data integrity
            validate_open_deviation_bar(&open_deviation_bar)?;
            open_deviation_bars.push(open_deviation_bar);
        }

        Ok(open_deviation_bars)
    }
}

impl DataFrameConverter<Vec<AggTrade>> for Vec<AggTrade> {
    fn to_polars_dataframe(&self) -> PolarsResult<DataFrame> {
        if self.is_empty() {
            return Err(PolarsError::NoData("Empty AggTrade vector".into()));
        }

        let agg_trade_ids: Vec<i64> = self.iter().map(|trade| trade.agg_trade_id).collect();
        let prices: Vec<i64> = self.iter().map(|trade| trade.price.0).collect();
        let volumes: Vec<i64> = self.iter().map(|trade| trade.volume.0).collect();
        let first_trade_ids: Vec<i64> = self.iter().map(|trade| trade.first_trade_id).collect();
        let last_trade_ids: Vec<i64> = self.iter().map(|trade| trade.last_trade_id).collect();
        let timestamps: Vec<i64> = self.iter().map(|trade| trade.timestamp).collect();
        let is_buyer_makers: Vec<bool> = self.iter().map(|trade| trade.is_buyer_maker).collect();

        DataFrame::new(vec![
            Column::new("agg_trade_id".into(), &agg_trade_ids),
            Column::new("price".into(), &prices),
            Column::new("volume".into(), &volumes),
            Column::new("first_trade_id".into(), &first_trade_ids),
            Column::new("last_trade_id".into(), &last_trade_ids),
            Column::new("timestamp".into(), &timestamps),
            Column::new("is_buyer_maker".into(), &is_buyer_makers),
        ])
    }

    fn from_polars_dataframe(df: DataFrame) -> Result<Vec<AggTrade>, ConversionError> {
        validate_aggtrade_columns(&df)?;

        let height = df.height();
        if height == 0 {
            return Ok(Vec::new());
        }

        let mut agg_trades = Vec::with_capacity(height);

        let agg_trade_ids = extract_i64_column(&df, "agg_trade_id")?;
        let prices = extract_i64_column(&df, "price")?;
        let volumes = extract_i64_column(&df, "volume")?;
        let first_trade_ids = extract_i64_column(&df, "first_trade_id")?;
        let last_trade_ids = extract_i64_column(&df, "last_trade_id")?;
        let timestamps = extract_i64_column(&df, "timestamp")?;
        let is_buyer_makers = extract_bool_column(&df, "is_buyer_maker")?;

        for i in 0..height {
            let agg_trade = AggTrade {
                agg_trade_id: agg_trade_ids[i],
                price: FixedPoint(prices[i]),
                volume: FixedPoint(volumes[i]),
                first_trade_id: first_trade_ids[i],
                last_trade_id: last_trade_ids[i],
                timestamp: timestamps[i],
                is_buyer_maker: is_buyer_makers[i],
                is_best_match: None,
            };

            validate_agg_trade(&agg_trade)?;
            agg_trades.push(agg_trade);
        }

        Ok(agg_trades)
    }
}

/// Validate OpenDeviationBar DataFrame has required columns
fn validate_opendeviationbar_columns(df: &DataFrame) -> Result<(), ConversionError> {
    for &column in OPENDEVIATIONBAR_COLUMNS {
        if !df
            .get_column_names()
            .iter()
            .any(|name| name.as_str() == column)
        {
            return Err(ConversionError::MissingColumn {
                column: column.to_string(),
            });
        }
    }
    Ok(())
}

/// Validate AggTrade DataFrame has required columns
fn validate_aggtrade_columns(df: &DataFrame) -> Result<(), ConversionError> {
    for &column in AGGTRADE_COLUMNS {
        if !df
            .get_column_names()
            .iter()
            .any(|name| name.as_str() == column)
        {
            return Err(ConversionError::MissingColumn {
                column: column.to_string(),
            });
        }
    }
    Ok(())
}

/// Extract i64 column with error handling
fn extract_i64_column(df: &DataFrame, column_name: &str) -> Result<Vec<i64>, ConversionError> {
    let series = df
        .column(column_name)
        .map_err(|_| ConversionError::MissingColumn {
            column: column_name.to_string(),
        })?;

    Ok(series
        .i64()
        .map_err(|_| ConversionError::InvalidDataType {
            column: column_name.to_string(),
            expected: "i64".to_string(),
            actual: format!("{:?}", series.dtype()),
        })?
        .into_no_null_iter()
        .collect::<Vec<i64>>())
}

/// Extract boolean column with error handling
fn extract_bool_column(df: &DataFrame, column_name: &str) -> Result<Vec<bool>, ConversionError> {
    let series = df
        .column(column_name)
        .map_err(|_| ConversionError::MissingColumn {
            column: column_name.to_string(),
        })?;

    Ok(series
        .bool()
        .map_err(|_| ConversionError::InvalidDataType {
            column: column_name.to_string(),
            expected: "bool".to_string(),
            actual: format!("{:?}", series.dtype()),
        })?
        .into_no_null_iter()
        .collect::<Vec<bool>>())
}

/// Validate individual OpenDeviationBar data integrity
fn validate_open_deviation_bar(bar: &OpenDeviationBar) -> Result<(), ConversionError> {
    // Temporal validation
    if bar.close_time <= bar.open_time {
        return Err(ConversionError::ValidationFailed {
            message: format!(
                "Invalid time sequence: close_time ({}) <= open_time ({})",
                bar.close_time, bar.open_time
            ),
        });
    }

    // OHLC validation
    if bar.high < bar.low {
        return Err(ConversionError::ValidationFailed {
            message: format!("Invalid OHLC: high ({}) < low ({})", bar.high, bar.low),
        });
    }

    if bar.open > bar.high || bar.open < bar.low {
        return Err(ConversionError::ValidationFailed {
            message: "Open price outside high-low range".to_string(),
        });
    }

    if bar.close > bar.high || bar.close < bar.low {
        return Err(ConversionError::ValidationFailed {
            message: "Close price outside high-low range".to_string(),
        });
    }

    // Volume validation
    if bar.volume <= 0 {
        // Issue #88: i128 volume
        return Err(ConversionError::ValidationFailed {
            message: "Volume must be positive".to_string(),
        });
    }

    // Trade count validation
    if bar.individual_trade_count == 0 {
        return Err(ConversionError::ValidationFailed {
            message: "Trade count must be positive".to_string(),
        });
    }

    // ID validation
    if bar.last_trade_id < bar.first_trade_id {
        return Err(ConversionError::ValidationFailed {
            message: "last_id must be >= first_id".to_string(),
        });
    }

    Ok(())
}

/// Validate individual AggTrade data integrity
fn validate_agg_trade(trade: &AggTrade) -> Result<(), ConversionError> {
    // Price validation
    if trade.price.0 <= 0 {
        return Err(ConversionError::ValidationFailed {
            message: "Price must be positive".to_string(),
        });
    }

    // Volume validation
    if trade.volume.0 <= 0 {
        return Err(ConversionError::ValidationFailed {
            message: "Volume must be positive".to_string(),
        });
    }

    // Timestamp validation (basic sanity check)
    if trade.timestamp <= 0 {
        return Err(ConversionError::ValidationFailed {
            message: "Timestamp must be positive".to_string(),
        });
    }

    // Trade ID validation
    if trade.last_trade_id < trade.first_trade_id {
        return Err(ConversionError::ValidationFailed {
            message: "last_trade_id must be >= first_trade_id".to_string(),
        });
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use opendeviationbar_core::{DataSource, FixedPoint};

    fn create_test_open_deviation_bar() -> OpenDeviationBar {
        OpenDeviationBar {
            open_time: 1000000,
            close_time: 1000001,
            open: FixedPoint(100000000),  // 1.0
            high: FixedPoint(110000000),  // 1.1
            low: FixedPoint(90000000),    // 0.9
            close: FixedPoint(105000000), // 1.05
            volume: 1000000000_i128,      // 10.0 — Issue #88
            turnover: 1050000000,
            individual_trade_count: 5,
            agg_record_count: 1,
            first_trade_id: 1,
            last_trade_id: 5,
            data_source: DataSource::BinanceFuturesUM,
            first_agg_trade_id: 0,       // Issue #72
            last_agg_trade_id: 0,        // Issue #72
            buy_volume: 600000000_i128,  // Issue #88
            sell_volume: 400000000_i128, // Issue #88
            buy_trade_count: 3,
            sell_trade_count: 2,
            vwap: FixedPoint(105000000),
            buy_turnover: 630000000,
            sell_turnover: 420000000,
            // Microstructure features (Issue #25)
            duration_us: 0,
            ofi: 0.0,
            vwap_close_deviation: 0.0,
            price_impact: 0.0,
            kyle_lambda_proxy: 0.0,
            trade_intensity: 0.0,
            volume_per_trade: 0.0,
            aggression_ratio: 0.0,
            aggregation_density_f64: 0.0,
            turnover_imbalance: 0.0,
            // Inter-bar features (Issue #59)
            lookback_trade_count: None,
            lookback_ofi: None,
            lookback_duration_us: None,
            lookback_intensity: None,
            lookback_vwap_raw: None,
            lookback_vwap_position: None,
            lookback_count_imbalance: None,
            lookback_kyle_lambda: None,
            lookback_burstiness: None,
            lookback_volume_skew: None,
            lookback_volume_kurt: None,
            lookback_price_range: None,
            lookback_kaufman_er: None,
            lookback_garman_klass_vol: None,
            lookback_hurst: None,
            lookback_permutation_entropy: None,
            // Intra-bar features (Issue #59)
            intra_bull_epoch_density: None,
            intra_bear_epoch_density: None,
            intra_bull_excess_gain: None,
            intra_bear_excess_gain: None,
            intra_bull_cv: None,
            intra_bear_cv: None,
            intra_max_drawdown: None,
            intra_max_runup: None,
            intra_trade_count: None,
            intra_ofi: None,
            intra_duration_us: None,
            intra_intensity: None,
            intra_vwap_position: None,
            intra_count_imbalance: None,
            intra_kyle_lambda: None,
            intra_burstiness: None,
            intra_volume_skew: None,
            intra_volume_kurt: None,
            intra_kaufman_er: None,
            intra_garman_klass_vol: None,
            intra_hurst: None,
            intra_permutation_entropy: None,
        }
    }

    #[test]
    fn test_opendeviationbar_to_dataframe_conversion() {
        let bars = vec![create_test_open_deviation_bar()];
        let df = bars.to_polars_dataframe().unwrap();

        assert_eq!(df.height(), 1);
        assert_eq!(df.width(), OPENDEVIATIONBAR_COLUMNS.len());

        // Verify specific values
        let open_series = df.column("open").unwrap();
        assert_eq!(open_series.i64().unwrap().get(0), Some(100000000));
    }

    #[test]
    fn test_dataframe_to_opendeviationbar_conversion() {
        let original_bars = vec![create_test_open_deviation_bar()];
        let df = original_bars.to_polars_dataframe().unwrap();
        let converted_bars = Vec::<OpenDeviationBar>::from_polars_dataframe(df).unwrap();

        assert_eq!(converted_bars.len(), 1);
        let bar = &converted_bars[0];
        let original = &original_bars[0];

        assert_eq!(bar.open_time, original.open_time);
        assert_eq!(bar.close_time, original.close_time);
        assert_eq!(bar.open, original.open);
        assert_eq!(bar.high, original.high);
        assert_eq!(bar.low, original.low);
        assert_eq!(bar.close, original.close);
    }

    #[test]
    fn test_empty_vector_conversion() {
        let empty_bars: Vec<OpenDeviationBar> = vec![];
        let result = empty_bars.to_polars_dataframe();
        assert!(result.is_err());
    }

    #[test]
    fn test_missing_column_validation() {
        let df = DataFrame::new(vec![
            Column::new("open_time".into(), vec![1000000i64]),
            // Missing other required columns
        ])
        .unwrap();

        let result = Vec::<OpenDeviationBar>::from_polars_dataframe(df);
        assert!(matches!(result, Err(ConversionError::MissingColumn { .. })));
    }

    #[test]
    fn test_invalid_open_deviation_bar_validation() {
        let mut invalid_bar = create_test_open_deviation_bar();
        invalid_bar.high = FixedPoint(50000000); // Invalid: high < low

        let result = validate_open_deviation_bar(&invalid_bar);
        assert!(matches!(
            result,
            Err(ConversionError::ValidationFailed { .. })
        ));
    }
}
