use std::collections::BTreeSet;

use super::args::Args;
use super::io::format_time;
use super::loader::{load_sample_ccr_from_meta, load_sample_cir_from_meta};
use super::model::{
    SandwichAnalysis, SandwichGroupStats, SandwichHeatmapRow, SandwichRecord, SequenceMeta,
    SequenceSample, Side,
};

pub(super) fn build_sandwich_analysis_streaming(
    args: &Args,
    left: &[SequenceMeta],
    right: &[SequenceMeta],
) -> Result<SandwichAnalysis, String> {
    let mut analysis = SandwichAnalysis::default();
    analyze_sandwich_cir_streaming(&mut analysis, Side::Left, left, right, args)?;
    analyze_sandwich_cir_streaming(&mut analysis, Side::Right, right, left, args)?;
    analyze_sandwich_ccr_streaming(&mut analysis, Side::Left, left, right, args)?;
    analyze_sandwich_ccr_streaming(&mut analysis, Side::Right, right, left, args)?;
    Ok(analysis)
}

fn analyze_sandwich_cir_streaming(
    analysis: &mut SandwichAnalysis,
    source_side: Side,
    source: &[SequenceMeta],
    peer: &[SequenceMeta],
    args: &Args,
) -> Result<(), String> {
    for pair in source.windows(2) {
        let source_start_meta = &pair[0];
        let source_end_meta = &pair[1];
        if source_start_meta.validation_time >= source_end_meta.validation_time {
            continue;
        }
        let peer_indices = peer_meta_indices_between(peer, source_start_meta, source_end_meta);
        if peer_indices.is_empty() {
            continue;
        }
        let source_start = load_sample_cir_from_meta(source_start_meta)?;
        let source_end = load_sample_cir_from_meta(source_end_meta)?;
        for peer_index in peer_indices {
            let peer_sample = load_sample_cir_from_meta(&peer[peer_index])?;
            analyze_loaded_sandwich_objects(
                analysis,
                source_side,
                &source_start,
                &source_end,
                &peer_sample,
                args,
            );
            analyze_loaded_sandwich_set(
                analysis,
                "reject_uri",
                "PEER_MISSING_STABLE_REJECT",
                source_side,
                &source_start,
                &source_end,
                &peer_sample,
                |sample| &sample.rejects,
                args,
            );
        }
    }
    Ok(())
}

fn analyze_sandwich_ccr_streaming(
    analysis: &mut SandwichAnalysis,
    source_side: Side,
    source: &[SequenceMeta],
    peer: &[SequenceMeta],
    args: &Args,
) -> Result<(), String> {
    for pair in source.windows(2) {
        let source_start_meta = &pair[0];
        let source_end_meta = &pair[1];
        if source_start_meta.validation_time >= source_end_meta.validation_time {
            continue;
        }
        let peer_indices = peer_meta_indices_between(peer, source_start_meta, source_end_meta);
        if peer_indices.is_empty() {
            continue;
        }
        let source_start = load_sample_ccr_from_meta(source_start_meta)?;
        let source_end = load_sample_ccr_from_meta(source_end_meta)?;
        for peer_index in peer_indices {
            let peer_sample = load_sample_ccr_from_meta(&peer[peer_index])?;
            analyze_loaded_sandwich_set(
                analysis,
                "vrp_output",
                "PEER_MISSING_STABLE_OUTPUT",
                source_side,
                &source_start,
                &source_end,
                &peer_sample,
                |sample| &sample.vrps,
                args,
            );
            analyze_loaded_sandwich_set(
                analysis,
                "vap_output",
                "PEER_MISSING_STABLE_OUTPUT",
                source_side,
                &source_start,
                &source_end,
                &peer_sample,
                |sample| &sample.vaps,
                args,
            );
        }
    }
    Ok(())
}

