// ml/src/backtesting/action_loader.rs
// DQN action loader for CSV-based backtesting

use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;

/// DQN action record from CSV (13,552 actions, 10 columns)
/// Format: timestamp,action,q_buy,q_sell,q_hold,open,high,low,close,volume
#[derive(Debug, Clone, Deserialize)]
pub struct DQNActionRecord {
    /// ISO8601 timestamp (e.g., "2024-10-20T23:31:00.000000000Z")
    pub timestamp: DateTime<Utc>,

    /// Action: 0=Buy, 1=Sell, 2=Hold
    pub action: u8,

    /// Q-value for buy action
    pub q_buy: f64,

    /// Q-value for sell action
    pub q_sell: f64,

    /// Q-value for hold action
    pub q_hold: f64,

    /// OHLCV data (for validation/debugging)
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: u64,
}

/// Load DQN actions from CSV file
///
/// # Arguments
/// * `csv_path` - Path to CSV file (e.g., "/tmp/dqn_actions_wave3.csv")
///
/// # Returns
/// * `Ok(Vec<DQNActionRecord>)` - Parsed and validated actions
/// * `Err(String)` - Validation errors (action bounds, Q-values, timestamps)
///
/// # Validations
/// 1. Action bounds: 0 <= action <= 2
/// 2. Finite Q-values: q_buy, q_sell, q_hold must be finite (no NaN/Inf)
/// 3. Timestamp ordering: timestamps must be monotonically increasing
///
/// # Example
/// ```ignore
/// let actions = load_actions_from_csv("/tmp/dqn_actions_wave3.csv")?;
/// assert_eq!(actions.len(), 13_552);
/// ```
pub fn load_actions_from_csv<P: AsRef<Path>>(csv_path: P) -> Result<Vec<DQNActionRecord>, String> {
    let path = csv_path.as_ref();

    // Open CSV file
    let file = File::open(path)
        .map_err(|e| format!("Failed to open CSV file '{}': {}", path.display(), e))?;

    let reader = BufReader::new(file);
    let mut csv_reader = csv::Reader::from_reader(reader);

    let mut actions = Vec::new();
    let mut prev_timestamp: Option<DateTime<Utc>> = None;
    let mut row_num = 1; // Start at 1 (header is row 0)

    for result in csv_reader.deserialize() {
        row_num += 1;

        let record: DQNActionRecord =
            result.map_err(|e| format!("CSV parsing error at row {}: {}", row_num, e))?;

        // Validation 1: Action bounds (0-2)
        if record.action > 2 {
            return Err(format!(
                "Invalid action {} at row {} (timestamp {}): action must be 0 (Buy), 1 (Sell), or 2 (Hold)",
                record.action,
                row_num,
                record.timestamp
            ));
        }

        // Validation 2: Finite Q-values
        if !record.q_buy.is_finite() {
            return Err(format!(
                "Invalid q_buy {} at row {} (timestamp {}): Q-value must be finite",
                record.q_buy, row_num, record.timestamp
            ));
        }
        if !record.q_sell.is_finite() {
            return Err(format!(
                "Invalid q_sell {} at row {} (timestamp {}): Q-value must be finite",
                record.q_sell, row_num, record.timestamp
            ));
        }
        if !record.q_hold.is_finite() {
            return Err(format!(
                "Invalid q_hold {} at row {} (timestamp {}): Q-value must be finite",
                record.q_hold, row_num, record.timestamp
            ));
        }

        // Validation 3: Timestamp ordering
        if let Some(prev_ts) = prev_timestamp {
            if record.timestamp < prev_ts {
                return Err(format!(
                    "Timestamp ordering violation at row {}: {} is before previous timestamp {}",
                    row_num, record.timestamp, prev_ts
                ));
            }
        }
        prev_timestamp = Some(record.timestamp);

        actions.push(record);
    }

    if actions.is_empty() {
        return Err(format!(
            "CSV file '{}' contains no data rows",
            path.display()
        ));
    }

    Ok(actions)
}

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

    #[test]
    fn test_valid_action_record() {
        // Test DQNActionRecord struct initialization
        let record = DQNActionRecord {
            timestamp: Utc.with_ymd_and_hms(2024, 10, 20, 23, 31, 0).unwrap(),
            action: 2, // Hold
            q_buy: -658.8440,
            q_sell: 355.0268,
            q_hold: 538.5875,
            open: 5914.50,
            high: 5914.75,
            low: 5914.25,
            close: 5914.25,
            volume: 27,
        };

        assert_eq!(record.action, 2);
        assert!(record.q_buy.is_finite());
        assert!(record.q_sell.is_finite());
        assert!(record.q_hold.is_finite());
    }

    #[test]
    fn test_action_bounds_validation() {
        // Test action bounds: 0 <= action <= 2
        // Create a temporary CSV with invalid action
        use std::io::Write;
        let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
        writeln!(
            tmpfile,
            "timestamp,action,q_buy,q_sell,q_hold,open,high,low,close,volume"
        )
        .unwrap();
        writeln!(tmpfile, "2024-10-20T23:31:00.000000000Z,3,-658.8440,355.0268,538.5875,5914.50,5914.75,5914.25,5914.25,27").unwrap();
        tmpfile.flush().unwrap();

        let result = load_actions_from_csv(tmpfile.path());
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("Invalid action 3"));
        assert!(err.contains("action must be 0 (Buy), 1 (Sell), or 2 (Hold)"));
    }

    #[test]
    fn test_finite_qvalue_validation() {
        // Test finite Q-value validation (NaN/Inf detection)
        use std::io::Write;
        let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
        writeln!(
            tmpfile,
            "timestamp,action,q_buy,q_sell,q_hold,open,high,low,close,volume"
        )
        .unwrap();
        writeln!(tmpfile, "2024-10-20T23:31:00.000000000Z,2,NaN,355.0268,538.5875,5914.50,5914.75,5914.25,5914.25,27").unwrap();
        tmpfile.flush().unwrap();

        let result = load_actions_from_csv(tmpfile.path());
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("Invalid q_buy"));
        assert!(err.contains("Q-value must be finite"));
    }
}
