//! Detrended Fluctuation Analysis (DFA) for Hurst exponent estimation
//!
//! Reference: Peng et al. (1994), "Mosaic organization of DNA nucleotides"
//! Implementation follows PhysioNet DFA reference (dfa.c).
//!
//! DFA-1 (linear detrending) is the standard variant. The algorithm:
//! 1. Compute cumulative profile Y(i) = Σ(x[k] - mean)
//! 2. Divide into non-overlapping boxes of size n
//! 3. In each box, fit polynomial of degree `order`, compute RMS residual
//! 4. Log-log regression of F(n) vs n → slope = alpha (≈ H for stationary)

use linreg::linear_regression;

/// DFA-1 (linear detrending) Hurst estimator.
///
/// Returns the DFA scaling exponent alpha, which equals H for stationary
/// fractional Gaussian noise. For non-stationary signals (fBm), H = alpha - 1.
///
/// # Arguments
/// * `x` - Input time series (prices or returns)
///
/// # Returns
/// DFA scaling exponent (typically 0–1 for stationary signals)
pub fn dfa(x: &[f64]) -> f64 {
    dfa_order(x, 1)
}

/// DFA with configurable polynomial order.
///
/// * `order=1`: DFA-1 (linear detrending) — standard
/// * `order=2`: DFA-2 (quadratic detrending) — removes linear trends
pub fn dfa_order(x: &[f64], order: usize) -> f64 {
    let n = x.len();
    if n < 16 {
        return 0.5;
    }

    // Step 1: Compute cumulative profile Y(i) = Σ(x[k] - mean)
    let mean = x.iter().sum::<f64>() / n as f64;
    let mut profile = Vec::with_capacity(n);
    let mut cumsum = 0.0;
    for &val in x {
        cumsum += val - mean;
        profile.push(cumsum);
    }

    // Step 2: Select box sizes — eighth-octave geometric spacing
    let min_box = (order + 2).max(4);
    let max_box = n / 4;
    if min_box >= max_box {
        return 0.5;
    }

    let mut box_sizes = Vec::with_capacity(16);
    let mut s = min_box as f64;
    while (s as usize) <= max_box {
        let bs = s as usize;
        if box_sizes.last().is_none_or(|&last| last != bs) {
            box_sizes.push(bs);
        }
        s *= 2.0_f64.powf(0.125); // eighth-octave spacing
    }

    if box_sizes.len() < 3 {
        return 0.5;
    }

    // Step 3: For each box size, compute RMS fluctuation
    let mut log_n = Vec::with_capacity(box_sizes.len());
    let mut log_f = Vec::with_capacity(box_sizes.len());

    for &box_size in &box_sizes {
        let num_boxes = n / box_size;
        if num_boxes == 0 {
            continue;
        }

        let mut f2_sum = 0.0;
        let mut total_points = 0;

        for seg in 0..num_boxes {
            let start = seg * box_size;
            let end = start + box_size;
            let segment = &profile[start..end];

            // Fit polynomial of degree `order` via least-squares
            let rms = segment_rms_detrended(segment, order);
            f2_sum += rms * rms * box_size as f64;
            total_points += box_size;
        }

        if total_points > 0 {
            let f_n = (f2_sum / total_points as f64).sqrt();
            if f_n > 0.0 {
                log_n.push((box_size as f64).ln());
                log_f.push(f_n.ln());
            }
        }
    }

    if log_n.len() < 3 {
        return 0.5;
    }

    // Step 4: Linear regression of log(F(n)) vs log(n)
    let result: Result<(f64, f64), _> = linear_regression(&log_n, &log_f);
    match result {
        Ok((slope, _)) => {
            if slope.is_finite() {
                slope
            } else {
                0.5
            }
        }
        Err(_) => 0.5,
    }
}

/// Compute RMS of residuals after polynomial detrending of a segment.
///
/// For order=1 (linear), fits y = a + b*x via closed-form least squares.
/// For higher orders, uses Gaussian elimination.
fn segment_rms_detrended(segment: &[f64], order: usize) -> f64 {
    let n = segment.len();
    if n <= order {
        return 0.0;
    }

    if order == 1 {
        // Fast path: closed-form linear regression
        return segment_rms_linear(segment);
    }

    // General path: polynomial fit via normal equations
    // Solve (X^T X) β = X^T y where X is Vandermonde matrix
    let p = order + 1; // number of coefficients
    let mut xtx = vec![0.0; p * p];
    let mut xty = vec![0.0; p];

    for (i, &seg_val) in segment.iter().enumerate().take(n) {
        let t = i as f64;
        // Build powers of t
        let mut powers = Vec::with_capacity(p);
        let mut tp = 1.0;
        for _ in 0..p {
            powers.push(tp);
            tp *= t;
        }

        for row in 0..p {
            xty[row] += powers[row] * seg_val;
            for col in 0..p {
                xtx[row * p + col] += powers[row] * powers[col];
            }
        }
    }

    // Solve via Gaussian elimination with partial pivoting
    let Some(coeffs) = solve_linear_system(&mut xtx, &mut xty, p) else {
        return 0.0;
    };

    // Compute RMS of residuals
    let mut sum_sq = 0.0;
    for (i, &seg_val) in segment.iter().enumerate().take(n) {
        let t = i as f64;
        let mut fitted = 0.0;
        let mut tp = 1.0;
        for c in &coeffs {
            fitted += c * tp;
            tp *= t;
        }
        let residual = seg_val - fitted;
        sum_sq += residual * residual;
    }

    (sum_sq / n as f64).sqrt()
}

