#![cfg(all(feature = "mmap", feature = "parallel"))]

use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use sqpack_rs::hash::hash_index1_key;
use sqpack_rs::{
    MmapFs, SqPack, SqPackHashManifestBuilder, SqPackHashVerifier, SqPackPathManifestBuilder,
    SqPackPathVerifier, StdFs, Vfs,
};

const SQPACK_HEADER_SIZE: u32 = 1024;
const INDEX_ENTRY_SIZE: u32 = 16;
const BLOCK_HEADER_SIZE: u32 = 16;
const BLOCK_TYPE_UNCOMPRESSED: u32 = 32_000;
const FIXTURE_ENTRIES: usize = 48;
const ARCHIVE_STEM: &str = "0a0000.win32";

const SQPACK_MAGIC: [u8; 8] = [b'S', b'q', b'P', b'a', b'c', b'k', 0, 0];

fn make_sqpack_header(file_type: u32) -> Vec<u8> {
    let mut buf = Vec::with_capacity(SQPACK_HEADER_SIZE as usize);
    buf.extend_from_slice(&SQPACK_MAGIC);
    buf.push(0);
    buf.extend_from_slice(&[0, 0, 0]);
    buf.extend_from_slice(&SQPACK_HEADER_SIZE.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    buf.extend_from_slice(&file_type.to_le_bytes());
    buf.resize(SQPACK_HEADER_SIZE as usize, 0);
    buf
}

fn make_index_header(
    index_data_offset: u32,
    index_data_size: u32,
    number_of_data_file: u32,
) -> Vec<u8> {
    let mut buf = Vec::with_capacity(1024);
    buf.extend_from_slice(&1024u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    buf.extend_from_slice(&index_data_offset.to_le_bytes());
    buf.extend_from_slice(&index_data_size.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&number_of_data_file.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 656, 0);
    buf.resize(buf.len() + 64, 0);
    assert_eq!(buf.len(), 1024);
    buf
}

fn append_standard_entry(buf: &mut Vec<u8>, payload: &[u8]) -> u64 {
    let aligned = (buf.len() + 127) & !127;
    buf.resize(aligned, 0);
    let info_off = u64::try_from(buf.len()).unwrap();

    let raw_file_size = u32::try_from(payload.len()).unwrap();
    buf.extend_from_slice(&32u32.to_le_bytes());
    buf.extend_from_slice(&2u32.to_le_bytes());
    buf.extend_from_slice(&raw_file_size.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());

    let block_total = u16::try_from(BLOCK_HEADER_SIZE as usize + payload.len()).expect("fits u16");
    let uncompressed = u16::try_from(payload.len()).expect("fits u16");
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&block_total.to_le_bytes());
    buf.extend_from_slice(&uncompressed.to_le_bytes());

    buf.extend_from_slice(&BLOCK_HEADER_SIZE.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&BLOCK_TYPE_UNCOMPRESSED.to_le_bytes());
    buf.extend_from_slice(&u32::from(uncompressed).to_le_bytes());
    buf.extend_from_slice(payload);

    info_off
}

fn data_field(info_off: u64) -> u32 {
    u32::try_from(info_off / 8).expect("fits in u32")
}

fn make_v1_index(entries: &[(u64, u32)]) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&make_sqpack_header(2));
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    let index_data_size = u32::try_from(entries.len()).unwrap() * INDEX_ENTRY_SIZE;
    buf.extend_from_slice(&make_index_header(index_data_offset, index_data_size, 1));
    for (hash, data) in entries {
        buf.extend_from_slice(&hash.to_le_bytes());
        buf.extend_from_slice(&data.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
    }
    buf
}

fn unique_tempdir(tag: &str) -> PathBuf {
    let path = std::env::temp_dir().join(format!(
        "sqpack-rs-test-{tag}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_or(0, |d| d.as_nanos())
    ));
    let _ = fs::remove_dir_all(&path);
    path
}