fn peer_meta_indices_between(
    peer: &[SequenceMeta],
    source_start: &SequenceMeta,
    source_end: &SequenceMeta,
) -> Vec<usize> {
    peer.iter()
        .enumerate()
        .filter_map(|(index, sample)| {
            (source_start.validation_time < sample.validation_time
                && sample.validation_time < source_end.validation_time)
                .then_some(index)
        })
        .collect()
}

fn analyze_loaded_sandwich_objects(
    analysis: &mut SandwichAnalysis,
    source_side: Side,
    source_start: &SequenceSample,
    source_end: &SequenceSample,
    peer_sample: &SequenceSample,
    args: &Args,
) {
    for (uri, source_hash) in &source_start.objects {
        if source_end.objects.get(uri) != Some(source_hash) {
            continue;
        }
        match peer_sample.objects.get(uri) {
            Some(peer_hash) if peer_hash == source_hash => {}
            Some(peer_hash) => analysis.add(
                "PEER_HASH_MISMATCH_STABLE_OBJECT",
                sandwich_record(
                    "PEER_HASH_MISMATCH_STABLE_OBJECT",
                    "object",
                    uri.clone(),
                    source_side,
                    source_start,
                    source_end,
                    peer_sample,
                    Some(source_hash.clone()),
                    Some(peer_hash.clone()),
                    "source interval has stable object hash; peer sample has same URI with another hash",
                ),
                args.sample_limit,
            ),
            None => analysis.add(
                "PEER_MISSING_STABLE_OBJECT",
                sandwich_record(
                    "PEER_MISSING_STABLE_OBJECT",
                    "object",
                    uri.clone(),
                    source_side,
                    source_start,
                    source_end,
                    peer_sample,
                    Some(source_hash.clone()),
                    None,
                    "source interval has stable object hash; peer sample misses the URI",
                ),
                args.sample_limit,
            ),
        }
    }
}

