use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use rrt_runtime::inspect::smp::bundle::inspect_smp_file;
use serde::Serialize;

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableSample {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) semantic_family: String,
    pub(crate) header_word_0_hex: String,
    pub(crate) header_word_1_hex: String,
    pub(crate) header_word_2_hex: String,
    pub(crate) observed_entry_count: usize,
    pub(crate) zero_trailer_entry_count: usize,
    pub(crate) nonzero_trailer_entry_count: usize,
    pub(crate) zero_trailer_entry_names: Vec<String>,
    pub(crate) footer_progress_word_0_hex: String,
    pub(crate) footer_progress_word_1_hex: String,
    pub(crate) availability_by_name: BTreeMap<String, u32>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableEntrySample {
    pub(crate) index: usize,
    pub(crate) offset: usize,
    pub(crate) text: String,
    pub(crate) availability_dword: u32,
    pub(crate) availability_dword_hex: String,
    pub(crate) trailer_word: u32,
    pub(crate) trailer_word_hex: String,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableInspectionReport {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) semantic_family: String,
    pub(crate) header_word_0_hex: String,
    pub(crate) header_word_1_hex: String,
    pub(crate) header_word_2_hex: String,
    pub(crate) observed_entry_capacity: usize,
    pub(crate) observed_entry_count: usize,
    pub(crate) zero_trailer_entry_count: usize,
    pub(crate) nonzero_trailer_entry_count: usize,
    pub(crate) zero_trailer_entry_names: Vec<String>,
    pub(crate) entries: Vec<RuntimeCandidateTableEntrySample>,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRun {
    pub(crate) prefix: String,
    pub(crate) start_index: usize,
    pub(crate) end_index: usize,
    pub(crate) count: usize,
    pub(crate) first_name: String,
    pub(crate) last_name: String,
    pub(crate) start_offset: usize,
    pub(crate) end_offset: usize,
    pub(crate) distinct_trailer_hex_words: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableComparisonReport {
    pub(crate) file_count: usize,
    pub(crate) matches: bool,
    pub(crate) common_profile_family: Option<String>,
    pub(crate) common_semantic_family: Option<String>,
    pub(crate) samples: Vec<RuntimeCandidateTableSample>,
    pub(crate) difference_count: usize,
    pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}

pub(crate) fn compare_candidate_table(
    smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
    let samples = smp_paths
        .iter()
        .map(|path| load_candidate_table_sample(path))
        .collect::<Result<Vec<_>, _>>()?;
    let common_profile_family = samples
        .first()
        .map(|sample| sample.profile_family.clone())
        .filter(|family| {
            samples
                .iter()
                .all(|sample| sample.profile_family == *family)
        });
    let common_semantic_family = samples
        .first()
        .map(|sample| sample.semantic_family.clone())
        .filter(|family| {
            samples
                .iter()
                .all(|sample| sample.semantic_family == *family)
        });
    let differences = diff_candidate_table_samples(&samples)?;
    let report = RuntimeCandidateTableComparisonReport {
        file_count: samples.len(),
        matches: differences.is_empty(),
        common_profile_family,
        common_semantic_family,
        difference_count: differences.len(),
        differences,
        samples,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn inspect_candidate_table(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let report = load_candidate_table_inspection_report(smp_path)?;
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn load_candidate_table_sample(
    smp_path: &Path,
) -> Result<RuntimeCandidateTableSample, Box<dyn std::error::Error>> {
    let inspection = inspect_smp_file(smp_path)?;
    let probe = inspection.rt3_105_save_name_table_probe.ok_or_else(|| {
        format!(
            "{} did not expose an RT3 1.05 candidate-availability table",
            smp_path.display()
        )
    })?;

    Ok(RuntimeCandidateTableSample {
        path: smp_path.display().to_string(),
        profile_family: probe.profile_family,
        source_kind: probe.source_kind,
        semantic_family: probe.semantic_family,
        header_word_0_hex: probe.header_word_0_hex,
        header_word_1_hex: probe.header_word_1_hex,
        header_word_2_hex: probe.header_word_2_hex,
        observed_entry_count: probe.observed_entry_count,
        zero_trailer_entry_count: probe.zero_trailer_entry_count,
        nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
        zero_trailer_entry_names: probe.zero_trailer_entry_names,
        footer_progress_word_0_hex: probe.footer_progress_word_0_hex,
        footer_progress_word_1_hex: probe.footer_progress_word_1_hex,
        availability_by_name: probe
            .entries
            .into_iter()
            .map(|entry| (entry.text, entry.availability_dword))
            .collect(),
    })
}

pub(crate) fn load_candidate_table_inspection_report(
    smp_path: &Path,
) -> Result<RuntimeCandidateTableInspectionReport, Box<dyn std::error::Error>> {
    let inspection = inspect_smp_file(smp_path)?;
    if let Some(probe) = inspection.rt3_105_save_name_table_probe {
        return Ok(RuntimeCandidateTableInspectionReport {
            path: smp_path.display().to_string(),
            profile_family: probe.profile_family,
            source_kind: probe.source_kind,
            semantic_family: probe.semantic_family,
            header_word_0_hex: probe.header_word_0_hex,
            header_word_1_hex: probe.header_word_1_hex,
            header_word_2_hex: probe.header_word_2_hex,
            observed_entry_capacity: probe.observed_entry_capacity,
            observed_entry_count: probe.observed_entry_count,
            zero_trailer_entry_count: probe.zero_trailer_entry_count,
            nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
            zero_trailer_entry_names: probe.zero_trailer_entry_names,
            entries: probe
                .entries
                .into_iter()
                .map(|entry| RuntimeCandidateTableEntrySample {
                    index: entry.index,
                    offset: entry.offset,
                    text: entry.text,
                    availability_dword: entry.availability_dword,
                    availability_dword_hex: entry.availability_dword_hex,
                    trailer_word: entry.trailer_word,
                    trailer_word_hex: entry.trailer_word_hex,
                })
                .collect(),
        });
    }

    let bytes = fs::read(smp_path)?;
    let header_offset = 0x6a70usize;
    let entries_offset = 0x6ad1usize;
    let block_end_offset = 0x73c0usize;
    let entry_stride = 0x22usize;
    if bytes.len() < block_end_offset
        || !matches_candidate_table_header_bytes(&bytes, header_offset)
    {
        return Err(format!(
            "{} did not expose an RT3 1.05 candidate-availability table",
            smp_path.display()
        )
        .into());
    }

    let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
        .ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
        as usize;
    let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
        .ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
        as usize;
    if observed_entry_capacity < observed_entry_count {
        return Err(format!(
            "{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
            smp_path.display()
        )
        .into());
    }
    let entries_end_offset = entries_offset
        .checked_add(
            observed_entry_count
                .checked_mul(entry_stride)
                .ok_or("candidate table length overflow")?,
        )
        .ok_or("candidate table end overflow")?;
    if entries_end_offset > block_end_offset {
        return Err(format!(
            "{} candidate table overruns fixed block end",
            smp_path.display()
        )
        .into());
    }

    let mut zero_trailer_entry_names = Vec::new();
    let mut entries = Vec::new();
    for index in 0..observed_entry_count {
        let offset = entries_offset + index * entry_stride;
        let chunk = &bytes[offset..offset + entry_stride];
        let nul_index = chunk
            .iter()
            .position(|byte| *byte == 0)
            .unwrap_or(entry_stride - 4);
        let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
            format!(
                "{} contains invalid UTF-8 in candidate table",
                smp_path.display()
            )
        })?;
        let availability_dword =
            read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
                format!(
                    "{} is missing candidate availability dword",
                    smp_path.display()
                )
            })?;
        if availability_dword == 0 {
            zero_trailer_entry_names.push(text.to_string());
        }
        entries.push(RuntimeCandidateTableEntrySample {
            index,
            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}"),
        });
    }

    Ok(RuntimeCandidateTableInspectionReport {
        path: smp_path.display().to_string(),
        profile_family: classify_candidate_table_header_profile(
            smp_path
                .extension()
                .and_then(|ext| ext.to_str())
                .map(|ext| ext.to_ascii_lowercase()),
            &bytes,
        ),
        source_kind: match smp_path
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.to_ascii_lowercase())
            .as_deref()
        {
            Some("gmp") => "map-fixed-catalog-range",
            Some("gms") => "save-fixed-catalog-range",
            _ => "fixed-catalog-range",
        }
        .to_string(),
        semantic_family: "scenario-named-candidate-availability-table".to_string(),
        header_word_0_hex: format!(
            "0x{:08x}",
            read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
        ),
        header_word_1_hex: format!(
            "0x{:08x}",
            read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
        ),
        header_word_2_hex: format!(
            "0x{:08x}",
            read_u32_le(&bytes, header_offset + 8).ok_or("missing candidate header word 2")?
        ),
        observed_entry_capacity,
        observed_entry_count,
        zero_trailer_entry_count: zero_trailer_entry_names.len(),
        nonzero_trailer_entry_count: observed_entry_count - zero_trailer_entry_names.len(),
        zero_trailer_entry_names,
        entries,
    })
}