fn build_fixture() -> (PathBuf, Vec<String>) {
    let root = unique_tempdir("mmap_parallel");
    let sqpack_ffxiv = root.join("sqpack").join("ffxiv");
    fs::create_dir_all(&sqpack_ffxiv).expect("mkdir sqpack/ffxiv");

    let mut dat = make_sqpack_header(1);
    let mut entries: Vec<(u64, u32)> = Vec::with_capacity(FIXTURE_ENTRIES);
    let mut paths: Vec<String> = Vec::with_capacity(FIXTURE_ENTRIES);

    for i in 0..FIXTURE_ENTRIES {
        let path = format!("exd/mmap_par_{i:04}.dat");
        let payload: Vec<u8> = (0..(96 + (i % 8) * 16))
            .map(|j| u8::try_from((i * 7 + j) & 0xff).expect("masked"))
            .collect();
        let off = append_standard_entry(&mut dat, &payload);
        entries.push((hash_index1_key(&path), data_field(off)));
        paths.push(path);
    }
    entries.sort_by_key(|&(h, _)| h);

    let index_bytes = make_v1_index(&entries);
    fs::write(
        sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.index")),
        &index_bytes,
    )
    .expect("write index");
    fs::write(sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.dat0")), &dat).expect("write dat");

    (root, paths)
}

fn cleanup(root: &Path) {
    let _ = fs::remove_dir_all(root);
}

fn open_with<V: Vfs + Send + Sync + 'static>(root: &Path, vfs: V) -> SqPack {
    SqPack::open_with_vfs(root, Arc::new(vfs)).expect("open")
}

#[test]
fn enumerate_archives_parallel_mmap_matches_std() {
    let (root, _paths) = build_fixture();
    let sq_std = open_with(&root, StdFs::new());
    let sq_mmap = open_with(&root, MmapFs::new());

    let std_archives = sq_std.enumerate_archives_parallel();
    let mmap_archives = sq_mmap.enumerate_archives_parallel();
    assert_eq!(std_archives.len(), mmap_archives.len());
    for (s, m) in std_archives.iter().zip(mmap_archives.iter()) {
        let s = s.as_ref().expect("std archive ok");
        let m = m.as_ref().expect("mmap archive ok");
        assert_eq!(s.triple(), m.triple(), "triple mismatch across backends");
        assert_eq!(s.len(), m.len(), "entry-count mismatch across backends");
    }
    cleanup(&root);
}

#[test]
fn verify_parallel_mmap_matches_std() {
    let (root, paths) = build_fixture();
    let sq_std = open_with(&root, StdFs::new());
    let sq_mmap = open_with(&root, MmapFs::new());

    let manifest = {
        let mut builder = SqPackPathManifestBuilder::new(&sq_std);
        for p in &paths {
            builder.add(p).expect("add");
        }
        builder.finish()
    };

    let std_report = SqPackPathVerifier::new(&sq_std).verify_parallel(&manifest);
    let mmap_report = SqPackPathVerifier::new(&sq_mmap).verify_parallel(&manifest);
    assert_eq!(
        std_report, mmap_report,
        "verify_parallel report must be byte-for-byte identical across StdFs and MmapFs",
    );
    assert!(std_report.is_clean());
    assert_eq!(std_report.verified_count, FIXTURE_ENTRIES);
    cleanup(&root);
}

#[test]
fn hash_verify_parallel_mmap_matches_std() {
    let (root, _paths) = build_fixture();
    let sq_std = open_with(&root, StdFs::new());
    let sq_mmap = open_with(&root, MmapFs::new());

    let manifest = SqPackHashManifestBuilder::new(&sq_std)
        .build()
        .expect("build manifest")
        .manifest;

    let std_report = SqPackHashVerifier::new(&sq_std).verify_parallel(&manifest);
    let mmap_report = SqPackHashVerifier::new(&sq_mmap).verify_parallel(&manifest);
    assert_eq!(
        std_report, mmap_report,
        "hash_verify_parallel report must be byte-for-byte identical across StdFs and MmapFs",
    );
    assert_eq!(std_report.verified_count, FIXTURE_ENTRIES);
    cleanup(&root);
}

#[test]
fn one_mapping_per_dat_across_workers() {
    let (root, _paths) = build_fixture();
    let mmap = MmapFs::new();
    let sq_mmap = open_with(&root, mmap.clone());

    let _ = sq_mmap.enumerate_archives_parallel();
    let len = mmap.len();
    assert!(
        len > 0,
        "parallel enumerate must have populated the mmap cache",
    );
    assert!(
        len <= 6,
        "mmap cache must stay bounded by file count, got {len}",
    );
    cleanup(&root);
}
