use crate::inspect::smp::profiles::*;

pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE: usize = 0x41;
pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT: usize = 8;
pub(in crate::inspect::smp) const RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN: usize = 0x4000;

pub(in crate::inspect::smp) fn parse_rt3_105_save_named_locomotive_availability_probe(
    bytes: &[u8],
    file_extension_hint: Option<&str>,
    container_profile: Option<&SmpContainerProfile>,
    packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>,
) -> Option<SmpRt3105SaveNamedLocomotiveAvailabilityProbe> {
    let packed_profile_probe = packed_profile_probe?;
    let extension = file_extension_hint.unwrap_or("");
    let profile_family = container_profile
        .map(|profile| profile.profile_family.clone())
        .unwrap_or_else(|| packed_profile_probe.profile_family.clone());
    if !matches!(extension, "gms" | "gmx") || !profile_family.contains("save-container") {
        return None;
    }

    let search_start = packed_profile_probe
        .packed_profile_offset
        .checked_add(packed_profile_probe.packed_profile_len)?;
    let search_end = search_start
        .checked_add(RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN)
        .map(|end| end.min(bytes.len()))
        .unwrap_or(bytes.len());
    if search_end <= search_start + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE {
        return None;
    }

    let mut best_start = None;
    let mut best_entries = Vec::new();
    for candidate_start in search_start..search_end {
        let entries = parse_direct_named_locomotive_entries(bytes, candidate_start, search_end);
        if entries.len() > best_entries.len() {
            best_entries = entries;
            best_start = Some(candidate_start);
        }
    }

    if best_entries.len() < RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT {
        return None;
    }

    let entries_offset = best_start?;
    let entries_end_offset = entries_offset
        .checked_add(best_entries.len() * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE)?;
    let zero_availability_names = best_entries
        .iter()
        .filter(|entry| entry.availability_dword == 0)
        .map(|entry| entry.text.clone())
        .collect::<Vec<_>>();
    let zero_availability_count = zero_availability_names.len();
    let source_kind = match extension {
        "gms" => "save-direct-locomotive-row-run",
        "gmx" => "sandbox-direct-locomotive-row-run",
        _ => "direct-locomotive-row-run",
    }
    .to_string();

    let observed_entry_count = best_entries.len();

    Some(SmpRt3105SaveNamedLocomotiveAvailabilityProbe {
        profile_family,
        source_kind,
        semantic_family: "scenario-named-locomotive-availability-table".to_string(),
        semantic_alignment: vec![
            "Matches the grounded `.smp` save-side locomotive-name-plus-dword row family restored into scenario state [world+0x66b6].".to_string(),
            "Entry layout is one availability dword at +0x00 followed by one fixed-width locomotive name buffer at +0x04..+0x40.".to_string(),
            "The recovered row order is treated conservatively as the live locomotive ordinal order later used by locomotives-page descriptor lowering.".to_string(),
        ],
        entries_offset,
        entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE,
        entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE),
        observed_entry_count,
        zero_availability_count,
        zero_availability_names,
        entries_end_offset,
        entries: best_entries,
        evidence: vec![
            format!("search span 0x{search_start:08x}..0x{search_end:08x}"),
            format!("entries offset 0x{entries_offset:08x}"),
            format!(
                "entry stride 0x{:x}",
                RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE
            ),
            format!("observed entry count {observed_entry_count}"),
        ],
    })
}

pub(in crate::inspect::smp) fn parse_direct_named_locomotive_entries(
    bytes: &[u8],
    start_offset: usize,
    search_end: usize,
) -> Vec<SmpRt3105SaveNameTableEntry> {
    let mut entries = Vec::new();
    let mut offset = start_offset;
    while offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE <= bytes.len() && offset < search_end
    {
        let record = &bytes[offset..offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE];
        let Some(nul_index) = record[4..].iter().position(|byte| *byte == 0) else {
            break;
        };
        let name_bytes = &record[4..4 + nul_index];
        if name_bytes.is_empty() {
            break;
        }
        let Ok(text) = std::str::from_utf8(name_bytes) else {
            break;
        };
        if !is_probable_named_locomotive_label(text) {
            break;
        }
        if record[4 + nul_index + 1..].iter().any(|byte| *byte != 0) {
            break;
        }

        let availability_dword = u32::from_le_bytes([record[0], record[1], record[2], record[3]]);
        entries.push(SmpRt3105SaveNameTableEntry {
            index: entries.len(),
            offset,
            text: text.to_string(),
            availability_dword,
            availability_dword_hex: format!("0x{availability_dword:08x}"),
            trailer_word: availability_dword,
            trailer_word_hex: format!("0x{availability_dword:08x}"),
        });
        offset += RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE;
    }
    entries
}

pub(in crate::inspect::smp) fn is_probable_named_locomotive_label(text: &str) -> bool {
    if text.is_empty() || text.len() > 40 {
        return false;
    }
    text.bytes().all(|byte| {
        byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'/' | b'(' | b')' | b'.')
    })
}