fn analyze_loaded_sandwich_set<F>(
    analysis: &mut SandwichAnalysis,
    set_type: &'static str,
    classification: &'static str,
    source_side: Side,
    source_start: &SequenceSample,
    source_end: &SequenceSample,
    peer_sample: &SequenceSample,
    extract: F,
    args: &Args,
) where
    F: for<'a> Fn(&'a SequenceSample) -> &'a BTreeSet<String>,
{
    let start_set = extract(source_start);
    let end_set = extract(source_end);
    let peer_set = extract(peer_sample);
    for key in start_set {
        if !end_set.contains(key) || peer_set.contains(key) {
            continue;
        }
        analysis.add(
            classification,
            sandwich_record(
                classification,
                set_type,
                key.clone(),
                source_side,
                source_start,
                source_end,
                peer_sample,
                Some(key.clone()),
                None,
                "source interval has a stable key; peer sample misses the key",
            ),
            args.sample_limit,
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn sandwich_record(
    classification: &'static str,
    set_type: &'static str,
    key: String,
    source_side: Side,
    source_start: &SequenceSample,
    source_end: &SequenceSample,
    peer_sample: &SequenceSample,
    source_value: Option<String>,
    peer_value: Option<String>,
    note: &str,
) -> SandwichRecord {
    SandwichRecord {
        classification,
        set_type,
        key,
        source_side,
        source_start_seq: source_start.raw.seq,
        source_start_run_id: source_start.raw.run_id.clone(),
        source_end_seq: source_end.raw.seq,
        source_end_run_id: source_end.raw.run_id.clone(),
        peer_seq: peer_sample.raw.seq,
        peer_run_id: peer_sample.raw.run_id.clone(),
        source_value,
        peer_value,
        source_start_time: format_time(source_start.validation_time),
        peer_time: format_time(peer_sample.validation_time),
        source_end_time: format_time(source_end.validation_time),
        note: note.to_string(),
    }
}

impl SandwichAnalysis {
    pub(super) fn add(&mut self, class: &'static str, record: SandwichRecord, sample_limit: usize) {
        self.total_occurrences += 1;
        self.unique_keys.insert(sandwich_unique_key(&record));
        *self.by_set_type.entry(record.set_type).or_default() += 1;
        self.add_heatmap_record(class, &record);
        self.add_group_record(class, &record, sample_limit.min(20));
        let stats = self.stats.entry(class).or_default();
        stats.total += 1;
        stats.unique_keys.insert(sandwich_unique_key(&record));
        if stats.samples.len() < sample_limit {
            stats.samples.push(record);
        }
    }

    fn add_heatmap_record(&mut self, class: &'static str, record: &SandwichRecord) {
        let key = format!(
            "{}|{}|{}",
            record.source_start_run_id, record.peer_run_id, record.source_end_run_id
        );
        let window = format!(
            "{} - {} - {}",
            record.source_start_run_id, record.peer_run_id, record.source_end_run_id
        );
        let row = self
            .heatmap
            .entry(key)
            .or_insert_with(|| SandwichHeatmapRow {
                window,
                source_side: record.source_side,
                source_start_seq: record.source_start_seq,
                source_start_run_id: record.source_start_run_id.clone(),
                source_end_seq: record.source_end_seq,
                source_end_run_id: record.source_end_run_id.clone(),
                peer_seq: record.peer_seq,
                peer_run_id: record.peer_run_id.clone(),
                source_start_time: record.source_start_time.clone(),
                peer_time: record.peer_time.clone(),
                source_end_time: record.source_end_time.clone(),
                total: 0,
                class_counts: Default::default(),
            });
        row.total += 1;
        *row.class_counts.entry(class).or_default() += 1;
    }

    fn add_group_record(
        &mut self,
        class: &'static str,
        record: &SandwichRecord,
        group_sample_limit: usize,
    ) {
        match record.set_type {
            "object" | "reject_uri" => {
                let prefix = uri_host_prefix(&record.key);
                let group_key = format!("{}|{}", record.set_type, prefix);
                let group =
                    self.uri_prefix_groups
                        .entry(group_key)
                        .or_insert_with(|| SandwichGroupStats {
                            group_value: prefix,
                            set_type: record.set_type,
                            ..Default::default()
                        });
                add_to_group(group, class, record, group_sample_limit);
            }
            "vrp_output" | "vap_output" => {
                let asn = output_key_asn(&record.key);
                let group_key = format!("{}|{}", record.set_type, asn);
                let group =
                    self.asn_groups
                        .entry(group_key)
                        .or_insert_with(|| SandwichGroupStats {
                            group_value: asn,
                            set_type: record.set_type,
                            ..Default::default()
                        });
                add_to_group(group, class, record, group_sample_limit);
            }
            _ => {}
        }
    }
}

fn sandwich_unique_key(record: &SandwichRecord) -> String {
    format!(
        "{}|{}|{}|{}",
        record.classification,
        record.set_type,
        record.source_side.as_str(),
        record.key
    )
}

fn add_to_group(
    group: &mut SandwichGroupStats,
    class: &'static str,
    record: &SandwichRecord,
    group_sample_limit: usize,
) {
    group.total += 1;
    group.unique_keys.insert(sandwich_unique_key(record));
    *group.class_counts.entry(class).or_default() += 1;
    if group.sample_keys.len() < group_sample_limit
        && !group.sample_keys.iter().any(|item| item == &record.key)
    {
        group.sample_keys.push(record.key.clone());
    }
}

fn uri_host_prefix(uri: &str) -> String {
    let scheme_end = uri.find("://").map(|index| index + 3).unwrap_or(0);
    if let Some(path_start) = uri[scheme_end..].find('/') {
        let end = scheme_end + path_start + 1;
        return uri[..end].to_string();
    }
    uri.to_string()
}

fn output_key_asn(key: &str) -> String {
    key.split('|').next().unwrap_or(key).to_string()
}
