//! Time series data types and validatable strategy trait for statistical validation.
//!
//! Provides [`TimeSeriesData`] for representing financial time series with prices,
//! features, and computed log returns, plus [`ValidatableStrategy`] as the interface
//! for strategies that can be evaluated through walk-forward validation, DSR, and PBO.

use chrono::{DateTime, Utc};
use crate::MLError;

/// Financial time series data for validation pipelines.
///
/// Holds aligned timestamps, feature matrices, close prices, and log returns
/// (computed automatically from prices). All vectors must have consistent
/// lengths: `timestamps.len() == features.len() == prices.len()`, and
/// `returns.len() == prices.len() - 1`.
#[derive(Debug, Clone)]
pub struct TimeSeriesData {
    /// Bar timestamps (one per bar).
    pub timestamps: Vec<DateTime<Utc>>,
    /// Feature matrix `[num_bars][feature_dim]`.
    pub features: Vec<Vec<f32>>,
    /// Close prices per bar (must have >= 2 elements).
    pub prices: Vec<f64>,
    /// Log returns `ln(p[i+1] / p[i])`, length = `prices.len() - 1`.
    pub returns: Vec<f64>,
}

impl TimeSeriesData {
    /// Create a new `TimeSeriesData`, validating lengths and computing log returns.
    ///
    /// # Errors
    ///
    /// Returns [`MLError::InvalidInput`] if:
    /// - `prices` has fewer than 2 elements
    /// - `timestamps`, `features`, and `prices` have different lengths
    /// - Any price is non-positive (cannot compute `ln`)
    pub fn new(
        timestamps: Vec<DateTime<Utc>>,
        features: Vec<Vec<f32>>,
        prices: Vec<f64>,
    ) -> Result<Self, MLError> {
        if prices.len() < 2 {
            return Err(MLError::InvalidInput(format!(
                "TimeSeriesData requires at least 2 prices, got {}",
                prices.len()
            )));
        }

        if timestamps.len() != prices.len() || features.len() != prices.len() {
            return Err(MLError::InvalidInput(format!(
                "Length mismatch: timestamps={}, features={}, prices={}",
                timestamps.len(),
                features.len(),
                prices.len()
            )));
        }

        // Validate all prices are positive before computing log returns
        for (i, &p) in prices.iter().enumerate() {
            if p <= 0.0 || !p.is_finite() {
                return Err(MLError::InvalidInput(format!(
                    "Price at index {} must be positive and finite, got {}",
                    i, p
                )));
            }
        }

        // Compute log returns using windows(2) with safe pattern matching
        let returns: Vec<f64> = prices
            .windows(2)
            .filter_map(|w| {
                if let [a, b] = *w {
                    Some((b / a).ln())
                } else {
                    None
                }
            })
            .collect();

        Ok(Self {
            timestamps,
            features,
            prices,
            returns,
        })
    }

    /// Number of bars in the time series.
    pub fn len(&self) -> usize {
        self.prices.len()
    }

    /// Returns `true` if the time series contains no data.
    pub fn is_empty(&self) -> bool {
        self.prices.is_empty()
    }

    /// Extract a sub-range `[start, end)` as a new `TimeSeriesData`.
    ///
    /// The slice must contain at least 2 bars so that log returns can
    /// be computed for the sub-range.
    ///
    /// # Errors
    ///
    /// Returns [`MLError::InvalidInput`] if:
    /// - `start >= end`
    /// - `end > self.len()`
    /// - The resulting slice has fewer than 2 elements
    pub fn slice(&self, start: usize, end: usize) -> Result<Self, MLError> {
        if start >= end {
            return Err(MLError::InvalidInput(format!(
                "Invalid slice range: start ({}) must be less than end ({})",
                start, end
            )));
        }

        if end > self.len() {
            return Err(MLError::InvalidInput(format!(
                "Slice end ({}) exceeds data length ({})",
                end,
                self.len()
            )));
        }

        let sub_len = end - start;
        if sub_len < 2 {
            return Err(MLError::InvalidInput(format!(
                "Slice must contain at least 2 elements, got {}",
                sub_len
            )));
        }

        let sub_timestamps = self
            .timestamps
            .get(start..end)
            .unwrap_or_default()
            .to_vec();
        let sub_features = self
            .features
            .get(start..end)
            .unwrap_or_default()
            .to_vec();
        let sub_prices = self
            .prices
            .get(start..end)
            .unwrap_or_default()
            .to_vec();

        // Recompute returns from the sub-range prices
        Self::new(sub_timestamps, sub_features, sub_prices)
    }
}

/// Trait for strategies that can be validated through walk-forward,
/// deflated Sharpe ratio, and probability of backtest overfitting tests.
pub trait ValidatableStrategy {
    /// Train the strategy on the provided time series data.
    ///
    /// # Errors
    ///
    /// Returns [`MLError`] if training fails.
    fn train(&mut self, data: &TimeSeriesData) -> Result<(), MLError>;

    /// Evaluate the strategy on out-of-sample data.
    ///
    /// Returns a vector of daily PnL values (one per bar in `data`).
    ///
    /// # Errors
    ///
    /// Returns [`MLError`] if evaluation fails.
    fn evaluate(&self, data: &TimeSeriesData) -> Result<Vec<f64>, MLError>;

