//! Per-file tick summary statistics.

use crate::analysis::model::Tick;
use crate::analysis::session::Session;

/// One summary row for a tick file.
#[derive(Debug, Clone)]
pub struct StatRow {
    pub symbol: String,
    pub format: String,
    pub ticks: u64,
    pub first: Option<u32>,
    pub last: Option<u32>,
    pub min: f64,
    pub max: f64,
    pub mean: f64,
    pub vwap: f64,
    pub volume: i64,
    pub gaps: u64,
}

/// Incrementally accumulates summary statistics from a stream of ascending ticks, so a file's
/// ticks never need to be materialized all at once.
///
/// Gaps are counted only between consecutive ticks on the **same** local day (per `session`'s
/// timezone) whose spacing exceeds `max_gap` seconds, so the overnight/weekend inter-day boundary
/// is never miscounted as a gap.
pub struct StatAccumulator<'a> {
    session: &'a Session,
    max_gap: u32,
    count: u64,
    first: Option<u32>,
    last: u32,
    prev_time: Option<u32>,
    min: f64,
    max: f64,
    price_sum: f64,
    weighted: f64,
    volume: i64,
    gaps: u64,
}

impl<'a> StatAccumulator<'a> {
    pub fn new(max_gap: u32, session: &'a Session) -> Self {
        Self {
            session,
            max_gap,
            count: 0,
            first: None,
            last: 0,
            prev_time: None,
            min: f64::INFINITY,
            max: f64::NEG_INFINITY,
            price_sum: 0.0,
            weighted: 0.0,
            volume: 0,
            gaps: 0,
        }
    }

    pub fn push(&mut self, tick: &Tick) {
        if self.first.is_none() {
            self.first = Some(tick.time);
        }
        self.last = tick.time;
        self.count += 1;
        self.min = self.min.min(tick.price);
        self.max = self.max.max(tick.price);
        self.price_sum += tick.price;
        self.weighted += tick.price * f64::from(tick.size);
        self.volume += i64::from(tick.size);

        if let Some(prev) = self.prev_time {
            let delta = tick.time.saturating_sub(prev);
            if delta > self.max_gap && self.session.day_key(prev) == self.session.day_key(tick.time)
            {
                self.gaps += 1;
            }
        }
        self.prev_time = Some(tick.time);
    }

    pub fn finish(self, symbol: String, format: String) -> StatRow {
        if self.count == 0 {
            return StatRow {
                symbol,
                format,
                ticks: 0,
                first: None,
                last: None,
                min: f64::NAN,
                max: f64::NAN,
                mean: f64::NAN,
                vwap: f64::NAN,
                volume: 0,
                gaps: 0,
            };
        }
        let vwap = if self.volume != 0 {
            self.weighted / self.volume as f64
        } else {
            f64::NAN
        };
        StatRow {
            symbol,
            format,
            ticks: self.count,
            first: self.first,
            last: Some(self.last),
            min: self.min,
            max: self.max,
            mean: self.price_sum / self.count as f64,
            vwap,
            volume: self.volume,
            gaps: self.gaps,
        }
    }
}

/// Computes the summary for one file's ticks (convenience wrapper over [`StatAccumulator`]).
pub fn compute_stat(
    symbol: String,
    format: String,
    ticks: &[Tick],
    max_gap: u32,
    session: &Session,
) -> StatRow {
    let mut acc = StatAccumulator::new(max_gap, session);
    for tick in ticks {
        acc.push(tick);
    }
    acc.finish(symbol, format)
}

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

    fn tick(time: u32, price: f64, size: i32) -> Tick {
        Tick { time, price, size }
    }

    #[test]
    fn counts_intraday_gaps_only() {
        let session = Session::new("America/New_York", "00:00-24:00").unwrap();
        // 14:30Z, +120s (gap > 60), then next "day" jump (~24h) which must NOT count.
        let base = 1_704_205_800u32; // 2024-01-02 14:30Z
        let ticks = vec![
            tick(base, 10.0, 100),
            tick(base + 120, 10.5, 100), // intraday gap of 120s -> counts
            tick(base + 86_400 + 60, 11.0, 100), // next day -> inter-day, not counted
        ];
        let row = compute_stat("AAPL".into(), "fwob-v2".into(), &ticks, 60, &session);
        assert_eq!(row.gaps, 1);
        assert_eq!(row.ticks, 3);
        assert_eq!(row.volume, 300);
        assert_eq!(row.first, Some(base));
    }
}
