use crate::protocol::LsvPoint;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PeakKind {
    FreeCl,
    TotalCl,
    Crossover,
}

#[derive(Debug, Clone)]
pub struct LsvPeak {
    pub v_mv: f32,
    pub i_ua: f32,
    pub kind: PeakKind,
}

pub fn smooth(data: &[f32], window: usize) -> Vec<f32> {
    let n = data.len();
    if n == 0 || window < 2 {
        return data.to_vec();
    }
    let half = window / 2;
    let mut out = Vec::with_capacity(n);
    for i in 0..n {
        let lo = i.saturating_sub(half);
        let hi = (i + half).min(n - 1);
        let sum: f32 = data[lo..=hi].iter().sum();
        out.push(sum / (hi - lo + 1) as f32);
    }
    out
}

pub fn find_extrema(_v: &[f32], i_smooth: &[f32], min_prominence: f32) -> Vec<(usize, bool)> {
    let n = i_smooth.len();
    if n < 3 {
        return Vec::new();
    }
    let mut candidates: Vec<(usize, bool)> = Vec::new();
    for i in 1..n - 1 {
        let prev = i_smooth[i - 1];
        let curr = i_smooth[i];
        let next = i_smooth[i + 1];
        if curr > prev && curr > next {
            candidates.push((i, true));
        } else if curr < prev && curr < next {
            candidates.push((i, false));
        }
    }

    candidates.retain(|&(idx, is_max)| {
        let val = i_smooth[idx];
        let left_bound = i_smooth[..idx].iter().copied()
            .reduce(if is_max { f32::min } else { f32::max })
            .unwrap_or(val);
        let right_bound = i_smooth[idx + 1..].iter().copied()
            .reduce(if is_max { f32::min } else { f32::max })
            .unwrap_or(val);
        let prom = if is_max {
            val - left_bound.max(right_bound)
        } else {
            left_bound.min(right_bound) - val
        };
        prom >= min_prominence
    });

    candidates
}

/// Detect Q/HQ redox peak in the -100 to +600 mV window.
/// Returns peak voltage in mV if found.
pub fn detect_qhq_peak(points: &[LsvPoint]) -> Option<f32> {
    if points.len() < 5 {
        return None;
    }

    let i_vals: Vec<f32> = points.iter().map(|p| p.i_ua).collect();
    let v_vals: Vec<f32> = points.iter().map(|p| p.v_mv).collect();

    let window = 5.max(points.len() / 50);
    let smoothed = smooth(&i_vals, window);

    let i_min = smoothed.iter().copied().fold(f32::INFINITY, f32::min);
    let i_max = smoothed.iter().copied().fold(f32::NEG_INFINITY, f32::max);
    let prominence = (i_max - i_min) * 0.05;

    let extrema = find_extrema(&v_vals, &smoothed, prominence);

    extrema.iter()
        .filter(|&&(idx, is_max)| is_max && v_vals[idx] >= -100.0 && v_vals[idx] <= 600.0)
        .max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal))
        .map(|&(idx, _)| v_vals[idx])
}

#[derive(Debug, Clone, Copy)]
pub struct ClPotentials {
    pub v_free: f32,
    pub v_free_detected: bool,
    pub v_total: f32,
    pub v_total_detected: bool,
}

pub fn derive_cl_potentials(points: &[LsvPoint]) -> ClPotentials {
    let default = ClPotentials {
        v_free: 100.0,
        v_free_detected: false,
        v_total: -200.0,
        v_total_detected: false,
    };
    if points.len() < 5 {
        return default;
    }

    let i_vals: Vec<f32> = points.iter().map(|p| p.i_ua).collect();
    let v_vals: Vec<f32> = points.iter().map(|p| p.v_mv).collect();

    let window = 5.max(points.len() / 50);
    let smoothed = smooth(&i_vals, window);

    let i_min = smoothed.iter().copied().fold(f32::INFINITY, f32::min);
    let i_max = smoothed.iter().copied().fold(f32::NEG_INFINITY, f32::max);
    let prominence = (i_max - i_min) * 0.05;

    let extrema = find_extrema(&v_vals, &smoothed, prominence);

    // v_free: most prominent cathodic peak (is_max==false) in +300 to -300 mV
    let free_peak = extrema.iter()
        .filter(|&&(idx, is_max)| !is_max && v_vals[idx] >= -300.0 && v_vals[idx] <= 300.0)
        .min_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));

    let (v_free, v_free_detected, free_idx) = match free_peak {
        Some(&(idx, _)) => (v_vals[idx], true, Some(idx)),
        None => (100.0, false, None),
    };

    // v_total: secondary cathodic peak between (v_free - 100) and -500 mV, excluding free peak
    let total_lo = -500.0_f32;
    let total_hi = v_free - 100.0;
    let total_peak = extrema.iter()
        .filter(|&&(idx, is_max)| {
            !is_max
                && v_vals[idx] >= total_lo
                && v_vals[idx] <= total_hi
                && Some(idx) != free_idx
        })
        .min_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));

    let (v_total, v_total_detected) = match total_peak {
        Some(&(idx, _)) => (v_vals[idx], true),
        None => (v_free - 300.0, false),
    };

    let v_total = v_total.max(-400.0);

    ClPotentials { v_free, v_free_detected, v_total, v_total_detected }
}

pub fn detect_peaks(points: &[LsvPoint]) -> Vec<LsvPeak> {
    if points.len() < 5 {
        return Vec::new();
    }

    let i_vals: Vec<f32> = points.iter().map(|p| p.i_ua).collect();
    let v_vals: Vec<f32> = points.iter().map(|p| p.v_mv).collect();

    let window = 5.max(points.len() / 50);
    let smoothed = smooth(&i_vals, window);

    let i_min = smoothed.iter().copied().fold(f32::INFINITY, f32::min);
    let i_max = smoothed.iter().copied().fold(f32::NEG_INFINITY, f32::max);
    let prominence = (i_max - i_min) * 0.05;

    let extrema = find_extrema(&v_vals, &smoothed, prominence);

    let mut peaks: Vec<LsvPeak> = Vec::new();

    // find crossover: where current changes sign
    for i in 1..smoothed.len() {
        if smoothed[i - 1].signum() != smoothed[i].signum() && smoothed[i - 1].signum() != 0.0 {
            let frac = smoothed[i - 1].abs() / (smoothed[i - 1].abs() + smoothed[i].abs());
            let v_cross = v_vals[i - 1] + frac * (v_vals[i] - v_vals[i - 1]);
            peaks.push(LsvPeak {
                v_mv: v_cross,
                i_ua: 0.0,
                kind: PeakKind::Crossover,
            });
            break;
        }
    }

    // largest peak in positive voltage region -> FreeCl
    let free_cl = extrema.iter()
        .filter(|&&(idx, is_max)| is_max && v_vals[idx] >= 0.0)
        .max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));
    if let Some(&(idx, _)) = free_cl {
        peaks.push(LsvPeak {
            v_mv: v_vals[idx],
            i_ua: smoothed[idx],
            kind: PeakKind::FreeCl,
        });
    }

    // largest peak in negative voltage region -> TotalCl
    let total_cl = extrema.iter()
        .filter(|&&(idx, is_max)| is_max && v_vals[idx] < 0.0)
        .max_by(|a, b| smoothed[a.0].partial_cmp(&smoothed[b.0]).unwrap_or(std::cmp::Ordering::Equal));
    if let Some(&(idx, _)) = total_cl {
        peaks.push(LsvPeak {
            v_mv: v_vals[idx],
            i_ua: smoothed[idx],
            kind: PeakKind::TotalCl,
        });
    }

    peaks
}
