use std::collections::{BTreeMap, BTreeSet};

use super::loader::{load_sample_ccr_from_meta, load_sample_cir_from_meta};
use super::model::{
    ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SequenceMeta, SequenceSample, Side,
};

pub(super) fn build_intra_rp_churn_streaming(
    left: &[SequenceMeta],
    right: &[SequenceMeta],
) -> Result<IntraRpChurn, String> {
    let left_records = build_side_records_streaming(Side::Left, left)?;
    let right_records = build_side_records_streaming(Side::Right, right)?;
    let mut summary = summarize_records(&left_records);
    summary.extend(summarize_records(&right_records));
    Ok(IntraRpChurn {
        left: left_records,
        right: right_records,
        summary,
    })
}

fn build_side_records_streaming(
    side: Side,
    samples: &[SequenceMeta],
) -> Result<Vec<ChurnRecord>, String> {
    let mut records = Vec::new();
    for pair in samples.windows(2) {
        let from_cir = load_sample_cir_from_meta(&pair[0])?;
        let to_cir = load_sample_cir_from_meta(&pair[1])?;
        records.push(record_churn(
            side,
            &from_cir,
            &to_cir,
            "object",
            &from_cir.object_hashes,
            &to_cir.object_hashes,
        ));
        records.push(record_churn(
            side,
            &from_cir,
            &to_cir,
            "reject",
            &from_cir.rejects,
            &to_cir.rejects,
        ));
        drop(from_cir);
        drop(to_cir);

        let from_ccr = load_sample_ccr_from_meta(&pair[0])?;
        let to_ccr = load_sample_ccr_from_meta(&pair[1])?;
        let from_output = output_keys(&from_ccr);
        let to_output = output_keys(&to_ccr);
        records.push(record_churn(
            side,
            &from_ccr,
            &to_ccr,
            "output",
            &from_output,
            &to_output,
        ));
        records.push(record_churn(
            side,
            &from_ccr,
            &to_ccr,
            "vrp_output",
            &from_ccr.vrps,
            &to_ccr.vrps,
        ));
        records.push(record_churn(
            side,
            &from_ccr,
            &to_ccr,
            "vap_output",
            &from_ccr.vaps,
            &to_ccr.vaps,
        ));
    }
    Ok(records)
}

fn output_keys(sample: &SequenceSample) -> BTreeSet<String> {
    sample
        .vrps
        .iter()
        .map(|key| format!("vrp|{key}"))
        .chain(sample.vaps.iter().map(|key| format!("vap|{key}")))
        .collect()
}

fn record_churn(
    side: Side,
    from: &SequenceSample,
    to: &SequenceSample,
    set_type: &'static str,
    from_set: &BTreeSet<String>,
    to_set: &BTreeSet<String>,
) -> ChurnRecord {
    let added_count = to_set.difference(from_set).count();
    let removed_count = from_set.difference(to_set).count();
    let changed_abs = added_count + removed_count;
    let union_count = from_set.union(to_set).count();
    let changed_ratio_from = ratio(changed_abs, from_set.len());
    let changed_ratio_union = ratio(changed_abs, union_count);
    ChurnRecord {
        side,
        rp_id: from.raw.rp_id.clone(),
        from_seq: from.raw.seq,
        to_seq: to.raw.seq,
        from_run_id: from.raw.run_id.clone(),
        to_run_id: to.raw.run_id.clone(),
        set_type,
        from_count: from_set.len(),
        to_count: to_set.len(),
        added_count,
        removed_count,
        changed_abs,
        union_count,
        changed_ratio_from,
        changed_ratio_union,
    }
}

fn ratio(numerator: usize, denominator: usize) -> f64 {
    if denominator == 0 {
        0.0
    } else {
        numerator as f64 / denominator as f64
    }
}

fn summarize_records(records: &[ChurnRecord]) -> Vec<ChurnSummaryRecord> {
    let mut grouped: BTreeMap<(Side, &'static str), Vec<&ChurnRecord>> = BTreeMap::new();
    for record in records {
        grouped
            .entry((record.side, record.set_type))
            .or_default()
            .push(record);
    }
    grouped
        .into_iter()
        .map(|((side, set_type), group)| {
            let count = group.len() as f64;
            let max_changed_abs = group
                .iter()
                .map(|record| record.changed_abs)
                .max()
                .unwrap_or(0);
            let avg_changed_abs = if count == 0.0 {
                0.0
            } else {
                group
                    .iter()
                    .map(|record| record.changed_abs as f64)
                    .sum::<f64>()
                    / count
            };
            let max_changed_ratio_from = group
                .iter()
                .map(|record| record.changed_ratio_from)
                .fold(0.0, f64::max);
            let avg_changed_ratio_from = if count == 0.0 {
                0.0
            } else {
                group
                    .iter()
                    .map(|record| record.changed_ratio_from)
                    .sum::<f64>()
                    / count
            };
            let max_changed_ratio_union = group
                .iter()
                .map(|record| record.changed_ratio_union)
                .fold(0.0, f64::max);
            let avg_changed_ratio_union = if count == 0.0 {
                0.0
            } else {
                group
                    .iter()
                    .map(|record| record.changed_ratio_union)
                    .sum::<f64>()
                    / count
            };
            ChurnSummaryRecord {
                side,
                set_type,
                max_changed_abs,
                avg_changed_abs,
                max_changed_ratio_from,
                avg_changed_ratio_from,
                max_changed_ratio_union,
                avg_changed_ratio_union,
            }
        })
        .collect()
}
