use super::candidate_table::{
    classify_candidate_table_header_profile, hex_encode, read_u16_le, read_u32_le,
};
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use rrt_runtime::inspect::campaign::{CAMPAIGN_SCENARIO_COUNT, OBSERVED_CAMPAIGN_SCENARIO_NAMES};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreSample {
    pub(crate) path: String,
    pub(crate) file_extension: String,
    pub(crate) inferred_profile_family: String,
    pub(crate) payload_word_0x14: u16,
    pub(crate) payload_word_0x14_hex: String,
    pub(crate) payload_byte_0x20: u8,
    pub(crate) payload_byte_0x20_hex: String,
    pub(crate) marker_bytes_0x2c9_0x2d0_hex: String,
    pub(crate) row_category_byte_0x31a: u8,
    pub(crate) row_category_byte_0x31a_hex: String,
    pub(crate) row_visibility_byte_0x31b: u8,
    pub(crate) row_visibility_byte_0x31b_hex: String,
    pub(crate) row_visibility_byte_0x31c: u8,
    pub(crate) row_visibility_byte_0x31c_hex: String,
    pub(crate) row_count_word_0x3ae: u16,
    pub(crate) row_count_word_0x3ae_hex: String,
    pub(crate) payload_word_0x3b2: u16,
    pub(crate) payload_word_0x3b2_hex: String,
    pub(crate) payload_word_0x3ba: u16,
    pub(crate) payload_word_0x3ba_hex: String,
    pub(crate) candidate_header_word_0_hex: Option<String>,
    pub(crate) candidate_header_word_1_hex: Option<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreComparisonReport {
    pub(crate) file_count: usize,
    pub(crate) matches: bool,
    pub(crate) samples: Vec<RuntimeSetupPayloadCoreSample>,
    pub(crate) difference_count: usize,
    pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadSample {
    pub(crate) path: String,
    pub(crate) file_extension: String,
    pub(crate) inferred_profile_family: String,
    pub(crate) launch_flag_byte_0x22: u8,
    pub(crate) launch_flag_byte_0x22_hex: String,
    pub(crate) campaign_progress_in_known_range: bool,
    pub(crate) campaign_progress_scenario_name: Option<String>,
    pub(crate) campaign_progress_page_index: Option<usize>,
    pub(crate) launch_selector_byte_0x33: u8,
    pub(crate) launch_selector_byte_0x33_hex: String,
    pub(crate) launch_token_block_0x23_0x32_hex: String,
    pub(crate) campaign_selector_values: BTreeMap<String, u8>,
    pub(crate) nonzero_campaign_selector_values: BTreeMap<String, u8>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadComparisonReport {
    pub(crate) file_count: usize,
    pub(crate) matches: bool,
    pub(crate) samples: Vec<RuntimeSetupLaunchPayloadSample>,
    pub(crate) difference_count: usize,
    pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}

pub(crate) fn compare_setup_payload_core(
    smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
    let samples = smp_paths
        .iter()
        .map(|path| load_setup_payload_core_sample(path))
        .collect::<Result<Vec<_>, _>>()?;
    let differences = diff_setup_payload_core_samples(&samples)?;
    let report = RuntimeSetupPayloadCoreComparisonReport {
        file_count: samples.len(),
        matches: differences.is_empty(),
        difference_count: differences.len(),
        differences,
        samples,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn compare_setup_launch_payload(
    smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
    let samples = smp_paths
        .iter()
        .map(|path| load_setup_launch_payload_sample(path))
        .collect::<Result<Vec<_>, _>>()?;
    let differences = diff_setup_launch_payload_samples(&samples)?;
    let report = RuntimeSetupLaunchPayloadComparisonReport {
        file_count: samples.len(),
        matches: differences.is_empty(),
        difference_count: differences.len(),
        differences,
        samples,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn load_setup_payload_core_sample(
    smp_path: &Path,
) -> Result<RuntimeSetupPayloadCoreSample, Box<dyn std::error::Error>> {
    let bytes = fs::read(smp_path)?;
    let extension = smp_path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_ascii_lowercase())
        .unwrap_or_default();
    let inferred_profile_family =
        classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
    let candidate_header_word_0 = read_u32_le(&bytes, 0x6a70);
    let candidate_header_word_1 = read_u32_le(&bytes, 0x6a74);

    Ok(RuntimeSetupPayloadCoreSample {
        path: smp_path.display().to_string(),
        file_extension: extension,
        inferred_profile_family,
        payload_word_0x14: read_u16_le(&bytes, 0x14)
            .ok_or_else(|| format!("{} missing setup payload word +0x14", smp_path.display()))?,
        payload_word_0x14_hex: format!(
            "0x{:04x}",
            read_u16_le(&bytes, 0x14).ok_or_else(|| format!(
                "{} missing setup payload word +0x14",
                smp_path.display()
            ))?
        ),
        payload_byte_0x20: bytes
            .get(0x20)
            .copied()
            .ok_or_else(|| format!("{} missing setup payload byte +0x20", smp_path.display()))?,
        payload_byte_0x20_hex: format!(
            "0x{:02x}",
            bytes.get(0x20).copied().ok_or_else(|| format!(
                "{} missing setup payload byte +0x20",
                smp_path.display()
            ))?
        ),
        marker_bytes_0x2c9_0x2d0_hex: bytes
            .get(0x2c9..0x2d1)
            .map(hex_encode)
            .ok_or_else(|| format!("{} missing setup payload marker bytes", smp_path.display()))?,
        row_category_byte_0x31a: bytes
            .get(0x31a)
            .copied()
            .ok_or_else(|| format!("{} missing setup payload byte +0x31a", smp_path.display()))?,
        row_category_byte_0x31a_hex: format!(
            "0x{:02x}",
            bytes.get(0x31a).copied().ok_or_else(|| format!(
                "{} missing setup payload byte +0x31a",
                smp_path.display()
            ))?
        ),
        row_visibility_byte_0x31b: bytes
            .get(0x31b)
            .copied()
            .ok_or_else(|| format!("{} missing setup payload byte +0x31b", smp_path.display()))?,
        row_visibility_byte_0x31b_hex: format!(
            "0x{:02x}",
            bytes.get(0x31b).copied().ok_or_else(|| format!(
                "{} missing setup payload byte +0x31b",
                smp_path.display()
            ))?
        ),
        row_visibility_byte_0x31c: bytes
            .get(0x31c)
            .copied()
            .ok_or_else(|| format!("{} missing setup payload byte +0x31c", smp_path.display()))?,
        row_visibility_byte_0x31c_hex: format!(
            "0x{:02x}",
            bytes.get(0x31c).copied().ok_or_else(|| format!(
                "{} missing setup payload byte +0x31c",
                smp_path.display()
            ))?
        ),
        row_count_word_0x3ae: read_u16_le(&bytes, 0x3ae)
            .ok_or_else(|| format!("{} missing setup payload word +0x3ae", smp_path.display()))?,
        row_count_word_0x3ae_hex: format!(
            "0x{:04x}",
            read_u16_le(&bytes, 0x3ae).ok_or_else(|| format!(
                "{} missing setup payload word +0x3ae",
                smp_path.display()
            ))?
        ),
        payload_word_0x3b2: read_u16_le(&bytes, 0x3b2)
            .ok_or_else(|| format!("{} missing setup payload word +0x3b2", smp_path.display()))?,
        payload_word_0x3b2_hex: format!(
            "0x{:04x}",
            read_u16_le(&bytes, 0x3b2).ok_or_else(|| format!(
                "{} missing setup payload word +0x3b2",
                smp_path.display()
            ))?
        ),
        payload_word_0x3ba: read_u16_le(&bytes, 0x3ba)
            .ok_or_else(|| format!("{} missing setup payload word +0x3ba", smp_path.display()))?,
        payload_word_0x3ba_hex: format!(
            "0x{:04x}",
            read_u16_le(&bytes, 0x3ba).ok_or_else(|| format!(
                "{} missing setup payload word +0x3ba",
                smp_path.display()
            ))?
        ),
        candidate_header_word_0_hex: candidate_header_word_0.map(|value| format!("0x{value:08x}")),
        candidate_header_word_1_hex: candidate_header_word_1.map(|value| format!("0x{value:08x}")),
    })
}

pub(crate) fn load_setup_launch_payload_sample(
    smp_path: &Path,
) -> Result<RuntimeSetupLaunchPayloadSample, Box<dyn std::error::Error>> {
    let bytes = fs::read(smp_path)?;
    let extension = smp_path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.to_ascii_lowercase())
        .unwrap_or_default();
    let inferred_profile_family =
        classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
    let launch_flag_byte_0x22 = bytes
        .get(0x22)
        .copied()
        .ok_or_else(|| format!("{} missing setup launch byte +0x22", smp_path.display()))?;
    let launch_selector_byte_0x33 = bytes
        .get(0x33)
        .copied()
        .ok_or_else(|| format!("{} missing setup launch byte +0x33", smp_path.display()))?;
    let token_block = bytes
        .get(0x23..0x33)
        .ok_or_else(|| format!("{} missing setup launch token block", smp_path.display()))?;
    let campaign_progress_in_known_range =
        (launch_flag_byte_0x22 as usize) < CAMPAIGN_SCENARIO_COUNT;
    let campaign_progress_scenario_name = campaign_progress_in_known_range
        .then(|| OBSERVED_CAMPAIGN_SCENARIO_NAMES[launch_flag_byte_0x22 as usize].to_string());
    let campaign_progress_page_index = match launch_flag_byte_0x22 {
        0..=4 => Some(1),
        5..=9 => Some(2),
        10..=12 => Some(3),
        13..=15 => Some(4),
        _ => None,
    };
    let campaign_selector_values = OBSERVED_CAMPAIGN_SCENARIO_NAMES
        .iter()
        .enumerate()
        .map(|(index, name)| (name.to_string(), token_block[index]))
        .collect::<BTreeMap<_, _>>();
    let nonzero_campaign_selector_values = campaign_selector_values
        .iter()
        .filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value)))
        .collect::<BTreeMap<_, _>>();

    Ok(RuntimeSetupLaunchPayloadSample {
        path: smp_path.display().to_string(),
        file_extension: extension,
        inferred_profile_family,
        launch_flag_byte_0x22,
        launch_flag_byte_0x22_hex: format!("0x{launch_flag_byte_0x22:02x}"),
        campaign_progress_in_known_range,
        campaign_progress_scenario_name,
        campaign_progress_page_index,
        launch_selector_byte_0x33,
        launch_selector_byte_0x33_hex: format!("0x{launch_selector_byte_0x33:02x}"),
        launch_token_block_0x23_0x32_hex: hex_encode(token_block),
        campaign_selector_values,
        nonzero_campaign_selector_values,
    })
}

pub(crate) fn diff_setup_payload_core_samples(
    samples: &[RuntimeSetupPayloadCoreSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
    let labeled_values = samples
        .iter()
        .map(|sample| {
            (
                sample.path.clone(),
                serde_json::json!({
                    "file_extension": sample.file_extension,
                    "inferred_profile_family": sample.inferred_profile_family,
                    "payload_word_0x14": sample.payload_word_0x14,
                    "payload_word_0x14_hex": sample.payload_word_0x14_hex,
                    "payload_byte_0x20": sample.payload_byte_0x20,
                    "payload_byte_0x20_hex": sample.payload_byte_0x20_hex,
                    "marker_bytes_0x2c9_0x2d0_hex": sample.marker_bytes_0x2c9_0x2d0_hex,
                    "row_category_byte_0x31a": sample.row_category_byte_0x31a,
                    "row_category_byte_0x31a_hex": sample.row_category_byte_0x31a_hex,
                    "row_visibility_byte_0x31b": sample.row_visibility_byte_0x31b,
                    "row_visibility_byte_0x31b_hex": sample.row_visibility_byte_0x31b_hex,
                    "row_visibility_byte_0x31c": sample.row_visibility_byte_0x31c,
                    "row_visibility_byte_0x31c_hex": sample.row_visibility_byte_0x31c_hex,
                    "row_count_word_0x3ae": sample.row_count_word_0x3ae,
                    "row_count_word_0x3ae_hex": sample.row_count_word_0x3ae_hex,
                    "payload_word_0x3b2": sample.payload_word_0x3b2,
                    "payload_word_0x3b2_hex": sample.payload_word_0x3b2_hex,
                    "payload_word_0x3ba": sample.payload_word_0x3ba,
                    "payload_word_0x3ba_hex": sample.payload_word_0x3ba_hex,
                    "candidate_header_word_0_hex": sample.candidate_header_word_0_hex,
                    "candidate_header_word_1_hex": sample.candidate_header_word_1_hex,
                }),
            )
        })
        .collect::<Vec<_>>();
    let mut differences = Vec::new();
    collect_json_multi_differences("$", &labeled_values, &mut differences);
    Ok(differences)
}

pub(crate) fn diff_setup_launch_payload_samples(
    samples: &[RuntimeSetupLaunchPayloadSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
    let labeled_values = samples
        .iter()
        .map(|sample| {
            (
                sample.path.clone(),
                serde_json::json!({
                    "file_extension": sample.file_extension,
                    "inferred_profile_family": sample.inferred_profile_family,
                    "launch_flag_byte_0x22": sample.launch_flag_byte_0x22,
                    "launch_flag_byte_0x22_hex": sample.launch_flag_byte_0x22_hex,
                    "campaign_progress_in_known_range": sample.campaign_progress_in_known_range,
                    "campaign_progress_scenario_name": sample.campaign_progress_scenario_name,
                    "campaign_progress_page_index": sample.campaign_progress_page_index,
                    "launch_selector_byte_0x33": sample.launch_selector_byte_0x33,
                    "launch_selector_byte_0x33_hex": sample.launch_selector_byte_0x33_hex,
                    "launch_token_block_0x23_0x32_hex": sample.launch_token_block_0x23_0x32_hex,
                    "campaign_selector_values": sample.campaign_selector_values,
                    "nonzero_campaign_selector_values": sample.nonzero_campaign_selector_values,
                }),
            )
        })
        .collect::<Vec<_>>();
    let mut differences = Vec::new();
    collect_json_multi_differences("$", &labeled_values, &mut differences);
    Ok(differences)
}