    /// Human-readable strategy name.
    fn name(&self) -> &str;

    /// Reset internal state so the strategy can be retrained from scratch.
    ///
    /// # Errors
    ///
    /// Returns [`MLError`] if reset fails.
    fn reset(&mut self) -> Result<(), MLError>;
}

#[cfg(test)]
#[allow(clippy::redundant_clone, clippy::unreachable, clippy::useless_vec)]
mod tests {
    use super::*;
    use chrono::TimeZone;

    /// Helper: create N UTC timestamps starting from 2024-01-01.
    fn make_timestamps(n: usize) -> Vec<DateTime<Utc>> {
        (0..n)
            .map(|i| {
                Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
                    .single()
                    .unwrap_or_else(Utc::now)
                    + chrono::Duration::days(i as i64)
            })
            .collect()
    }

    /// Helper: create N dummy feature rows of given dimension.
    fn make_features(n: usize, dim: usize) -> Vec<Vec<f32>> {
        (0..n).map(|_| vec![0.0_f32; dim]).collect()
    }

    #[test]
    fn test_time_series_data_new_computes_returns() {
        let prices = vec![100.0, 110.0, 105.0, 120.0];
        let n = prices.len();
        let ts = TimeSeriesData::new(make_timestamps(n), make_features(n, 3), prices.clone());
        assert!(ts.is_ok(), "Expected Ok, got {:?}", ts.err());
        let ts = ts.unwrap_or_else(|_| unreachable!());

        assert_eq!(ts.returns.len(), n - 1);

        // Verify each return is ln(p[i+1] / p[i])
        for (i, &r) in ts.returns.iter().enumerate() {
            let p_curr = prices.get(i).copied().unwrap_or(1.0);
            let p_next = prices.get(i + 1).copied().unwrap_or(1.0);
            let expected = (p_next / p_curr).ln();
            assert!(
                (r - expected).abs() < 1e-12,
                "Return at index {} mismatch: got {}, expected {}",
                i,
                r,
                expected
            );
        }
    }

    #[test]
    fn test_time_series_data_length_mismatch_errors() {
        // timestamps has 3 elements, prices has 4
        let result = TimeSeriesData::new(
            make_timestamps(3),
            make_features(4, 2),
            vec![100.0, 110.0, 105.0, 120.0],
        );
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(
            err_msg.contains("Length mismatch"),
            "Expected 'Length mismatch' in error, got: {}",
            err_msg
        );
    }

    #[test]
    fn test_time_series_data_too_short_errors() {
        // Only 1 price
        let result = TimeSeriesData::new(
            make_timestamps(1),
            make_features(1, 2),
            vec![100.0],
        );
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(
            err_msg.contains("at least 2 prices"),
            "Expected 'at least 2 prices' in error, got: {}",
            err_msg
        );
    }

    #[test]
    fn test_slice_valid_range() {
        let prices = vec![100.0, 110.0, 105.0, 120.0, 115.0];
        let n = prices.len();
        let ts = TimeSeriesData::new(make_timestamps(n), make_features(n, 2), prices.clone());
        let ts = ts.unwrap_or_else(|_| unreachable!());

        // Slice [1, 4) => prices [110, 105, 120]
        let sliced = ts.slice(1, 4);
        assert!(sliced.is_ok(), "Expected Ok, got {:?}", sliced.err());
        let sliced = sliced.unwrap_or_else(|_| unreachable!());

        assert_eq!(sliced.len(), 3);
        assert_eq!(sliced.returns.len(), 2);

        // Verify sliced prices
        let expected_prices = vec![110.0, 105.0, 120.0];
        for (i, (&got, &want)) in sliced.prices.iter().zip(expected_prices.iter()).enumerate() {
            assert!(
                (got - want).abs() < 1e-12,
                "Price mismatch at index {}: got {}, expected {}",
                i,
                got,
                want
            );
        }

        // Verify sliced returns are recomputed correctly
        let expected_r0 = (105.0_f64 / 110.0).ln();
        let expected_r1 = (120.0_f64 / 105.0).ln();
        let r0 = sliced.returns.first().copied().unwrap_or(f64::NAN);
        let r1 = sliced.returns.get(1).copied().unwrap_or(f64::NAN);
        assert!((r0 - expected_r0).abs() < 1e-12);
        assert!((r1 - expected_r1).abs() < 1e-12);
    }

    #[test]
    fn test_slice_out_of_bounds_errors() {
        let prices = vec![100.0, 110.0, 105.0];
        let n = prices.len();
        let ts = TimeSeriesData::new(make_timestamps(n), make_features(n, 2), prices);
        let ts = ts.unwrap_or_else(|_| unreachable!());

        // end > len
        let result = ts.slice(0, 10);
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(
            err_msg.contains("exceeds data length"),
            "Expected 'exceeds data length' in error, got: {}",
            err_msg
        );

        // start >= end
        let result = ts.slice(2, 1);
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(
            err_msg.contains("must be less than end"),
            "Expected 'must be less than end' in error, got: {}",
            err_msg
        );

        // Slice too small (only 1 element)
        let result = ts.slice(1, 2);
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(
            err_msg.contains("at least 2 elements"),
            "Expected 'at least 2 elements' in error, got: {}",
            err_msg
        );
    }
}
