use crate::inspect::smp::common::*;
use crate::inspect::smp::*;

pub(in crate::inspect::smp) fn parse_save_tagged_collection_header_probe(
    bytes: &[u8],
    file_extension_hint: Option<&str>,
    container_profile: Option<&SmpContainerProfile>,
    metadata_tag: u32,
    records_tag: u32,
    close_tag: u32,
    source_kind: &str,
    semantic_family: &str,
    predicate: impl Fn(IndexedCollectionHeaderSummary) -> bool,
    mut evidence: Vec<String>,
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
    if file_extension_hint != Some("gms") {
        return None;
    }
    let profile = container_profile?;
    if !matches!(
        profile.profile_family.as_str(),
        "rt3-classic-save-container-v1"
            | "rt3-105-save-container-v1"
            | "rt3-105-scenario-save-container-v1"
            | "rt3-105-alt-save-container-v1"
    ) {
        return None;
    }

    let metadata_offsets = find_u32_le_offsets(bytes, metadata_tag);
    let records_offsets = find_u32_le_offsets(bytes, records_tag);
    let close_offsets = find_u32_le_offsets(bytes, close_tag);

    let summary = metadata_offsets
        .into_iter()
        .filter_map(|metadata_tag_offset| {
            let records_tag_offset = records_offsets
                .iter()
                .copied()
                .find(|offset| *offset > metadata_tag_offset)?;
            let close_tag_offset = close_offsets
                .iter()
                .copied()
                .find(|offset| *offset > records_tag_offset)?;
            let payload = bytes.get(metadata_tag_offset + 4..records_tag_offset)?;
            if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
                return None;
            }

            let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
                .map(|index| read_u32_at(payload, index * 4))
                .collect::<Option<Vec<_>>>()?;
            let header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT] =
                header_words.try_into().ok()?;
            let summary = IndexedCollectionHeaderSummary {
                metadata_tag_offset,
                records_tag_offset,
                close_tag_offset,
                direct_collection_flag: header_words[0],
                direct_record_stride: header_words[1],
                live_id_bound: header_words[4],
                live_record_count: header_words[5],
                header_words,
            };
            predicate(summary).then_some(summary)
        })
        .next()?;

    evidence.push(format!(
        "exact little-endian u32 tag family 0x{metadata_tag:04x}/0x{records_tag:04x}/0x{close_tag:04x} appears at file offsets 0x{:x}/0x{:x}/0x{:x}",
        summary.metadata_tag_offset, summary.records_tag_offset, summary.close_tag_offset
    ));
    evidence.push(format!(
        "header words report direct_collection_flag={}, direct_record_stride=0x{:x}, live_id_bound={}, live_record_count={}",
        summary.direct_collection_flag,
        summary.direct_record_stride,
        summary.live_id_bound,
        summary.live_record_count
    ));

    Some(SmpSaveTaggedCollectionHeaderProbe {
        profile_family: profile.profile_family.clone(),
        source_kind: source_kind.to_string(),
        semantic_family: semantic_family.to_string(),
        metadata_tag_offset: summary.metadata_tag_offset,
        records_tag_offset: summary.records_tag_offset,
        close_tag_offset: summary.close_tag_offset,
        direct_collection_flag: summary.direct_collection_flag,
        direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag),
        direct_record_stride: summary.direct_record_stride,
        direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride),
        live_id_bound: summary.live_id_bound,
        live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound),
        live_record_count: summary.live_record_count,
        live_record_count_hex: format!("0x{:08x}", summary.live_record_count),
        header_words: summary.header_words.to_vec(),
        header_hex_words: summary
            .header_words
            .iter()
            .map(|word| format!("0x{word:08x}"))
            .collect(),
        evidence,
    })
}