/// Fast linear detrending: y = a + b*x, RMS of residuals
#[inline]
fn segment_rms_linear(segment: &[f64]) -> f64 {
    let n = segment.len();
    let n_f = n as f64;
    let inv_n = 1.0 / n_f;

    // Compute sums for least-squares: Σx, Σy, Σxy, Σx²
    let mut sx = 0.0;
    let mut sy = 0.0;
    let mut sxy = 0.0;
    let mut sxx = 0.0;

    for (i, &y) in segment.iter().enumerate().take(n) {
        let x = i as f64;
        sx += x;
        sy += y;
        sxy += x * y;
        sxx += x * x;
    }

    let denom = n_f * sxx - sx * sx;
    if denom.abs() < f64::EPSILON {
        // All x same (shouldn't happen) — just return std dev
        let mean = sy * inv_n;
        let var: f64 = segment
            .iter()
            .map(|&v| (v - mean) * (v - mean))
            .sum::<f64>()
            * inv_n;
        return var.sqrt();
    }

    let b = (n_f * sxy - sx * sy) / denom;
    let a = (sy - b * sx) * inv_n;

    // RMS of residuals
    let mut sum_sq = 0.0;
    for (i, &seg_val) in segment.iter().enumerate().take(n) {
        let residual = seg_val - (a + b * i as f64);
        sum_sq += residual * residual;
    }

    (sum_sq * inv_n).sqrt()
}

/// Gaussian elimination with partial pivoting
fn solve_linear_system(a: &mut [f64], b: &mut [f64], n: usize) -> Option<Vec<f64>> {
    // Forward elimination
    for col in 0..n {
        // Partial pivoting
        let mut max_val = a[col * n + col].abs();
        let mut max_row = col;
        for row in (col + 1)..n {
            let val = a[row * n + col].abs();
            if val > max_val {
                max_val = val;
                max_row = row;
            }
        }

        if max_val < f64::EPSILON {
            return None; // Singular
        }

        // Swap rows
        if max_row != col {
            for j in 0..n {
                a.swap(col * n + j, max_row * n + j);
            }
            b.swap(col, max_row);
        }

        // Eliminate below
        let pivot = a[col * n + col];
        for row in (col + 1)..n {
            let factor = a[row * n + col] / pivot;
            for j in col..n {
                a[row * n + j] -= factor * a[col * n + j];
            }
            b[row] -= factor * b[col];
        }
    }

    // Back substitution
    let mut x = vec![0.0; n];
    for i in (0..n).rev() {
        let mut sum = b[i];
        for j in (i + 1)..n {
            sum -= a[i * n + j] * x[j];
        }
        x[i] = sum / a[i * n + i];
    }

    Some(x)
}

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

    #[test]
    fn test_dfa_trending() {
        let prices: Vec<f64> = (0..256).map(|i| 100.0 + i as f64 * 0.5).collect();
        let h = dfa(&prices);
        assert!(h > 0.7, "Trending series should have high DFA alpha: {h}");
    }

    #[test]
    fn test_dfa_finite_output() {
        // Verify DFA returns finite values for various series types
        let mut prices = Vec::with_capacity(256);
        let mut p = 100.0;
        let mut seed: u64 = 42;
        for _ in 0..256 {
            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
            let r = ((seed >> 33) as f64 / (u32::MAX as f64)) - 0.5;
            p += r * 0.2;
            prices.push(p);
        }
        let h = dfa(&prices);
        assert!(h.is_finite(), "DFA must return finite value: {h}");
    }

    #[test]
    fn test_dfa_short_series() {
        let h = dfa(&[1.0, 2.0, 3.0]);
        assert_eq!(h, 0.5, "Short series should return 0.5");
    }

    #[test]
    fn test_dfa_constant() {
        let prices = vec![100.0; 128];
        let h = dfa(&prices);
        // Constant series: all deviations are 0, profile is flat
        // F(n) = 0 for all n → log(0) is undefined → should handle gracefully
        assert!(h.is_finite(), "Constant series should return finite H: {h}");
    }

    #[test]
    fn test_dfa_mean_reverting() {
        let prices: Vec<f64> = (0..256)
            .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
            .collect();
        let h = dfa(&prices);
        assert!(
            h < 0.5,
            "Mean-reverting series should have low DFA alpha: {h}"
        );
    }
}
