use super::common::{
    POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
    POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT,
    SPECIAL_CONDITIONS_OFFSET, collect_special_conditions_input_paths, parse_hex_offset,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;

use serde::Serialize;

#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanSample {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) nonzero_relative_offsets: Vec<usize>,
    pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarOffsetSummary {
    pub(crate) relative_offset_hex: String,
    pub(crate) file_count_present: usize,
    pub(crate) distinct_value_count: usize,
    pub(crate) sample_value_hexes: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarFamilySummary {
    pub(crate) profile_family: String,
    pub(crate) source_kinds: Vec<String>,
    pub(crate) file_count: usize,
    pub(crate) files_with_any_nonzero_count: usize,
    pub(crate) distinct_nonzero_offset_set_count: usize,
    pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
    pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
    pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsScalarOffsetSummary>,
    pub(crate) sample_paths: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanReport {
    pub(crate) root_path: String,
    pub(crate) file_count: usize,
    pub(crate) files_with_probe_count: usize,
    pub(crate) files_with_any_nonzero_count: usize,
    pub(crate) skipped_file_count: usize,
    pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsScalarFamilySummary>,
}

#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsTailScanSample {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) nonzero_relative_offsets: Vec<usize>,
    pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailOffsetSummary {
    pub(crate) relative_offset_hex: String,
    pub(crate) file_count_present: usize,
    pub(crate) distinct_value_count: usize,
    pub(crate) sample_value_hexes: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailFamilySummary {
    pub(crate) profile_family: String,
    pub(crate) source_kinds: Vec<String>,
    pub(crate) file_count: usize,
    pub(crate) files_with_any_nonzero_count: usize,
    pub(crate) distinct_nonzero_offset_set_count: usize,
    pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
    pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
    pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsTailOffsetSummary>,
    pub(crate) sample_paths: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailScanReport {
    pub(crate) root_path: String,
    pub(crate) file_count: usize,
    pub(crate) files_with_probe_count: usize,
    pub(crate) files_with_any_nonzero_count: usize,
    pub(crate) skipped_file_count: usize,
    pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsTailFamilySummary>,
}

pub(crate) fn scan_post_special_conditions_scalars(
    root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut candidate_paths = Vec::new();
    collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;

    let file_count = candidate_paths.len();
    let mut samples = Vec::new();
    let mut skipped_file_count = 0usize;
    for path in candidate_paths {
        match load_post_special_conditions_scalar_scan_sample(&path) {
            Ok(sample) => samples.push(sample),
            Err(_) => skipped_file_count += 1,
        }
    }

    let files_with_probe_count = samples.len();
    let files_with_any_nonzero_count = samples
        .iter()
        .filter(|sample| !sample.nonzero_relative_offsets.is_empty())
        .count();

    let family_summaries = build_scalar_family_summaries(samples);

    let report = RuntimePostSpecialConditionsScalarScanReport {
        root_path: root_path.display().to_string(),
        file_count,
        files_with_probe_count,
        files_with_any_nonzero_count,
        skipped_file_count,
        family_summaries,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn scan_post_special_conditions_tail(
    root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut candidate_paths = Vec::new();
    collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;

    let file_count = candidate_paths.len();
    let mut samples = Vec::new();
    let mut skipped_file_count = 0usize;
    for path in candidate_paths {
        match load_post_special_conditions_tail_scan_sample(&path) {
            Ok(sample) => samples.push(sample),
            Err(_) => skipped_file_count += 1,
        }
    }

    let files_with_probe_count = samples.len();
    let files_with_any_nonzero_count = samples
        .iter()
        .filter(|sample| !sample.nonzero_relative_offsets.is_empty())
        .count();

    let family_summaries = build_tail_family_summaries(samples);

    let report = RuntimePostSpecialConditionsTailScanReport {
        root_path: root_path.display().to_string(),
        file_count,
        files_with_probe_count,
        files_with_any_nonzero_count,
        skipped_file_count,
        family_summaries,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn load_post_special_conditions_scalar_scan_sample(
    smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsScalarScanSample, Box<dyn std::error::Error>> {
    let bytes = fs::read(smp_path)?;
    let table_end = SPECIAL_CONDITIONS_OFFSET + 36 * 4;
    if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
        return Err(format!(
            "{} is too small for the fixed post-special-conditions scalar window",
            smp_path.display()
        )
        .into());
    }

    let hidden_sentinel = read_u32_le(
        &bytes,
        SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
    )
    .ok_or_else(|| {
        format!(
            "{} is missing the hidden special-condition sentinel",
            smp_path.display()
        )
    })?;
    if hidden_sentinel != 1 {
        return Err(format!(
            "{} does not match the fixed special-conditions table sentinel",
            smp_path.display()
        )
        .into());
    }

    let extension = smp_path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_ascii_lowercase())
        .unwrap_or_default();
    let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
    let source_kind = match extension.as_str() {
        "gmp" => "map-post-special-conditions-window",
        "gms" => "save-post-special-conditions-window",
        "gmx" => "sandbox-post-special-conditions-window",
        _ => "post-special-conditions-window",
    }
    .to_string();

    let mut nonzero_relative_offsets = Vec::new();
    let mut values_by_relative_offset_hex = BTreeMap::new();
    for offset in (POST_SPECIAL_CONDITIONS_SCALAR_OFFSET..POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET)
        .step_by(4)
    {
        let value = read_u32_le(&bytes, offset).ok_or_else(|| {
            format!(
                "{} is truncated inside the fixed post-special-conditions scalar window",
                smp_path.display()
            )
        })?;
        if value == 0 {
            continue;
        }
        let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET;
        nonzero_relative_offsets.push(relative_offset);
        values_by_relative_offset_hex
            .insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
    }

    Ok(RuntimePostSpecialConditionsScalarScanSample {
        path: smp_path.display().to_string(),
        profile_family,
        source_kind,
        nonzero_relative_offsets,
        values_by_relative_offset_hex,
    })
}

pub(crate) fn load_post_special_conditions_tail_scan_sample(
    smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsTailScanSample, Box<dyn std::error::Error>> {
    let bytes = fs::read(smp_path)?;
    if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET {
        return Err(format!(
            "{} is too small for the fixed post-special-conditions tail window",
            smp_path.display()
        )
        .into());
    }

    let extension = smp_path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_ascii_lowercase())
        .unwrap_or_default();
    let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
    let source_kind = match extension.as_str() {
        "gmp" => "map-post-special-conditions-tail",
        "gms" => "save-post-special-conditions-tail",
        "gmx" => "sandbox-post-special-conditions-tail",
        _ => "post-special-conditions-tail",
    }
    .to_string();

    let mut nonzero_relative_offsets = Vec::new();
    let mut values_by_relative_offset_hex = BTreeMap::new();
    for offset in (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET..bytes.len()).step_by(4) {
        let Some(value) = read_u32_le(&bytes, offset) else {
            break;
        };
        if value == 0 {
            continue;
        }
        let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET;
        nonzero_relative_offsets.push(relative_offset);
        values_by_relative_offset_hex
            .insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
    }

    Ok(RuntimePostSpecialConditionsTailScanSample {
        path: smp_path.display().to_string(),
        profile_family,
        source_kind,
        nonzero_relative_offsets,
        values_by_relative_offset_hex,
    })
}

fn build_scalar_family_summaries(
    samples: Vec<RuntimePostSpecialConditionsScalarScanSample>,
) -> Vec<RuntimePostSpecialConditionsScalarFamilySummary> {
    let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsScalarScanSample>>::new();
    for sample in samples {
        grouped
            .entry(sample.profile_family.clone())
            .or_default()
            .push(sample);
    }
    grouped
        .into_iter()
        .map(
            |(profile_family, samples)| RuntimePostSpecialConditionsScalarFamilySummary {
                profile_family,
                source_kinds: samples
                    .iter()
                    .map(|sample| sample.source_kind.clone())
                    .collect::<BTreeSet<_>>()
                    .into_iter()
                    .collect(),
                file_count: samples.len(),
                files_with_any_nonzero_count: samples
                    .iter()
                    .filter(|sample| !sample.nonzero_relative_offsets.is_empty())
                    .count(),
                distinct_nonzero_offset_set_count: samples
                    .iter()
                    .map(|sample| sample.nonzero_relative_offsets.clone())
                    .collect::<BTreeSet<_>>()
                    .len(),
                stable_nonzero_relative_offset_hexes: stable_offsets(
                    samples
                        .iter()
                        .map(|sample| sample.nonzero_relative_offsets.as_slice()),
                )
                .into_iter()
                .map(|offset| format!("0x{offset:x}"))
                .collect(),
                union_nonzero_relative_offset_hexes: collect_offset_values(
                    samples
                        .iter()
                        .map(|sample| &sample.values_by_relative_offset_hex),
                )
                .0
                .keys()
                .copied()
                .map(|offset| format!("0x{offset:x}"))
                .collect(),
                offset_summaries: build_offset_summaries(collect_offset_values(
                    samples
                        .iter()
                        .map(|sample| &sample.values_by_relative_offset_hex),
                )),
                sample_paths: samples
                    .iter()
                    .take(12)
                    .map(|sample| sample.path.clone())
                    .collect(),
            },
        )
        .collect()
}

fn build_tail_family_summaries(
    samples: Vec<RuntimePostSpecialConditionsTailScanSample>,
) -> Vec<RuntimePostSpecialConditionsTailFamilySummary> {
    let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsTailScanSample>>::new();
    for sample in samples {
        grouped
            .entry(sample.profile_family.clone())
            .or_default()
            .push(sample);
    }
    grouped
        .into_iter()
        .map(
            |(profile_family, samples)| RuntimePostSpecialConditionsTailFamilySummary {
                profile_family,
                source_kinds: samples
                    .iter()
                    .map(|sample| sample.source_kind.clone())
                    .collect::<BTreeSet<_>>()
                    .into_iter()
                    .collect(),
                file_count: samples.len(),
                files_with_any_nonzero_count: samples
                    .iter()
                    .filter(|sample| !sample.nonzero_relative_offsets.is_empty())
                    .count(),
                distinct_nonzero_offset_set_count: samples
                    .iter()
                    .map(|sample| sample.nonzero_relative_offsets.clone())
                    .collect::<BTreeSet<_>>()
                    .len(),
                stable_nonzero_relative_offset_hexes: stable_offsets(
                    samples
                        .iter()
                        .map(|sample| sample.nonzero_relative_offsets.as_slice()),
                )
                .into_iter()
                .map(|offset| format!("0x{offset:x}"))
                .collect(),
                union_nonzero_relative_offset_hexes: collect_offset_values(
                    samples
                        .iter()
                        .map(|sample| &sample.values_by_relative_offset_hex),
                )
                .0
                .keys()
                .copied()
                .map(|offset| format!("0x{offset:x}"))
                .collect(),
                offset_summaries: build_tail_offset_summaries(collect_offset_values(
                    samples
                        .iter()
                        .map(|sample| &sample.values_by_relative_offset_hex),
                )),
                sample_paths: samples
                    .iter()
                    .take(12)
                    .map(|sample| sample.path.clone())
                    .collect(),
            },
        )
        .collect()
}

fn stable_offsets<'a>(offset_sets: impl Iterator<Item = &'a [usize]>) -> BTreeSet<usize> {
    let mut offset_sets = offset_sets.peekable();
    if offset_sets.peek().is_none() {
        return BTreeSet::new();
    }

    let mut stable = offset_sets
        .next()
        .map(|set| set.iter().copied().collect::<BTreeSet<_>>())
        .unwrap_or_default();
    for offsets in offset_sets {
        let current = offsets.iter().copied().collect::<BTreeSet<_>>();
        stable = stable.intersection(&current).copied().collect();
    }
    stable
}

fn collect_offset_values<'a>(
    maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>) {
    let mut offset_values = BTreeMap::<usize, BTreeSet<String>>::new();
    let mut offset_counts = BTreeMap::<usize, usize>::new();
    for map in maps {
        for (offset_hex, value_hex) in map {
            if let Some(offset) = parse_hex_offset(offset_hex) {
                *offset_counts.entry(offset).or_default() += 1;
                offset_values
                    .entry(offset)
                    .or_default()
                    .insert(value_hex.clone());
            }
        }
    }
    (offset_values, offset_counts)
}

fn build_offset_summaries(
    (offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsScalarOffsetSummary> {
    offset_counts
        .into_iter()
        .map(
            |(offset, count)| RuntimePostSpecialConditionsScalarOffsetSummary {
                relative_offset_hex: format!("0x{offset:x}"),
                file_count_present: count,
                distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
                sample_value_hexes: offset_values
                    .get(&offset)
                    .map(|values| values.iter().take(8).cloned().collect())
                    .unwrap_or_default(),
            },
        )
        .collect()
}

fn build_tail_offset_summaries(
    (offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsTailOffsetSummary> {
    offset_counts
        .into_iter()
        .map(
            |(offset, count)| RuntimePostSpecialConditionsTailOffsetSummary {
                relative_offset_hex: format!("0x{offset:x}"),
                file_count_present: count,
                distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
                sample_value_hexes: offset_values
                    .get(&offset)
                    .map(|values| values.iter().take(8).cloned().collect())
                    .unwrap_or_default(),
            },
        )
        .collect()
}
