use std::fs;
use std::path::Path;

use serde::{Deserialize, Serialize};

const WIN_COMMON_HEADER_LEN: usize = 0x50;
const WIN_INLINE_RESOURCE_OFFSET: usize = 0x50;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinHeaderWord {
    pub offset: usize,
    pub offset_hex: String,
    pub value: u32,
    pub value_hex: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinResourceReference {
    pub offset: usize,
    pub offset_hex: String,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinReferenceDeltaFrequency {
    pub delta: usize,
    pub delta_hex: String,
    pub count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinResourceRecordSample {
    pub offset: usize,
    pub offset_hex: String,
    pub name: String,
    pub delta_from_previous: Option<usize>,
    pub delta_from_previous_hex: Option<String>,
    pub prelude_words: Vec<WinHeaderWord>,
    pub post_name_word_0: u32,
    pub post_name_word_0_hex: String,
    pub post_name_word_0_high_u16: u16,
    pub post_name_word_0_high_u16_hex: String,
    pub post_name_word_0_low_u16: u16,
    pub post_name_word_0_low_u16_hex: String,
    pub post_name_word_1: u32,
    pub post_name_word_1_hex: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinResourceSelectorRecord {
    pub offset: usize,
    pub offset_hex: String,
    pub name: String,
    pub post_name_word_0: u32,
    pub post_name_word_0_hex: String,
    pub selector_high_u16: u16,
    pub selector_high_u16_hex: String,
    pub selector_low_u16: u16,
    pub selector_low_u16_hex: String,
    pub post_name_word_1: u32,
    pub post_name_word_1_hex: String,
    pub post_name_word_1_high_u16: u16,
    pub post_name_word_1_high_u16_hex: String,
    pub post_name_word_1_middle_u16: u16,
    pub post_name_word_1_middle_u16_hex: String,
    pub post_name_word_1_low_u16: u16,
    pub post_name_word_1_low_u16_hex: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinAnonymousSelectorRecord {
    pub record_offset: usize,
    pub record_offset_hex: String,
    pub preceding_named_record_name: Option<String>,
    pub preceding_named_record_offset_hex: Option<String>,
    pub following_named_record_name: Option<String>,
    pub following_named_record_offset_hex: Option<String>,
    pub selector_word_0: u32,
    pub selector_word_0_hex: String,
    pub selector_word_0_high_u16: u16,
    pub selector_word_0_high_u16_hex: String,
    pub selector_word_0_low_u16: u16,
    pub selector_word_0_low_u16_hex: String,
    pub selector_word_1: u32,
    pub selector_word_1_hex: String,
    pub selector_word_1_middle_u16: u16,
    pub selector_word_1_middle_u16_hex: String,
    pub body_word_0: u32,
    pub body_word_0_hex: String,
    pub body_word_1: u32,
    pub body_word_1_hex: String,
    pub body_word_2: u32,
    pub body_word_2_hex: String,
    pub body_word_3: u32,
    pub body_word_3_hex: String,
    pub footer_word_0: u32,
    pub footer_word_0_hex: String,
    pub footer_word_1: u32,
    pub footer_word_1_hex: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WinInspectionReport {
    pub file_size: usize,
    pub common_header_len: usize,
    pub common_header_len_hex: String,
    pub shared_header_words: Vec<WinHeaderWord>,
    pub matches_observed_common_signature: bool,
    pub common_resource_record_prelude_prefix_words: Option<Vec<String>>,
    pub name_len_matches_prelude_word_3_plus_nul_count: usize,
    pub inline_root_resource_name: Option<String>,
    pub inline_root_resource_offset: Option<usize>,
    pub inline_root_resource_offset_hex: Option<String>,
    pub imb_reference_count: usize,
    pub unique_imb_reference_count: usize,
    pub unique_imb_references: Vec<String>,
    pub dominant_reference_deltas: Vec<WinReferenceDeltaFrequency>,
    pub resource_selector_records: Vec<WinResourceSelectorRecord>,
    pub anonymous_selector_records: Vec<WinAnonymousSelectorRecord>,
    pub first_resource_record_samples: Vec<WinResourceRecordSample>,
    pub first_imb_references: Vec<WinResourceReference>,
    pub notes: Vec<String>,
}

pub fn inspect_win_file(path: &Path) -> Result<WinInspectionReport, Box<dyn std::error::Error>> {
    let bytes = fs::read(path)?;
    inspect_win_bytes(&bytes)
}

pub fn inspect_win_bytes(bytes: &[u8]) -> Result<WinInspectionReport, Box<dyn std::error::Error>> {
    if bytes.len() < WIN_COMMON_HEADER_LEN {
        return Err(format!(
            "window resource is too short for the observed common header: {} < 0x{WIN_COMMON_HEADER_LEN:x}",
            bytes.len()
        )
        .into());
    }

    let header_offsets = [
        0x00usize, 0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34,
        0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c,
    ];
    let shared_header_words = header_offsets
        .iter()
        .map(|offset| {
            let value = read_u32_le(bytes, *offset).expect("validated common header length");
            WinHeaderWord {
                offset: *offset,
                offset_hex: format!("0x{offset:02x}"),
                value,
                value_hex: format!("0x{value:08x}"),
            }
        })
        .collect::<Vec<_>>();

    let matches_observed_common_signature = read_u32_le(bytes, 0x00) == Some(0x0000_07d0)
        && read_u32_le(bytes, 0x04) == Some(0)
        && read_u32_le(bytes, 0x08) == Some(0)
        && read_u32_le(bytes, 0x0c) == Some(0x8000_0000)
        && read_u32_le(bytes, 0x10) == Some(0x8000_003f)
        && read_u32_le(bytes, 0x14) == Some(0x0000_003f)
        && read_u32_le(bytes, 0x34) == Some(0x0007_d100)
        && read_u32_le(bytes, 0x38) == Some(0x0007_d200)
        && read_u32_le(bytes, 0x40) == Some(0x000b_b800)
        && read_u32_le(bytes, 0x48) == Some(0x000b_b900);

    let inline_root_resource_name = parse_inline_ascii_name(bytes, WIN_INLINE_RESOURCE_OFFSET);
    let inline_root_resource_offset = inline_root_resource_name
        .as_ref()
        .map(|_| WIN_INLINE_RESOURCE_OFFSET + 1);
    let inline_root_resource_offset_hex =
        inline_root_resource_offset.map(|offset| format!("0x{offset:04x}"));

    let all_imb_references = collect_imb_references(bytes);
    let resource_record_samples = build_resource_record_samples(bytes, &all_imb_references);
    let resource_selector_records = build_resource_selector_records(&resource_record_samples);
    let anonymous_selector_records = collect_anonymous_selector_records(bytes, &all_imb_references);
    let common_resource_record_prelude_prefix_words =
        shared_prelude_prefix_hex(&resource_record_samples);
    let name_len_matches_prelude_word_3_plus_nul_count = resource_record_samples
        .iter()
        .filter(|sample| {
            sample.prelude_words.len() == 4
                && sample.prelude_words[3].value == (sample.name.len() as u32 + 1)
        })
        .count();
    let mut unique_imb_references = Vec::new();
    for reference in &all_imb_references {
        if !unique_imb_references.contains(&reference.name) {
            unique_imb_references.push(reference.name.clone());
        }
    }

    let mut notes = Vec::new();
    if matches_observed_common_signature {
        notes.push(
            "Header matches the observed shared .win signature seen in Campaign.win, CompanyDetail.win, and setup.win."
                .to_string(),
        );
    } else {
        notes.push(
            "Header diverges from the currently observed shared .win signature; treat field meanings as provisional."
                .to_string(),
        );
    }
    if inline_root_resource_name.is_some() {
        notes.push(
            "The blob carries an inline root .imb resource name immediately after the common 0x50-byte header."
                .to_string(),
        );
    } else {
        notes.push(
            "No inline root .imb resource name appears at 0x50; this window likely starts directly with control records."
                .to_string(),
        );
    }
    notes.push(
        "Embedded .imb strings are reported as resource references with selector lanes; this inspector still does not decode full control record semantics."
            .to_string(),
    );

    Ok(WinInspectionReport {
        file_size: bytes.len(),
        common_header_len: WIN_COMMON_HEADER_LEN,
        common_header_len_hex: format!("0x{WIN_COMMON_HEADER_LEN:02x}"),
        shared_header_words,
        matches_observed_common_signature,
        common_resource_record_prelude_prefix_words,
        name_len_matches_prelude_word_3_plus_nul_count,
        inline_root_resource_name,
        inline_root_resource_offset,
        inline_root_resource_offset_hex,
        imb_reference_count: all_imb_references.len(),
        unique_imb_reference_count: unique_imb_references.len(),
        unique_imb_references,
        dominant_reference_deltas: build_delta_histogram(&resource_record_samples),
        resource_selector_records,
        anonymous_selector_records,
        first_resource_record_samples: resource_record_samples.into_iter().take(32).collect(),
        first_imb_references: all_imb_references.into_iter().take(32).collect(),
        notes,
    })
}

fn collect_imb_references(bytes: &[u8]) -> Vec<WinResourceReference> {
    let mut references = Vec::new();
    let mut offset = 0usize;
    while offset < bytes.len() {
        if let Some(name) = parse_imb_reference_at(bytes, offset) {
            references.push(WinResourceReference {
                offset,
                offset_hex: format!("0x{offset:04x}"),
                name,
            });
        }
        offset += 1;
    }
    references
}

fn build_resource_record_samples(
    bytes: &[u8],
    references: &[WinResourceReference],
) -> Vec<WinResourceRecordSample> {
    let mut samples = Vec::with_capacity(references.len());
    for (index, reference) in references.iter().enumerate() {
        let previous_offset = index
            .checked_sub(1)
            .and_then(|previous| references.get(previous))
            .map(|previous| previous.offset);
        let delta_from_previous = previous_offset.map(|previous| reference.offset - previous);
        let delta_from_previous_hex = delta_from_previous.map(|delta| format!("0x{delta:x}"));

        let prelude_words = if reference.offset >= 16 {
            (0..4)
                .map(|index| {
                    let offset = reference.offset - 16 + index * 4;
                    let value = read_u32_le(bytes, offset).unwrap_or(0);
                    WinHeaderWord {
                        offset,
                        offset_hex: format!("0x{offset:04x}"),
                        value,
                        value_hex: format!("0x{value:08x}"),
                    }
                })
                .collect()
        } else {
            Vec::new()
        };

        let name_end = reference.offset + reference.name.len();
        let post_name_word_0 = read_u32_le(bytes, name_end + 1).unwrap_or(0);
        let post_name_word_1 = read_u32_le(bytes, name_end + 5).unwrap_or(0);
        let post_name_word_0_high_u16 = ((post_name_word_0 >> 16) & 0xffff) as u16;
        let post_name_word_0_low_u16 = (post_name_word_0 & 0xffff) as u16;

        samples.push(WinResourceRecordSample {
            offset: reference.offset,
            offset_hex: reference.offset_hex.clone(),
            name: reference.name.clone(),
            delta_from_previous,
            delta_from_previous_hex,
            prelude_words,
            post_name_word_0,
            post_name_word_0_hex: format!("0x{post_name_word_0:08x}"),
            post_name_word_0_high_u16,
            post_name_word_0_high_u16_hex: format!("0x{post_name_word_0_high_u16:04x}"),
            post_name_word_0_low_u16,
            post_name_word_0_low_u16_hex: format!("0x{post_name_word_0_low_u16:04x}"),
            post_name_word_1,
            post_name_word_1_hex: format!("0x{post_name_word_1:08x}"),
        });
    }
    samples
}

fn build_delta_histogram(samples: &[WinResourceRecordSample]) -> Vec<WinReferenceDeltaFrequency> {
    let mut counts = std::collections::BTreeMap::<usize, usize>::new();
    for sample in samples {
        if let Some(delta) = sample.delta_from_previous {
            *counts.entry(delta).or_default() += 1;
        }
    }

    let mut frequencies = counts
        .into_iter()
        .map(|(delta, count)| WinReferenceDeltaFrequency {
            delta,
            delta_hex: format!("0x{delta:x}"),
            count,
        })
        .collect::<Vec<_>>();
    frequencies.sort_by(|left, right| {
        right
            .count
            .cmp(&left.count)
            .then_with(|| left.delta.cmp(&right.delta))
    });
    frequencies.truncate(12);
    frequencies
}

fn build_resource_selector_records(
    samples: &[WinResourceRecordSample],
) -> Vec<WinResourceSelectorRecord> {
    samples
        .iter()
        .map(|sample| {
            let post_name_word_1_high_u16 = ((sample.post_name_word_1 >> 16) & 0xffff) as u16;
            let post_name_word_1_middle_u16 = ((sample.post_name_word_1 >> 8) & 0xffff) as u16;
            let post_name_word_1_low_u16 = (sample.post_name_word_1 & 0xffff) as u16;
            WinResourceSelectorRecord {
                offset: sample.offset,
                offset_hex: sample.offset_hex.clone(),
                name: sample.name.clone(),
                post_name_word_0: sample.post_name_word_0,
                post_name_word_0_hex: sample.post_name_word_0_hex.clone(),
                selector_high_u16: sample.post_name_word_0_high_u16,
                selector_high_u16_hex: sample.post_name_word_0_high_u16_hex.clone(),
                selector_low_u16: sample.post_name_word_0_low_u16,
                selector_low_u16_hex: sample.post_name_word_0_low_u16_hex.clone(),
                post_name_word_1: sample.post_name_word_1,
                post_name_word_1_hex: sample.post_name_word_1_hex.clone(),
                post_name_word_1_high_u16,
                post_name_word_1_high_u16_hex: format!("0x{post_name_word_1_high_u16:04x}"),
                post_name_word_1_middle_u16,
                post_name_word_1_middle_u16_hex: format!("0x{post_name_word_1_middle_u16:04x}"),
                post_name_word_1_low_u16,
                post_name_word_1_low_u16_hex: format!("0x{post_name_word_1_low_u16:04x}"),
            }
        })
        .collect()
}

fn collect_anonymous_selector_records(
    bytes: &[u8],
    references: &[WinResourceReference],
) -> Vec<WinAnonymousSelectorRecord> {
    const PRELUDE: [u8; 12] = [
        0xb8, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x0b, 0x00, 0x00,
    ];

    let mut records = Vec::new();
    let mut start = 0usize;
    while let Some(relative) = bytes.get(start..).and_then(|slice| {
        slice
            .windows(PRELUDE.len())
            .position(|window| window == PRELUDE)
    }) {
        let record_offset = start + relative;
        let name_len = read_u32_le(bytes, record_offset + PRELUDE.len()).unwrap_or(0);
        if name_len == 0 {
            let selector_word_0 = read_u32_le(bytes, record_offset + 0x10).unwrap_or(0);
            let selector_word_0_low_u16 = (selector_word_0 & 0xffff) as u16;
            if (0xc352..=0xc39b).contains(&selector_word_0_low_u16) {
                let preceding_named_record = references
                    .iter()
                    .rev()
                    .find(|reference| reference.offset < record_offset);
                let following_named_record = references
                    .iter()
                    .find(|reference| reference.offset > record_offset);
                let selector_word_1 = read_u32_le(bytes, record_offset + 0x14).unwrap_or(0);
                let selector_word_0_high_u16 = ((selector_word_0 >> 16) & 0xffff) as u16;
                let selector_word_1_middle_u16 = ((selector_word_1 >> 8) & 0xffff) as u16;
                let body_word_0 = read_u32_le(bytes, record_offset + 0x18).unwrap_or(0);
                let body_word_1 = read_u32_le(bytes, record_offset + 0x1c).unwrap_or(0);
                let body_word_2 = read_u32_le(bytes, record_offset + 0x20).unwrap_or(0);
                let body_word_3 = read_u32_le(bytes, record_offset + 0x24).unwrap_or(0);
                let footer_word_0 = read_u32_le(bytes, record_offset + 0x98).unwrap_or(0);
                let footer_word_1 = read_u32_le(bytes, record_offset + 0x9c).unwrap_or(0);
                records.push(WinAnonymousSelectorRecord {
                    record_offset,
                    record_offset_hex: format!("0x{record_offset:04x}"),
                    preceding_named_record_name: preceding_named_record
                        .map(|record| record.name.clone()),
                    preceding_named_record_offset_hex: preceding_named_record
                        .map(|record| record.offset_hex.clone()),
                    following_named_record_name: following_named_record
                        .map(|record| record.name.clone()),
                    following_named_record_offset_hex: following_named_record
                        .map(|record| record.offset_hex.clone()),
                    selector_word_0,
                    selector_word_0_hex: format!("0x{selector_word_0:08x}"),
                    selector_word_0_high_u16,
                    selector_word_0_high_u16_hex: format!("0x{selector_word_0_high_u16:04x}"),
                    selector_word_0_low_u16,
                    selector_word_0_low_u16_hex: format!("0x{selector_word_0_low_u16:04x}"),
                    selector_word_1,
                    selector_word_1_hex: format!("0x{selector_word_1:08x}"),
                    selector_word_1_middle_u16,
                    selector_word_1_middle_u16_hex: format!("0x{selector_word_1_middle_u16:04x}"),
                    body_word_0,
                    body_word_0_hex: format!("0x{body_word_0:08x}"),
                    body_word_1,
                    body_word_1_hex: format!("0x{body_word_1:08x}"),
                    body_word_2,
                    body_word_2_hex: format!("0x{body_word_2:08x}"),
                    body_word_3,
                    body_word_3_hex: format!("0x{body_word_3:08x}"),
                    footer_word_0,
                    footer_word_0_hex: format!("0x{footer_word_0:08x}"),
                    footer_word_1,
                    footer_word_1_hex: format!("0x{footer_word_1:08x}"),
                });
            }
        }
        start = record_offset + 1;
    }
    records
}

fn shared_prelude_prefix_hex(samples: &[WinResourceRecordSample]) -> Option<Vec<String>> {
    let first = samples.first()?;
    if first.prelude_words.len() < 3 {
        return None;
    }
    let prefix = first.prelude_words[..3]
        .iter()
        .map(|word| word.value)
        .collect::<Vec<_>>();
    if samples.iter().all(|sample| {
        sample.prelude_words.len() >= 3
            && sample.prelude_words[..3]
                .iter()
                .map(|word| word.value)
                .collect::<Vec<_>>()
                == prefix
    }) {
        return Some(
            prefix
                .into_iter()
                .map(|value| format!("0x{value:08x}"))
                .collect(),
        );
    }
    None
}

fn parse_imb_reference_at(bytes: &[u8], offset: usize) -> Option<String> {
    if offset > 0 {
        let previous = *bytes.get(offset - 1)?;
        if previous != 0 {
            return None;
        }
    }
    let slice = bytes.get(offset..)?;
    let nul = slice.iter().position(|byte| *byte == 0)?;
    let candidate = slice.get(..nul)?;
    if candidate.len() < 5 {
        return None;
    }
    let value = std::str::from_utf8(candidate).ok()?;
    if !value.ends_with(".imb") {
        return None;
    }
    if !value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b' '))
    {
        return None;
    }
    Some(value.to_string())
}

fn parse_inline_ascii_name(bytes: &[u8], offset: usize) -> Option<String> {
    let prefix = *bytes.get(offset)?;
    if prefix != 0 {
        return None;
    }
    parse_imb_reference_at(bytes, offset + 1)
}

fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
    let slice = bytes.get(offset..offset + 4)?;
    Some(u32::from_le_bytes(slice.try_into().ok()?))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn inspects_synthetic_window_blob() {
        let mut bytes = vec![0u8; 0x90];
        bytes[0x00..0x04].copy_from_slice(&0x0000_07d0u32.to_le_bytes());
        bytes[0x0c..0x10].copy_from_slice(&0x8000_0000u32.to_le_bytes());
        bytes[0x10..0x14].copy_from_slice(&0x8000_003fu32.to_le_bytes());
        bytes[0x14..0x18].copy_from_slice(&0x0000_003fu32.to_le_bytes());
        bytes[0x34..0x38].copy_from_slice(&0x0007_d100u32.to_le_bytes());
        bytes[0x38..0x3c].copy_from_slice(&0x0007_d200u32.to_le_bytes());
        bytes[0x40..0x44].copy_from_slice(&0x000b_b800u32.to_le_bytes());
        bytes[0x48..0x4c].copy_from_slice(&0x000b_b900u32.to_le_bytes());
        bytes[0x50] = 0;
        bytes[0x51..0x51 + "Root.imb".len()].copy_from_slice(b"Root.imb");
        bytes[0x59] = 0;
        bytes.extend_from_slice(b"\0Button.imb\0");

        let report = inspect_win_bytes(&bytes).expect("inspection should succeed");
        assert!(report.matches_observed_common_signature);
        assert_eq!(
            report.inline_root_resource_name.as_deref(),
            Some("Root.imb")
        );
        assert_eq!(report.imb_reference_count, 2);
        assert_eq!(report.unique_imb_reference_count, 2);
        assert_eq!(report.resource_selector_records.len(), 2);
        assert_eq!(report.resource_selector_records[0].name, "Root.imb");
        assert!(report.anonymous_selector_records.is_empty());
    }
}
