use crate::inspect::smp::structures::*;

pub(in crate::inspect::smp) fn derive_loaded_placed_structure_collection_from_probe(
    probe: &SmpSavePlacedStructureRecordTripletProbe,
) -> SmpLoadedPlacedStructureCollection {
    SmpLoadedPlacedStructureCollection {
        source_kind: probe.source_kind.clone(),
        semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(),
        observed_entry_count: probe.record_count,
        entries: probe
            .entries
            .iter()
            .map(|entry| SmpLoadedPlacedStructureEntry {
                record_index: entry.record_index,
                primary_name: entry.primary_name.clone(),
                secondary_name: entry.secondary_name.clone(),
                policy_trailing_word: entry.policy_trailing_word,
                policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(),
                profile_payload_dword: entry.profile_payload_dword,
                profile_payload_dword_hex: entry.profile_payload_dword_hex.clone(),
                profile_status_kind: entry.profile_status_kind.clone(),
                farm_growth_stage_index: entry.farm_growth_stage_index,
                profile_companion_byte_u8: entry.profile_companion_byte_u8,
                profile_companion_byte_hex: entry.profile_companion_byte_hex.clone(),
            })
            .collect(),
    }
}

pub(in crate::inspect::smp) fn parse_save_placed_structure_record_triplet_probe(
    bytes: &[u8],
    header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
) -> Option<SmpSavePlacedStructureRecordTripletProbe> {
    let header_probe = header_probe?;
    if header_probe.source_kind != "save-placed-structure-tagged-header-counts" {
        return None;
    }
    let records_payload =
        bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?;
    let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG);
    let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG);
    let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG);
    let record_count = header_probe.live_record_count as usize;
    if name_offsets.len() != record_count
        || policy_offsets.len() != record_count
        || profile_offsets.len() != record_count
    {
        return None;
    }
    let mut entries = Vec::with_capacity(record_count);
    for index in 0..record_count {
        let name_tag_relative_offset = name_offsets[index];
        let policy_tag_relative_offset = policy_offsets[index];
        let profile_tag_relative_offset = profile_offsets[index];
        let next_record_relative_offset = name_offsets
            .get(index + 1)
            .copied()
            .unwrap_or(records_payload.len());
        if !(name_tag_relative_offset < policy_tag_relative_offset
            && policy_tag_relative_offset < profile_tag_relative_offset
            && profile_tag_relative_offset < next_record_relative_offset)
        {
            return None;
        }
        let name_payload =
            records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?;
        let (primary_name, secondary_name) = parse_save_len_prefixed_ascii_name_pair(name_payload)?;
        let policy_chunk_len =
            profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?;
        if policy_chunk_len != 0x1a {
            return None;
        }
        let policy_payload =
            records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?;
        let policy_f32_lane_0 = f32::from_bits(read_u32_at(policy_payload, 0)?);
        let policy_f32_lane_1 = f32::from_bits(read_u32_at(policy_payload, 4)?);
        let policy_f32_lane_2 = f32::from_bits(read_u32_at(policy_payload, 8)?);
        let policy_f32_lane_3 = f32::from_bits(read_u32_at(policy_payload, 12)?);
        let policy_f32_lane_4 = f32::from_bits(read_u32_at(policy_payload, 16)?);
        let policy_reserved_dword = read_u32_at(policy_payload, 20)?;
        let policy_trailing_word = read_u16_at(policy_payload, 24)?;
        let profile_chunk_len =
            next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
        let profile_payload =
            records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?;
        let profile_open_marker = read_u32_at(profile_payload, 0)?;
        if profile_open_marker != 0x00005dc1 {
            return None;
        }
        let (profile_repeated_primary_name, profile_repeated_secondary_name) =
            parse_save_len_prefixed_ascii_name_pair(profile_payload.get(4..)?)?;
        let mut trailer_offset = 4usize;
        let repeated_primary_len = *profile_payload.get(trailer_offset)? as usize;
        trailer_offset += 1 + repeated_primary_len;
        while matches!(profile_payload.get(trailer_offset), Some(0)) {
            trailer_offset += 1;
        }
        let repeated_secondary_len = *profile_payload.get(trailer_offset)? as usize;
        trailer_offset += 1 + repeated_secondary_len;
        let mut matched_footer = None;
        for candidate_offset in [trailer_offset, trailer_offset + 1] {
            if let (
                Some(profile_payload_dword),
                Some(profile_sentinel_i32),
                Some(profile_close_marker),
            ) = (
                read_u32_at(profile_payload, candidate_offset),
                read_i32_at(profile_payload, candidate_offset + 4),
                read_u32_at(profile_payload, candidate_offset + 8),
            ) {
                if profile_close_marker == 0x00005dc2 {
                    matched_footer = Some((
                        candidate_offset,
                        profile_payload_dword,
                        profile_sentinel_i32,
                        profile_close_marker,
                    ));
                    break;
                }
            }
        }
        let (
            profile_footer_relative_offset,
            profile_payload_dword,
            profile_sentinel_i32,
            profile_close_marker,
        ) = matched_footer?;
        let profile_pre_footer_padding = profile_payload
            .get(trailer_offset..profile_footer_relative_offset)?
            .iter()
            .map(|byte| format!("0x{byte:02x}"))
            .collect::<Vec<_>>();
        let profile_companion_byte_u8 = if profile_pre_footer_padding.len() == 1 {
            profile_payload.get(trailer_offset).copied()
        } else {
            None
        };
        let (profile_status_kind, farm_growth_stage_index) =
            derive_save_placed_structure_profile_status(
                &primary_name,
                &secondary_name,
                profile_sentinel_i32,
            );
        entries.push(SmpSavePlacedStructureRecordTripletEntryProbe {
            record_index: index,
            primary_name,
            secondary_name,
            name_tag_relative_offset,
            policy_tag_relative_offset,
            profile_tag_relative_offset,
            policy_chunk_len,
            profile_chunk_len,
            policy_f32_lane_0,
            policy_f32_lane_1,
            policy_f32_lane_2,
            policy_f32_lane_3,
            policy_f32_lane_4,
            policy_reserved_dword,
            policy_trailing_word,
            policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"),
            profile_open_marker,
            profile_open_marker_hex: format!("0x{profile_open_marker:08x}"),
            profile_repeated_primary_name,
            profile_repeated_secondary_name,
            profile_footer_relative_offset,
            profile_footer_relative_offset_hex: format!("0x{profile_footer_relative_offset:x}"),
            profile_pre_footer_padding_len: profile_pre_footer_padding.len(),
            profile_pre_footer_padding_hex_bytes: profile_pre_footer_padding,
            profile_companion_byte_hex: profile_companion_byte_u8
                .map(|byte| format!("0x{byte:02x}")),
            profile_companion_byte_u8,
            profile_payload_dword,
            profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"),
            profile_sentinel_i32,
            profile_status_kind: profile_status_kind.to_string(),
            farm_growth_stage_index,
            profile_close_marker,
            profile_close_marker_hex: format!("0x{profile_close_marker:08x}"),
        });
    }
    let farm_growth_stage_entry_count = entries
        .iter()
        .filter(|entry| entry.farm_growth_stage_index.is_some())
        .count();
    Some(SmpSavePlacedStructureRecordTripletProbe {
        profile_family: header_probe.profile_family.clone(),
        source_kind: "save-placed-structure-record-triplets".to_string(),
        semantic_family: "scenario-save-placed-structure-record-triplets".to_string(),
        records_tag_offset: header_probe.records_tag_offset,
        close_tag_offset: header_probe.close_tag_offset,
        record_count,
        entries,
        evidence: vec![
            "save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(),
            "the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(),
            "each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(),
            format!(
                "the compact 0x55f3 footer status lane behaves like a farm growth-stage bucket on grounded saves: {farm_growth_stage_entry_count} entries expose nonnegative 0..11 values and all observed non-farm families stay at -1"
            ),
        ],
    })
}

