use crate::app::runtime_compare::{
    RuntimeCandidateTableNamedRun, classify_candidate_table_header_profile,
    collect_numbered_candidate_name_runs, load_candidate_table_inspection_report,
    matches_candidate_table_header_bytes, read_u32_le,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use serde::Serialize;

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderCluster {
    pub(crate) header_word_0_hex: String,
    pub(crate) header_word_1_hex: String,
    pub(crate) file_count: usize,
    pub(crate) profile_families: Vec<String>,
    pub(crate) source_kinds: Vec<String>,
    pub(crate) zero_trailer_count_min: usize,
    pub(crate) zero_trailer_count_max: usize,
    pub(crate) zero_trailer_count_values: Vec<usize>,
    pub(crate) distinct_zero_name_set_count: usize,
    pub(crate) sample_paths: Vec<String>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderScanReport {
    pub(crate) root_path: String,
    pub(crate) file_count: usize,
    pub(crate) cluster_count: usize,
    pub(crate) skipped_file_count: usize,
    pub(crate) clusters: Vec<RuntimeCandidateTableHeaderCluster>,
}

#[derive(Debug, Clone)]
pub(crate) struct RuntimeCandidateTableHeaderScanSample {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) header_word_0_hex: String,
    pub(crate) header_word_1_hex: String,
    pub(crate) zero_trailer_entry_count: usize,
    pub(crate) zero_trailer_entry_names: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanSample {
    pub(crate) path: String,
    pub(crate) profile_family: String,
    pub(crate) source_kind: String,
    pub(crate) observed_entry_count: usize,
    pub(crate) port_runs: Vec<RuntimeCandidateTableNamedRun>,
    pub(crate) warehouse_runs: Vec<RuntimeCandidateTableNamedRun>,
}

#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanReport {
    pub(crate) root_path: String,
    pub(crate) file_count: usize,
    pub(crate) files_with_probe_count: usize,
    pub(crate) files_with_any_numbered_port_runs_count: usize,
    pub(crate) files_with_any_numbered_warehouse_runs_count: usize,
    pub(crate) files_with_both_numbered_run_families_count: usize,
    pub(crate) skipped_file_count: usize,
    pub(crate) samples: Vec<RuntimeCandidateTableNamedRunScanSample>,
}

pub(crate) fn scan_candidate_table_headers(
    root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut candidate_paths = Vec::new();
    collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;

    let mut samples = Vec::new();
    let mut skipped_file_count = 0usize;
    for path in candidate_paths {
        match load_candidate_table_header_scan_sample(&path) {
            Ok(sample) => samples.push(sample),
            Err(_) => skipped_file_count += 1,
        }
    }

    let mut grouped =
        BTreeMap::<(String, String), Vec<RuntimeCandidateTableHeaderScanSample>>::new();
    for sample in samples {
        grouped
            .entry((
                sample.header_word_0_hex.clone(),
                sample.header_word_1_hex.clone(),
            ))
            .or_default()
            .push(sample);
    }

    let file_count = grouped.values().map(Vec::len).sum();
    let clusters = grouped
        .into_iter()
        .map(|((header_word_0_hex, header_word_1_hex), samples)| {
            let mut profile_families = samples
                .iter()
                .map(|sample| sample.profile_family.clone())
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect::<Vec<_>>();
            let mut source_kinds = samples
                .iter()
                .map(|sample| sample.source_kind.clone())
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect::<Vec<_>>();
            let mut zero_trailer_count_values = samples
                .iter()
                .map(|sample| sample.zero_trailer_entry_count)
                .collect::<BTreeSet<_>>()
                .into_iter()
                .collect::<Vec<_>>();
            let distinct_zero_name_set_count = samples
                .iter()
                .map(|sample| sample.zero_trailer_entry_names.clone())
                .collect::<BTreeSet<_>>()
                .len();
            let zero_trailer_count_min = samples
                .iter()
                .map(|sample| sample.zero_trailer_entry_count)
                .min()
                .unwrap_or(0);
            let zero_trailer_count_max = samples
                .iter()
                .map(|sample| sample.zero_trailer_entry_count)
                .max()
                .unwrap_or(0);
            let sample_paths = samples
                .iter()
                .take(12)
                .map(|sample| sample.path.clone())
                .collect::<Vec<_>>();
            profile_families.sort();
            source_kinds.sort();
            zero_trailer_count_values.sort();

            RuntimeCandidateTableHeaderCluster {
                header_word_0_hex,
                header_word_1_hex,
                file_count: samples.len(),
                profile_families,
                source_kinds,
                zero_trailer_count_min,
                zero_trailer_count_max,
                zero_trailer_count_values,
                distinct_zero_name_set_count,
                sample_paths,
            }
        })
        .collect::<Vec<_>>();

    let report = RuntimeCandidateTableHeaderScanReport {
        root_path: root_path.display().to_string(),
        file_count,
        cluster_count: clusters.len(),
        skipped_file_count,
        clusters,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn scan_candidate_table_named_runs(
    root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut candidate_paths = Vec::new();
    collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;

    let file_count = candidate_paths.len();
    let mut samples = Vec::new();
    let mut skipped_file_count = 0usize;
    for path in candidate_paths {
        match load_candidate_table_named_run_scan_sample(&path) {
            Ok(sample) => samples.push(sample),
            Err(_) => skipped_file_count += 1,
        }
    }

    let files_with_probe_count = samples.len();
    let files_with_any_numbered_port_runs_count = samples
        .iter()
        .filter(|sample| !sample.port_runs.is_empty())
        .count();
    let files_with_any_numbered_warehouse_runs_count = samples
        .iter()
        .filter(|sample| !sample.warehouse_runs.is_empty())
        .count();
    let files_with_both_numbered_run_families_count = samples
        .iter()
        .filter(|sample| !sample.port_runs.is_empty() && !sample.warehouse_runs.is_empty())
        .count();

    let report = RuntimeCandidateTableNamedRunScanReport {
        root_path: root_path.display().to_string(),
        file_count,
        files_with_probe_count,
        files_with_any_numbered_port_runs_count,
        files_with_any_numbered_warehouse_runs_count,
        files_with_both_numbered_run_families_count,
        skipped_file_count,
        samples,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn collect_candidate_table_input_paths(
    root_path: &Path,
    out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
    let metadata = match fs::symlink_metadata(root_path) {
        Ok(metadata) => metadata,
        Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
        Err(err) => return Err(err.into()),
    };
    if metadata.file_type().is_symlink() {
        return Ok(());
    }

    if root_path.is_file() {
        if root_path
            .extension()
            .and_then(|ext| ext.to_str())
            .is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
        {
            out.push(root_path.to_path_buf());
        }
        return Ok(());
    }

    let entries = match fs::read_dir(root_path) {
        Ok(entries) => entries,
        Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
        Err(err) => return Err(err.into()),
    };

    for entry in entries {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            collect_candidate_table_input_paths(&path, out)?;
            continue;
        }
        if path
            .extension()
            .and_then(|ext| ext.to_str())
            .is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
        {
            out.push(path);
        }
    }

    Ok(())
}

pub(crate) fn load_candidate_table_header_scan_sample(
    smp_path: &Path,
) -> Result<RuntimeCandidateTableHeaderScanSample, Box<dyn std::error::Error>> {
    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 {
        return Err(format!(
            "{} is too small for the fixed candidate table range",
            smp_path.display()
        )
        .into());
    }
    if !matches_candidate_table_header_bytes(&bytes, header_offset) {
        return Err(format!(
            "{} does not contain the fixed candidate table header",
            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();
    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 = read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
            format!(
                "{} is missing candidate availability dword",
                smp_path.display()
            )
        })?;
        if availability == 0 {
            zero_trailer_entry_names.push(text.to_string());
        }
    }

    let profile_family = classify_candidate_table_header_profile(
        smp_path
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.to_ascii_lowercase()),
        &bytes,
    );
    let 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();

    Ok(RuntimeCandidateTableHeaderScanSample {
        path: smp_path.display().to_string(),
        profile_family,
        source_kind,
        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")?
        ),
        zero_trailer_entry_count: zero_trailer_entry_names.len(),
        zero_trailer_entry_names,
    })
}

pub(crate) fn load_candidate_table_named_run_scan_sample(
    smp_path: &Path,
) -> Result<RuntimeCandidateTableNamedRunScanSample, Box<dyn std::error::Error>> {
    let report = load_candidate_table_inspection_report(smp_path)?;
    let port_runs = collect_numbered_candidate_name_runs(&report.entries, "Port");
    let warehouse_runs = collect_numbered_candidate_name_runs(&report.entries, "Warehouse");

    Ok(RuntimeCandidateTableNamedRunScanSample {
        path: report.path,
        profile_family: report.profile_family,
        source_kind: report.source_kind,
        observed_entry_count: report.observed_entry_count,
        port_runs,
        warehouse_runs,
    })
}
