use std::fs;
use std::path::PathBuf;

use codspeed_criterion_compat::{BenchmarkId, Criterion, criterion_group, criterion_main};
use std::hint::black_box;

use sqpack_rs::{SqPack, SqPackHashManifestBuilder, SqPackHashVerifier};

mod common;
use common::{
    BLOCK_HEADER_SIZE, BLOCK_TYPE_UNCOMPRESSED, INDEX_ENTRY_SIZE, SQPACK_HEADER_SIZE,
    make_index_header, make_sqpack_header,
};

const SYN_ENTRY_SIZE: u32 = 256;

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 in u16");
    let uncompressed = u16::try_from(payload.len()).expect("fits in 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 synonym_sentinel() -> [u8; 256] {
    let mut e = [0u8; 256];
    e[0..8].copy_from_slice(&u64::MAX.to_le_bytes());
    e[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
    e
}

fn synonym_record(hash: u64, data: u32, conflict_index: u32, path: &str) -> [u8; 256] {
    let mut e = [0u8; 256];
    e[0..8].copy_from_slice(&hash.to_le_bytes());
    e[8..12].copy_from_slice(&data.to_le_bytes());
    e[12..16].copy_from_slice(&conflict_index.to_le_bytes());
    let path_b = path.as_bytes();
    let n = path_b.len().min(240);
    e[16..16 + n].copy_from_slice(&path_b[..n]);
    e
}

fn make_v1_index_with_synonyms(
    entries: &[(u64, u32)],
    syn_records: &[(u64, u32, u32, &str)],
) -> Vec<u8> {
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    let index_data_size = u32::try_from(entries.len()).unwrap() * INDEX_ENTRY_SIZE;
    let synonym_data_offset = index_data_offset + index_data_size;
    let synonym_records = u32::try_from(syn_records.len()).unwrap() + 1;
    let synonym_data_size = synonym_records * SYN_ENTRY_SIZE;

    let mut buf = Vec::new();
    buf.extend_from_slice(&make_sqpack_header(2));

    let mut hdr = Vec::with_capacity(1024);
    hdr.extend_from_slice(&1024u32.to_le_bytes());
    hdr.extend_from_slice(&1u32.to_le_bytes());
    hdr.extend_from_slice(&index_data_offset.to_le_bytes());
    hdr.extend_from_slice(&index_data_size.to_le_bytes());
    hdr.resize(hdr.len() + 64, 0);
    hdr.extend_from_slice(&1u32.to_le_bytes());
    hdr.extend_from_slice(&synonym_data_offset.to_le_bytes());
    hdr.extend_from_slice(&synonym_data_size.to_le_bytes());
    hdr.resize(hdr.len() + 64, 0);
    hdr.extend_from_slice(&0u32.to_le_bytes());
    hdr.extend_from_slice(&0u32.to_le_bytes());
    hdr.resize(hdr.len() + 64, 0);
    hdr.extend_from_slice(&0u32.to_le_bytes());
    hdr.extend_from_slice(&0u32.to_le_bytes());
    hdr.resize(hdr.len() + 64, 0);
    hdr.extend_from_slice(&0u32.to_le_bytes());
    hdr.resize(hdr.len() + 656, 0);
    hdr.resize(hdr.len() + 64, 0);
    assert_eq!(hdr.len(), 1024);
    buf.extend_from_slice(&hdr);

    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());
    }
    for (hash, data, ci, path) in syn_records {
        buf.extend_from_slice(&synonym_record(*hash, *data, *ci, path));
    }
    buf.extend_from_slice(&synonym_sentinel());

    buf
}

fn make_v1_index_clean(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
}