pub(in crate::inspect::smp) fn scan_save_unclassified_tagged_collection_header_probes(
    bytes: &[u8],
    file_extension_hint: Option<&str>,
    container_profile: Option<&SmpContainerProfile>,
) -> Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe> {
    if file_extension_hint != Some("gms") {
        return Vec::new();
    }
    let Some(profile) = container_profile else {
        return Vec::new();
    };
    if !matches!(
        profile.profile_family.as_str(),
        "rt3-classic-save-container-v1"
            | "rt3-105-save-container-v1"
            | "rt3-105-scenario-save-container-v1"
            | "rt3-105-alt-save-container-v1"
    ) {
        return Vec::new();
    }
    let known_metadata_tags = BTreeSet::from([
        RT3_SAVE_WORLD_BLOCK_CHUNK_TAG,
        0x000061a9,
        0x00005209,
        0x000036b1,
        EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32,
    ]);
    let mut low_tag_offsets: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
    for offset in 0..bytes.len().saturating_sub(4) {
        let Some(tag) = read_u32_at(bytes, offset) else {
            continue;
        };
        if (3..=0xffff).contains(&tag) {
            low_tag_offsets.entry(tag).or_default().push(offset);
        }
    }
    let mut probes = Vec::new();
    for (&metadata_tag, metadata_offsets) in &low_tag_offsets {
        if known_metadata_tags.contains(&metadata_tag) {
            continue;
        }
        let Some(records_offsets) = low_tag_offsets.get(&(metadata_tag + 1)) else {
            continue;
        };
        let Some(close_offsets) = low_tag_offsets.get(&(metadata_tag + 2)) else {
            continue;
        };
        let records_tag = metadata_tag + 1;
        let close_tag = metadata_tag + 2;
        for &metadata_tag_offset in metadata_offsets {
            let mut header_words = [0u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT];
            let mut valid_header = true;
            for (index, word) in header_words.iter_mut().enumerate() {
                let Some(value) = read_u32_at(bytes, metadata_tag_offset + 4 + index * 4) else {
                    valid_header = false;
                    break;
                };
                *word = value;
            }
            if !valid_header {
                continue;
            }
            let summary = IndexedCollectionHeaderSummary {
                metadata_tag_offset,
                records_tag_offset: 0,
                close_tag_offset: 0,
                direct_collection_flag: header_words[0],
                direct_record_stride: header_words[1],
                live_id_bound: header_words[4],
                live_record_count: header_words[5],
                header_words,
            };
            if !matches!(summary.direct_collection_flag, 0 | 1)
                || summary.direct_record_stride == 0
                || summary.direct_record_stride > 0x2000
                || summary.live_id_bound == 0
                || summary.live_record_count == 0
                || summary.live_record_count > summary.live_id_bound
                || summary.live_id_bound > 0x1000
                || summary.live_record_count > 0x1000
            {
                continue;
            }
            let records_search_start = metadata_tag_offset + 4;
            let records_index =
                records_offsets.partition_point(|offset| *offset < records_search_start);
            let Some(&records_tag_offset) = records_offsets.get(records_index) else {
                continue;
            };
            let close_search_start = records_tag_offset + 4;
            let close_index = close_offsets.partition_point(|offset| *offset < close_search_start);
            let Some(&close_tag_offset) = close_offsets.get(close_index) else {
                continue;
            };
            let records_span_len = close_tag_offset.saturating_sub(records_tag_offset + 4);
            if records_span_len == 0 || records_span_len < summary.live_record_count as usize {
                continue;
            }
            if probes
                .iter()
                .any(|probe: &SmpSaveUnclassifiedTaggedCollectionHeaderProbe| {
                    probe.metadata_tag_offset == metadata_tag_offset
                        && probe.records_tag_offset == records_tag_offset
                        && probe.close_tag_offset == close_tag_offset
                })
            {
                continue;
            }
            probes.push(SmpSaveUnclassifiedTaggedCollectionHeaderProbe {
                profile_family: profile.profile_family.clone(),
                source_kind: "save-unclassified-tagged-header-counts".to_string(),
                semantic_family: "scenario-save-unclassified-tagged-header-counts".to_string(),
                metadata_tag,
                metadata_tag_hex: format!("0x{metadata_tag:08x}"),
                records_tag,
                records_tag_hex: format!("0x{records_tag:08x}"),
                close_tag,
                close_tag_hex: format!("0x{close_tag:08x}"),
                metadata_tag_offset,
                records_tag_offset,
                close_tag_offset,
                records_span_len,
                direct_collection_flag: summary.direct_collection_flag,
                direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag),
                direct_record_stride: summary.direct_record_stride,
                direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride),
                live_id_bound: summary.live_id_bound,
                live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound),
                live_record_count: summary.live_record_count,
                live_record_count_hex: format!("0x{:08x}", summary.live_record_count),
                header_words: summary.header_words.to_vec(),
                header_hex_words: summary
                    .header_words
                    .iter()
                    .map(|word| format!("0x{word:08x}"))
                    .collect(),
                evidence: vec![
                    "generic save-side tagged collection scan over plausible low u32 metadata tags not yet claimed by the checked-in collection probes".to_string(),
                    "candidate uses adjacent metadata/records/close tags with a header that matches the grounded indexed-collection shape (flag, stride, live_id_bound, live_record_count)".to_string(),
                ],
            });
        }
    }
    probes.sort_by(|left, right| {
        right
            .live_record_count
            .cmp(&left.live_record_count)
            .then_with(|| left.metadata_tag.cmp(&right.metadata_tag))
            .then_with(|| left.metadata_tag_offset.cmp(&right.metadata_tag_offset))
    });
    probes.truncate(32);
    probes
}

pub(in crate::inspect::smp) fn filter_unclassified_tagged_collection_header_probes_outside_known_spans(
    probes: Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe>,
    known_header_probes: &[Option<&SmpSaveTaggedCollectionHeaderProbe>],
) -> Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe> {
    probes
        .into_iter()
        .filter(|probe| {
            !known_header_probes.iter().flatten().any(|known| {
                probe.metadata_tag_offset >= known.metadata_tag_offset
                    && probe.close_tag_offset <= known.close_tag_offset
            })
        })
        .collect()
}
