use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

use super::types::{CargoTypeEntry, CargoTypeInspectionReport, parse_cargo_name_token};

pub fn inspect_cargo_types_dir(
    path: &Path,
) -> Result<CargoTypeInspectionReport, Box<dyn std::error::Error>> {
    let mut entries = Vec::new();
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        if !entry.file_type()?.is_file() {
            continue;
        }
        let file_name = entry.file_name();
        let file_name = file_name.to_string_lossy().into_owned();
        if Path::new(&file_name)
            .extension()
            .and_then(|extension| extension.to_str())
            .map(|extension| extension.eq_ignore_ascii_case("cty"))
            != Some(true)
        {
            continue;
        }
        let bytes = fs::read(entry.path())?;
        entries.push(parse_cargo_type_entry(&file_name, &bytes)?);
    }
    entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name));

    let mut notes = Vec::new();
    notes.push(
        "CargoTypes entries carry a 0x03ea header and an inline NUL-terminated cargo token string."
            .to_string(),
    );
    notes.push(
        "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix."
            .to_string(),
    );

    let unique_visible_name_count = entries
        .iter()
        .map(|entry| entry.name.visible_name.as_str())
        .collect::<BTreeSet<_>>()
        .len();

    Ok(CargoTypeInspectionReport {
        directory_path: path.display().to_string(),
        entry_count: entries.len(),
        unique_visible_name_count,
        notes,
        entries,
    })
}

pub(super) fn parse_cargo_type_entry(
    file_name: &str,
    bytes: &[u8],
) -> Result<CargoTypeEntry, Box<dyn std::error::Error>> {
    if bytes.len() < 5 {
        return Err(format!("cargo type entry {file_name} is too short").into());
    }
    let header_magic = u32::from_le_bytes(bytes[0..4].try_into().expect("length checked"));
    let raw_name = parse_nul_terminated_utf8(bytes, 4)
        .ok_or_else(|| format!("cargo type entry {file_name} is missing a NUL-terminated name"))?;

    Ok(CargoTypeEntry {
        file_name: file_name.to_string(),
        file_size: bytes.len(),
        file_size_hex: format!("0x{:x}", bytes.len()),
        header_magic,
        header_magic_hex: format!("0x{header_magic:08x}"),
        name: parse_cargo_name_token(&raw_name),
    })
}

fn parse_nul_terminated_utf8(bytes: &[u8], offset: usize) -> Option<String> {
    let tail = bytes.get(offset..)?;
    let end = tail.iter().position(|byte| *byte == 0)?;
    String::from_utf8(tail[..end].to_vec()).ok()
}
