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

use serde::{Deserialize, Serialize};

pub const PK4_MAGIC: u32 = 0x0000_03eb;
pub const PK4_DIRECTORY_ENTRY_STRIDE: usize = 0x4a;
pub const PK4_DIRECTORY_METADATA_LEN: usize = 13;
pub const PK4_DIRECTORY_NAME_LEN: usize = PK4_DIRECTORY_ENTRY_STRIDE - PK4_DIRECTORY_METADATA_LEN;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pk4Entry {
    pub index: usize,
    pub directory_offset: usize,
    pub directory_offset_hex: String,
    pub crc32: u32,
    pub crc32_hex: String,
    pub payload_len: u32,
    pub payload_len_hex: String,
    pub payload_offset: u32,
    pub payload_offset_hex: String,
    pub payload_absolute_offset: usize,
    pub payload_absolute_offset_hex: String,
    pub payload_end_offset: usize,
    pub payload_end_offset_hex: String,
    pub flag: u8,
    pub flag_hex: String,
    pub extension: Option<String>,
    pub payload_signature_ascii: String,
    pub payload_signature_hex: String,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pk4InspectionReport {
    pub magic: u32,
    pub magic_hex: String,
    pub entry_count: usize,
    pub directory_entry_stride: usize,
    pub directory_len: usize,
    pub directory_len_hex: String,
    pub payload_base_offset: usize,
    pub payload_base_offset_hex: String,
    pub file_size: usize,
    pub payloads_are_contiguous: bool,
    pub notes: Vec<String>,
    pub entries: Vec<Pk4Entry>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pk4ExtractionReport {
    pub matched_entry_name: String,
    pub case_insensitive_match: bool,
    pub extracted_len: usize,
    pub extracted_len_hex: String,
    pub entry: Pk4Entry,
}

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

pub fn inspect_pk4_bytes(bytes: &[u8]) -> Result<Pk4InspectionReport, Box<dyn std::error::Error>> {
    let magic = read_u32_le(bytes, 0).ok_or("truncated pk4 magic")?;
    let entry_count = read_u32_le(bytes, 4).ok_or("truncated pk4 entry count")? as usize;
    let directory_len = entry_count
        .checked_mul(PK4_DIRECTORY_ENTRY_STRIDE)
        .ok_or("pk4 directory length overflow")?;
    let payload_base_offset = 8usize
        .checked_add(directory_len)
        .ok_or("pk4 payload base overflow")?;
    if payload_base_offset > bytes.len() {
        return Err(format!(
            "pk4 directory extends past end of file: payload base 0x{payload_base_offset:x}, file size 0x{:x}",
            bytes.len()
        )
        .into());
    }

    let mut entries = Vec::with_capacity(entry_count);
    for index in 0..entry_count {
        let directory_offset = 8 + index * PK4_DIRECTORY_ENTRY_STRIDE;
        let directory_entry = bytes
            .get(directory_offset..directory_offset + PK4_DIRECTORY_ENTRY_STRIDE)
            .ok_or_else(|| {
                format!(
                    "truncated pk4 directory entry {} at offset 0x{directory_offset:x}",
                    index
                )
            })?;

        let crc32 = read_u32_le(directory_entry, 0).ok_or("truncated pk4 entry crc32")?;
        let payload_len = read_u32_le(directory_entry, 4).ok_or("truncated pk4 entry length")?;
        let payload_offset =
            read_u32_le(directory_entry, 8).ok_or("truncated pk4 entry payload offset")?;
        let flag = directory_entry[12];
        let name = parse_name(&directory_entry[13..])?;
        let payload_absolute_offset = payload_base_offset
            .checked_add(payload_offset as usize)
            .ok_or_else(|| format!("pk4 payload offset overflow for entry {name}"))?;
        let payload_end_offset = payload_absolute_offset
            .checked_add(payload_len as usize)
            .ok_or_else(|| format!("pk4 payload end overflow for entry {name}"))?;
        let payload = bytes.get(payload_absolute_offset..payload_end_offset).ok_or_else(|| {
            format!(
                "pk4 payload for entry {name} extends past end of file: 0x{payload_absolute_offset:x}..0x{payload_end_offset:x} > 0x{:x}",
                bytes.len()
            )
        })?;

        entries.push(Pk4Entry {
            index,
            directory_offset,
            directory_offset_hex: format!("0x{directory_offset:04x}"),
            crc32,
            crc32_hex: format!("0x{crc32:08x}"),
            payload_len,
            payload_len_hex: format!("0x{payload_len:08x}"),
            payload_offset,
            payload_offset_hex: format!("0x{payload_offset:08x}"),
            payload_absolute_offset,
            payload_absolute_offset_hex: format!("0x{payload_absolute_offset:08x}"),
            payload_end_offset,
            payload_end_offset_hex: format!("0x{payload_end_offset:08x}"),
            flag,
            flag_hex: format!("0x{flag:02x}"),
            extension: Path::new(&name)
                .extension()
                .and_then(|extension| extension.to_str())
                .map(|extension| extension.to_ascii_lowercase()),
            payload_signature_ascii: ascii_preview(payload, 8),
            payload_signature_hex: hex_preview(payload, 8),
            name,
        });
    }

    let payloads_are_contiguous = entries
        .windows(2)
        .all(|window| window[0].payload_end_offset == window[1].payload_absolute_offset);

    let mut notes = Vec::new();
    if magic == PK4_MAGIC {
        notes.push(
            "Header magic matches the observed RT3 pack4 container family (0x03eb).".to_string(),
        );
    } else {
        notes.push(format!(
            "Header magic 0x{magic:08x} differs from the observed RT3 pack4 container family 0x{PK4_MAGIC:08x}."
        ));
    }
    notes.push(format!(
        "Payload base is derived as 8 + entry_count * 0x{PK4_DIRECTORY_ENTRY_STRIDE:02x}."
    ));
    if payloads_are_contiguous {
        notes.push(
            "Entry payload offsets form one contiguous packed data region in directory order."
                .to_string(),
        );
    }

    Ok(Pk4InspectionReport {
        magic,
        magic_hex: format!("0x{magic:08x}"),
        entry_count,
        directory_entry_stride: PK4_DIRECTORY_ENTRY_STRIDE,
        directory_len,
        directory_len_hex: format!("0x{directory_len:08x}"),
        payload_base_offset,
        payload_base_offset_hex: format!("0x{payload_base_offset:08x}"),
        file_size: bytes.len(),
        payloads_are_contiguous,
        notes,
        entries,
    })
}

pub fn extract_pk4_entry_file(
    pk4_path: &Path,
    entry_name: &str,
    output_path: &Path,
) -> Result<Pk4ExtractionReport, Box<dyn std::error::Error>> {
    let bytes = fs::read(pk4_path)?;
    let (report, payload) = extract_pk4_entry_bytes(&bytes, entry_name)?;
    fs::write(output_path, payload)?;
    Ok(report)
}

pub fn extract_pk4_entry_bytes(
    bytes: &[u8],
    entry_name: &str,
) -> Result<(Pk4ExtractionReport, Vec<u8>), Box<dyn std::error::Error>> {
    let inspection = inspect_pk4_bytes(bytes)?;
    let (entry, case_insensitive_match) = find_entry(&inspection.entries, entry_name)
        .ok_or_else(|| format!("pk4 entry not found: {entry_name}"))?;
    let payload = bytes[entry.payload_absolute_offset..entry.payload_end_offset].to_vec();
    let report = Pk4ExtractionReport {
        matched_entry_name: entry.name.clone(),
        case_insensitive_match,
        extracted_len: payload.len(),
        extracted_len_hex: format!("0x{:08x}", payload.len()),
        entry: entry.clone(),
    };
    Ok((report, payload))
}

fn find_entry<'a>(entries: &'a [Pk4Entry], requested_name: &str) -> Option<(&'a Pk4Entry, bool)> {
    if let Some(entry) = entries.iter().find(|entry| entry.name == requested_name) {
        return Some((entry, false));
    }

    let requested_lower = requested_name.to_ascii_lowercase();
    let mut matches = entries
        .iter()
        .filter(|entry| entry.name.to_ascii_lowercase() == requested_lower);
    let first = matches.next()?;
    if matches.next().is_some() {
        return None;
    }
    Some((first, true))
}

fn parse_name(bytes: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
    let raw = bytes
        .split(|byte| *byte == 0)
        .next()
        .ok_or("missing pk4 entry name")?;
    if raw.is_empty() {
        return Err("empty pk4 entry name".into());
    }
    Ok(String::from_utf8(raw.to_vec())?)
}

fn ascii_preview(bytes: &[u8], limit: usize) -> String {
    bytes
        .iter()
        .take(limit)
        .map(|byte| match byte {
            b' '..=b'~' => *byte as char,
            _ => '.',
        })
        .collect()
}

fn hex_preview(bytes: &[u8], limit: usize) -> String {
    let mut output = String::new();
    for byte in bytes.iter().take(limit) {
        output.push_str(&format!("{byte:02x}"));
    }
    output
}

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::*;

    fn build_entry(
        crc32: u32,
        payload_len: u32,
        payload_offset: u32,
        name: &str,
    ) -> [u8; PK4_DIRECTORY_ENTRY_STRIDE] {
        let mut entry = [0u8; PK4_DIRECTORY_ENTRY_STRIDE];
        entry[0..4].copy_from_slice(&crc32.to_le_bytes());
        entry[4..8].copy_from_slice(&payload_len.to_le_bytes());
        entry[8..12].copy_from_slice(&payload_offset.to_le_bytes());
        let name_bytes = name.as_bytes();
        entry[13..13 + name_bytes.len()].copy_from_slice(name_bytes);
        entry
    }

    #[test]
    fn inspects_synthetic_pk4_bytes() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PK4_MAGIC.to_le_bytes());
        bytes.extend_from_slice(&(2u32).to_le_bytes());
        bytes.extend_from_slice(&build_entry(0x11223344, 5, 0, "alpha.txt"));
        bytes.extend_from_slice(&build_entry(0x55667788, 4, 5, "beta.dds"));
        bytes.extend_from_slice(b"helloDDS!");

        let report = inspect_pk4_bytes(&bytes).expect("pk4 inspection should succeed");
        assert_eq!(report.entry_count, 2);
        assert_eq!(
            report.payload_base_offset,
            8 + 2 * PK4_DIRECTORY_ENTRY_STRIDE
        );
        assert!(report.payloads_are_contiguous);
        assert_eq!(report.entries[0].name, "alpha.txt");
        assert_eq!(report.entries[0].payload_signature_ascii, "hello");
        assert_eq!(report.entries[1].name, "beta.dds");
        assert_eq!(report.entries[1].payload_signature_ascii, "DDS!");
    }

    #[test]
    fn extracts_case_insensitive_entry_match() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PK4_MAGIC.to_le_bytes());
        bytes.extend_from_slice(&(1u32).to_le_bytes());
        bytes.extend_from_slice(&build_entry(0x11223344, 5, 0, "Campaign.win"));
        bytes.extend_from_slice(b"HELLO");

        let (report, payload) =
            extract_pk4_entry_bytes(&bytes, "campaign.win").expect("pk4 extraction should succeed");
        assert!(report.case_insensitive_match);
        assert_eq!(report.matched_entry_name, "Campaign.win");
        assert_eq!(payload, b"HELLO");
    }
}