struct ArchiveSpec {
    stem: &'static str,
    entry_count: usize,
    synonyms: Vec<(u64, u32, &'static str)>,
}

fn build_verify_fixture(
    tag: &str,
    specs: &[ArchiveSpec],
) -> (PathBuf, sqpack_rs::SqPackHashManifest) {
    let root = common::unique_tempdir(tag);
    let sqpack_ffxiv = root.join("sqpack").join("ffxiv");
    fs::create_dir_all(&sqpack_ffxiv).expect("mkdir sqpack/ffxiv");

    for spec in specs {
        let mut dat = make_sqpack_header(1);
        let mut v1_entries: Vec<(u64, u32)> = Vec::new();

        for i in 0..spec.entry_count {
            let payload = format!("bench-payload-{i:04}");
            let off = append_standard_entry(&mut dat, payload.as_bytes());
            let hash = 0x0001_0000_0000_0000_u64 + u64::try_from(i).unwrap();
            v1_entries.push((hash, data_field(off)));
        }

        let mut syn_records: Vec<(u64, u32, u32, &str)> = Vec::new();
        for (parent_hash, conflict_index, path) in &spec.synonyms {
            let payload = format!("syn-payload-{path}");
            let off = append_standard_entry(&mut dat, payload.as_bytes());
            syn_records.push((*parent_hash, data_field(off), *conflict_index, path));
        }

        let mut parent_hashes_seen = std::collections::BTreeSet::new();
        for (parent_hash, _ci, _path) in &spec.synonyms {
            if parent_hashes_seen.insert(*parent_hash) {
                v1_entries.push((*parent_hash, 1u32));
            }
        }

        v1_entries.sort_by_key(|&(h, _)| h);

        let index_bytes = if syn_records.is_empty() {
            make_v1_index_clean(&v1_entries)
        } else {
            make_v1_index_with_synonyms(&v1_entries, &syn_records)
        };

        fs::write(
            sqpack_ffxiv.join(format!("{}.index", spec.stem)),
            &index_bytes,
        )
        .expect("write index");
        fs::write(sqpack_ffxiv.join(format!("{}.dat0", spec.stem)), &dat).expect("write dat");
    }

    let sq = SqPack::open(&root).expect("open for manifest build");
    let mut builder = SqPackHashManifestBuilder::new(&sq);
    let report = builder.build().expect("build manifest");
    let manifest = report.manifest;

    (root, manifest)
}

fn small_clean_fixture() -> (PathBuf, sqpack_rs::SqPackHashManifest) {
    build_verify_fixture(
        "hash_verify_small_clean",
        &[ArchiveSpec {
            stem: "0a0000.win32",
            entry_count: 100,
            synonyms: vec![],
        }],
    )
}

fn small_synonym_fixture() -> (PathBuf, sqpack_rs::SqPackHashManifest) {
    build_verify_fixture(
        "hash_verify_small_syn",
        &[ArchiveSpec {
            stem: "0a0000.win32",
            entry_count: 97,
            synonyms: vec![
                (0xDEAD_BEEF_0000_0001, 1, "exd/syn-a.exl"),
                (0xDEAD_BEEF_0000_0001, 2, "exd/syn-b.exl"),
                (0xDEAD_BEEF_0000_0001, 3, "exd/syn-c.exl"),
            ],
        }],
    )
}

fn medium_clean_fixture() -> (PathBuf, sqpack_rs::SqPackHashManifest) {
    build_verify_fixture(
        "hash_verify_medium_clean",
        &[
            ArchiveSpec {
                stem: "0a0000.win32",
                entry_count: 333,
                synonyms: vec![],
            },
            ArchiveSpec {
                stem: "020000.win32",
                entry_count: 333,
                synonyms: vec![],
            },
            ArchiveSpec {
                stem: "010000.win32",
                entry_count: 334,
                synonyms: vec![],
            },
        ],
    )
}

fn medium_synonym_fixture() -> (PathBuf, sqpack_rs::SqPackHashManifest) {
    build_verify_fixture(
        "hash_verify_medium_syn",
        &[
            ArchiveSpec {
                stem: "0a0000.win32",
                entry_count: 330,
                synonyms: vec![
                    (0xCAFE_BABE_0001_0001, 1, "exd/syn-a.exl"),
                    (0xCAFE_BABE_0001_0001, 2, "exd/syn-b.exl"),
                    (0xCAFE_BABE_0001_0001, 3, "exd/syn-c.exl"),
                ],
            },
            ArchiveSpec {
                stem: "020000.win32",
                entry_count: 330,
                synonyms: vec![
                    (0xCAFE_BABE_0002_0001, 1, "bg/syn-a.bin"),
                    (0xCAFE_BABE_0002_0001, 2, "bg/syn-b.bin"),
                    (0xCAFE_BABE_0002_0001, 3, "bg/syn-c.bin"),
                ],
            },
            ArchiveSpec {
                stem: "010000.win32",
                entry_count: 331,
                synonyms: vec![
                    (0xCAFE_BABE_0003_0001, 1, "bgcommon/syn-a.mdl"),
                    (0xCAFE_BABE_0003_0001, 2, "bgcommon/syn-b.mdl"),
                    (0xCAFE_BABE_0003_0001, 3, "bgcommon/syn-c.mdl"),
                ],
            },
        ],
    )
}

fn bench_hash_verify_eager(c: &mut Criterion) {
    let (small_clean_root, small_clean_manifest) = small_clean_fixture();
    let (small_syn_root, small_syn_manifest) = small_synonym_fixture();
    let (medium_clean_root, medium_clean_manifest) = medium_clean_fixture();
    let (medium_syn_root, medium_syn_manifest) = medium_synonym_fixture();

    let mut group = c.benchmark_group("sqpack.hash_verify.eager");

    group.bench_with_input(
        BenchmarkId::from_parameter("small_clean"),
        &(&small_clean_root, &small_clean_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let report = verifier.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("small_with_synonyms"),
        &(&small_syn_root, &small_syn_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let report = verifier.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("medium_clean"),
        &(&medium_clean_root, &medium_clean_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let report = verifier.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("medium_with_synonyms"),
        &(&medium_syn_root, &medium_syn_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let report = verifier.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.finish();

    let _ = fs::remove_dir_all(&small_clean_root);
    let _ = fs::remove_dir_all(&small_syn_root);
    let _ = fs::remove_dir_all(&medium_clean_root);
    let _ = fs::remove_dir_all(&medium_syn_root);
}

fn bench_hash_verify_streaming(c: &mut Criterion) {
    let (small_clean_root, small_clean_manifest) = small_clean_fixture();
    let (small_syn_root, small_syn_manifest) = small_synonym_fixture();
    let (medium_clean_root, medium_clean_manifest) = medium_clean_fixture();
    let (medium_syn_root, medium_syn_manifest) = medium_synonym_fixture();

    let mut group = c.benchmark_group("sqpack.hash_verify.streaming");

    group.bench_with_input(
        BenchmarkId::from_parameter("small_clean"),
        &(&small_clean_root, &small_clean_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let events: Vec<_> = verifier.verify_streaming(black_box(manifest)).collect();
                black_box(events);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("small_with_synonyms"),
        &(&small_syn_root, &small_syn_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let events: Vec<_> = verifier.verify_streaming(black_box(manifest)).collect();
                black_box(events);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("medium_clean"),
        &(&medium_clean_root, &medium_clean_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let events: Vec<_> = verifier.verify_streaming(black_box(manifest)).collect();
                black_box(events);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("medium_with_synonyms"),
        &(&medium_syn_root, &medium_syn_manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut verifier = SqPackHashVerifier::new(&sq);
                let events: Vec<_> = verifier.verify_streaming(black_box(manifest)).collect();
                black_box(events);
            });
        },
    );

    group.finish();

    let _ = fs::remove_dir_all(&small_clean_root);
    let _ = fs::remove_dir_all(&small_syn_root);
    let _ = fs::remove_dir_all(&medium_clean_root);
    let _ = fs::remove_dir_all(&medium_syn_root);
}

criterion_group!(
    benches,
    bench_hash_verify_eager,
    bench_hash_verify_streaming
);
criterion_main!(benches);