pub(crate) fn diff_candidate_table_samples(
    samples: &[RuntimeCandidateTableSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
    let labeled_values = samples
        .iter()
        .map(|sample| {
            (
                sample.path.clone(),
                serde_json::json!({
                    "profile_family": sample.profile_family,
                    "source_kind": sample.source_kind,
                    "semantic_family": sample.semantic_family,
                    "header_word_0_hex": sample.header_word_0_hex,
                    "header_word_1_hex": sample.header_word_1_hex,
                    "header_word_2_hex": sample.header_word_2_hex,
                    "observed_entry_count": sample.observed_entry_count,
                    "zero_trailer_entry_count": sample.zero_trailer_entry_count,
                    "nonzero_trailer_entry_count": sample.nonzero_trailer_entry_count,
                    "zero_trailer_entry_names": sample.zero_trailer_entry_names,
                    "footer_progress_word_0_hex": sample.footer_progress_word_0_hex,
                    "footer_progress_word_1_hex": sample.footer_progress_word_1_hex,
                    "availability_by_name": sample.availability_by_name,
                }),
            )
        })
        .collect::<Vec<_>>();
    let mut differences = Vec::new();
    collect_json_multi_differences("$", &labeled_values, &mut differences);
    Ok(differences)
}

pub(crate) fn collect_numbered_candidate_name_runs(
    entries: &[RuntimeCandidateTableEntrySample],
    prefix: &str,
) -> Vec<RuntimeCandidateTableNamedRun> {
    let mut numbered_entries = entries
        .iter()
        .filter_map(|entry| {
            parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number))
        })
        .collect::<Vec<_>>();
    numbered_entries.sort_by_key(|(entry, number)| (entry.index, *number));

    let mut runs = Vec::new();
    let mut cursor = 0usize;
    while cursor < numbered_entries.len() {
        let (first_entry, first_number) = numbered_entries[cursor];
        let mut last_entry = first_entry;
        let mut last_number = first_number;
        let mut distinct_trailer_hex_words = BTreeSet::from([first_entry.trailer_word_hex.clone()]);
        let mut next = cursor + 1;
        while next < numbered_entries.len() {
            let (entry, number) = numbered_entries[next];
            if entry.index != last_entry.index + 1 || number != last_number + 1 {
                break;
            }
            distinct_trailer_hex_words.insert(entry.trailer_word_hex.clone());
            last_entry = entry;
            last_number = number;
            next += 1;
        }
        runs.push(RuntimeCandidateTableNamedRun {
            prefix: prefix.to_string(),
            start_index: first_entry.index,
            end_index: last_entry.index,
            count: next - cursor,
            first_name: first_entry.text.clone(),
            last_name: last_entry.text.clone(),
            start_offset: first_entry.offset,
            end_offset: last_entry.offset,
            distinct_trailer_hex_words: distinct_trailer_hex_words.into_iter().collect(),
        });
        cursor = next;
    }

    runs
}