pub(in crate::inspect::smp) fn derive_save_placed_structure_profile_status(
    primary_name: &str,
    secondary_name: &str,
    raw_status: i32,
) -> (&'static str, Option<u8>) {
    let looks_like_farm = primary_name.starts_with("Farm") || secondary_name.contains("Farm");
    if raw_status == -1 {
        return ("unset", None);
    }
    if looks_like_farm && (0..=11).contains(&raw_status) {
        return ("farm_growth_stage_bucket", Some(raw_status as u8));
    }
    ("opaque_nondefault", None)
}

pub(in crate::inspect::smp) fn parse_save_placed_structure_collection_header_probe(
    bytes: &[u8],
    file_extension_hint: Option<&str>,
    container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
    parse_save_tagged_collection_header_probe(
        bytes,
        file_extension_hint,
        container_profile,
        0x000036b1,
        0x000036b2,
        0x000036b3,
        "save-placed-structure-tagged-header-counts",
        "scenario-save-placed-structure-header-counts",
        |header| {
            header.direct_collection_flag == 0
                && header.direct_record_stride >= 1
                && header.direct_record_stride <= 0x20
                && header.live_id_bound >= 0x100
                && header.live_record_count >= 0x100
                && header.live_record_count <= header.live_id_bound
        },
        vec![
            "save-side placed-structure collection uses tagged family 0x36b1/0x36b2/0x36b3 beneath the wider local-runtime and route-entry rebuild owners".to_string(),
            "current evidence only grounds header-level placed-structure collection counts here; direct record-body reconstruction still needs the later per-entry load/save slot study.".to_string(),
        ],
    )
}