pub(crate) fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option<usize> {
    let digits = text.strip_prefix(prefix)?;
    if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
        return None;
    }
    digits.parse().ok()
}

pub(crate) fn matches_candidate_table_header_bytes(bytes: &[u8], header_offset: usize) -> bool {
    matches!(
        (
            read_u32_le(bytes, header_offset + 0x08),
            read_u32_le(bytes, header_offset + 0x0c),
            read_u32_le(bytes, header_offset + 0x10),
            read_u32_le(bytes, header_offset + 0x14),
            read_u32_le(bytes, header_offset + 0x18),
            read_u32_le(bytes, header_offset + 0x1c),
            read_u32_le(bytes, header_offset + 0x20),
            read_u32_le(bytes, header_offset + 0x24),
            read_u32_le(bytes, header_offset + 0x28),
        ),
        (
            Some(0x0000332e),
            Some(0x00000001),
            Some(0x00000022),
            Some(0x00000002),
            Some(0x00000002),
            Some(68),
            Some(67),
            Some(0x00000000),
            Some(0x00000001),
        )
    )
}

pub(crate) fn classify_candidate_table_header_profile(
    extension: Option<String>,
    bytes: &[u8],
) -> String {
    let word_2 = read_u32_le(bytes, 8);
    let word_3 = read_u32_le(bytes, 12);
    let word_5 = read_u32_le(bytes, 20);
    match (extension.as_deref().unwrap_or(""), word_2, word_3, word_5) {
        ("gmp", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
            "rt3-105-map-container-v1".to_string()
        }
        ("gmp", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
            "rt3-105-scenario-map-container-v1".to_string()
        }
        ("gmp", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
            "rt3-105-alt-map-container-v1".to_string()
        }
        ("gms", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
            "rt3-105-save-container-v1".to_string()
        }
        ("gms", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
            "rt3-105-scenario-save-container-v1".to_string()
        }
        ("gms", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
            "rt3-105-alt-save-container-v1".to_string()
        }
        ("gmp", _, _, _) => "map-fixed-catalog-container-unknown".to_string(),
        ("gms", _, _, _) => "save-fixed-catalog-container-unknown".to_string(),
        _ => "fixed-catalog-container-unknown".to_string(),
    }
}

pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
    let chunk = bytes.get(offset..offset + 4)?;
    Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
}

pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
    let chunk = bytes.get(offset..offset + 2)?;
    Some(u16::from_le_bytes([chunk[0], chunk[1]]))
}

pub(crate) fn hex_encode(bytes: &[u8]) -> String {
    let mut text = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        use std::fmt::Write as _;
        let _ = write!(&mut text, "{byte:02x}");
    }
    text
}
