use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;

use binrw::BinRead;

use crate::error::{Error, Result};
#[cfg(test)]
use crate::header::SQPACK_HEADER_SIZE;
use crate::header::{DatLocation, SqPackHeader, SqPackIndexHeader, check_data_region_fits_stream};
use crate::index::{INDEX_ENTRY_SIZE, IndexEntry};
use crate::vfs::Vfs;
use crate::write::sha1::{pad_to_64, sha1};

/// Encodes a [`DatLocation`] into the 32-bit `data` field of a `.index` entry.
///
/// The encoding is `(byte_offset / 8) | (data_file_id << 1) | synonym_bit`.
/// Returns an error if `data_file_id > 7` (3-bit field), if `byte_offset` is not
/// 8-byte-aligned, or if the resulting value would overflow a `u32`.
pub(crate) fn pack_index_data(synonym: bool, location: DatLocation) -> Result<u32> {
    let DatLocation {
        data_file_id,
        byte_offset,
    } = location;
    if data_file_id > 7 {
        return Err(Error::InvalidBlockLayout {
            context: "index_entry.data_file_id",
            message: format!("data_file_id {data_file_id} > 7 (3-bit field)"),
        });
    }
    if byte_offset % 8 != 0 {
        return Err(Error::InvalidBlockLayout {
            context: "index_entry.byte_offset",
            message: format!("byte_offset {byte_offset} is not 8-byte aligned"),
        });
    }
    let raw_offset = byte_offset / 8;
    let raw_offset_u32 = u32::try_from(raw_offset).map_err(|_| Error::InvalidBlockLayout {
        context: "index_entry.byte_offset",
        message: format!("byte_offset/8 = {raw_offset} exceeds u32::MAX"),
    })?;
    if raw_offset_u32 & 0xF != 0 {
        return Err(Error::InvalidBlockLayout {
            context: "index_entry.byte_offset",
            message: format!(
                "byte_offset {byte_offset} → low-nibble of byte_offset/8 ({raw_offset_u32:#x}) is non-zero"
            ),
        });
    }
    let synonym_bit = u32::from(synonym);
    let file_id_bits = u32::from(data_file_id & 0b111) << 1;
    Ok(raw_offset_u32 | file_id_bits | synonym_bit)
}

/// Parsed representation of a `.index` file, including the raw bytes of auxiliary regions
/// that are round-tripped verbatim during serialisation.
#[derive(Debug)]
pub(crate) struct ParsedIndex {
    /// The outer `SqPack` file header.
    pub header: SqPackHeader,
    /// The index-specific header containing region offsets and SHA-1 checksums.
    pub index_header: SqPackIndexHeader,
    /// All hash-table entries, loaded in on-disk order.
    pub entries: Vec<IndexEntry>,
    /// Raw bytes of the synonym region, preserved unchanged during round-trips.
    pub synonym_bytes: Vec<u8>,
    /// Raw bytes of the empty-block region, preserved unchanged during round-trips.
    pub empty_block_bytes: Vec<u8>,
    /// Raw bytes of the directory-index region, preserved unchanged during round-trips.
    pub dir_index_bytes: Vec<u8>,
}

/// Reads and fully parses a `.index` file into a [`ParsedIndex`].
pub(crate) fn read_index_file(vfs: &dyn Vfs, path: &Path) -> Result<ParsedIndex> {
    let handle = vfs.open_read(path).map_err(|e| Error::io_at(path, e))?;
    let mut reader = BufReader::new(handle);
    read_index_inner(&mut reader)
}

fn read_index_inner<R: Read + Seek>(reader: &mut R) -> Result<ParsedIndex> {
    let header = SqPackHeader::parse(reader, crate::header::SqPackFileType::Index)?;
    if header.size < 24 {
        return Err(Error::InvalidHeaderSize {
            field: "sqpack_header.size",
            value: header.size,
            minimum: 24,
        });
    }
    reader.seek(SeekFrom::Start(u64::from(header.size)))?;
    let index_header = SqPackIndexHeader::parse(reader)?;

    if index_header.index_data_size % INDEX_ENTRY_SIZE != 0 {
        return Err(Error::MisalignedIndexData {
            size: index_header.index_data_size,
            entry_size: INDEX_ENTRY_SIZE,
        });
    }
    check_data_region_fits_stream(
        reader,
        index_header.index_data_offset,
        index_header.index_data_size,
    )?;

    let entry_count = (index_header.index_data_size / INDEX_ENTRY_SIZE) as usize;
    reader.seek(SeekFrom::Start(u64::from(index_header.index_data_offset)))?;
    let mut entries = Vec::with_capacity(entry_count);
    for _ in 0..entry_count {
        let e = IndexEntry::read(reader).map_err(|e| Error::decode("index_entry", &e))?;
        entries.push(e);
    }

    let synonym_bytes = read_region(
        reader,
        index_header.synonym_data_offset,
        index_header.synonym_data_size,
    )?;
    let empty_block_bytes = read_region(
        reader,
        index_header.empty_block_data_offset,
        index_header.empty_block_data_size,
    )?;
    let dir_index_bytes = read_region(
        reader,
        index_header.dir_index_data_offset,
        index_header.dir_index_data_size,
    )?;

    Ok(ParsedIndex {
        header,
        index_header,
        entries,
        synonym_bytes,
        empty_block_bytes,
        dir_index_bytes,
    })
}

fn read_region<R: Read + Seek>(reader: &mut R, offset: u32, size: u32) -> Result<Vec<u8>> {
    if size == 0 {
        return Ok(Vec::new());
    }
    reader.seek(SeekFrom::Start(u64::from(offset)))?;
    let mut buf = vec![0u8; size as usize];
    reader.read_exact(&mut buf)?;
    Ok(buf)
}

#[cfg(test)]
#[derive(Debug)]
pub(crate) struct InsertResult {
    pub bytes: Vec<u8>,
}

#[cfg(test)]
pub(crate) fn insert_entry(
    parsed: &ParsedIndex,
    new_entry: IndexEntry,
    game_path: &str,
) -> Result<InsertResult> {
    if parsed.entries.iter().any(|e| e.hash == new_entry.hash) {
        return Err(Error::FileAlreadyExists {
            game_path: game_path.to_string(),
        });
    }

    let mut entries = Vec::with_capacity(parsed.entries.len() + 1);
    entries.extend_from_slice(&parsed.entries);
    entries.push(new_entry);
    entries.sort_by_key(|e| e.hash);

    Ok(InsertResult {
        bytes: serialise_index(parsed, &entries)?,
    })
}

/// Re-serialises a [`ParsedIndex`] with its current entry list, recomputing all SHA-1 hashes.
///
/// Equivalent to inserting zero new entries; used after the caller has mutated
/// `parsed.entries` in place (e.g. to append pending entries from staging).
pub(crate) fn serialise_via_insert(parsed: &ParsedIndex) -> Result<Vec<u8>> {
    serialise_index(parsed, &parsed.entries)
}

#[allow(clippy::too_many_lines)]
fn serialise_index(parsed: &ParsedIndex, entries: &[IndexEntry]) -> Result<Vec<u8>> {
    let new_index_data_size = u32::try_from(entries.len())
        .map_err(|_| Error::InvalidBlockLayout {
            context: "index.entry_count",
            message: format!("entry count {} exceeds u32::MAX", entries.len()),
        })?
        .checked_mul(INDEX_ENTRY_SIZE)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index.index_data_size",
            message: format!("entry_count × {INDEX_ENTRY_SIZE} overflows u32"),
        })?;

    let index_data_offset = parsed
        .header
        .size
        .checked_add(parsed.index_header.size)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index.index_data_offset",
            message: "header.size + index_header.size overflows u32".to_string(),
        })?;
    let synonym_data_offset = index_data_offset
        .checked_add(new_index_data_size)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index.synonym_data_offset",
            message: "index_data_offset + index_data_size overflows u32".to_string(),
        })?;
    let synonym_data_size =
        u32::try_from(parsed.synonym_bytes.len()).map_err(|_| Error::InvalidBlockLayout {
            context: "index.synonym_data_size",
            message: format!(
                "synonym bytes length {} exceeds u32::MAX",
                parsed.synonym_bytes.len()
            ),
        })?;
    let empty_block_data_offset = synonym_data_offset
        .checked_add(synonym_data_size)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index.empty_block_data_offset",
            message: "synonym_data_offset + synonym_data_size overflows u32".to_string(),
        })?;
    let empty_block_data_size =
        u32::try_from(parsed.empty_block_bytes.len()).map_err(|_| Error::InvalidBlockLayout {
            context: "index.empty_block_data_size",
            message: format!(
                "empty block bytes length {} exceeds u32::MAX",
                parsed.empty_block_bytes.len()
            ),
        })?;
    let dir_index_data_offset = empty_block_data_offset
        .checked_add(empty_block_data_size)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index.dir_index_data_offset",
            message: "empty_block_data_offset + empty_block_data_size overflows u32".to_string(),
        })?;
    let dir_index_data_size =
        u32::try_from(parsed.dir_index_bytes.len()).map_err(|_| Error::InvalidBlockLayout {
            context: "index.dir_index_data_size",
            message: format!(
                "dir index bytes length {} exceeds u32::MAX",
                parsed.dir_index_bytes.len()
            ),
        })?;

    let index_data_bytes = serialise_hash_table(entries);
    let index_data_hash = pad_to_64(sha1(&index_data_bytes));
    let synonym_data_hash = pad_to_64(sha1(&parsed.synonym_bytes));
    let empty_block_data_hash = pad_to_64(sha1(&parsed.empty_block_bytes));
    let dir_index_data_hash = pad_to_64(sha1(&parsed.dir_index_bytes));

    let mut header_bytes = Vec::with_capacity(parsed.index_header.size as usize);
    header_bytes.extend_from_slice(&parsed.index_header.size.to_le_bytes());
    header_bytes.extend_from_slice(&parsed.index_header.version.to_le_bytes());
    header_bytes.extend_from_slice(&index_data_offset.to_le_bytes());
    header_bytes.extend_from_slice(&new_index_data_size.to_le_bytes());
    header_bytes.extend_from_slice(&index_data_hash);
    header_bytes.extend_from_slice(&parsed.index_header.number_of_data_file.to_le_bytes());
    header_bytes.extend_from_slice(&synonym_data_offset.to_le_bytes());
    header_bytes.extend_from_slice(&synonym_data_size.to_le_bytes());
    header_bytes.extend_from_slice(&synonym_data_hash);
    header_bytes.extend_from_slice(&empty_block_data_offset.to_le_bytes());
    header_bytes.extend_from_slice(&empty_block_data_size.to_le_bytes());
    header_bytes.extend_from_slice(&empty_block_data_hash);
    header_bytes.extend_from_slice(&dir_index_data_offset.to_le_bytes());
    header_bytes.extend_from_slice(&dir_index_data_size.to_le_bytes());
    header_bytes.extend_from_slice(&dir_index_data_hash);
    header_bytes.extend_from_slice(&parsed.index_header.index_type.to_le_bytes());
    header_bytes.extend_from_slice(&parsed.index_header.reserved);
    let self_hash = pad_to_64(sha1(&header_bytes));
    header_bytes.extend_from_slice(&self_hash);
    debug_assert_eq!(
        header_bytes.len(),
        parsed.index_header.size as usize,
        "writer assumes the shipped 1024-byte index header layout; a layout change must update the field-emission sequence"
    );

    let mut sqpack_header_bytes = Vec::with_capacity(parsed.header.size as usize);
    sqpack_header_bytes.extend_from_slice(&parsed.header.magic);
    sqpack_header_bytes.push(parsed.header.platform_id);
    sqpack_header_bytes.extend_from_slice(&parsed.header.unknown);
    sqpack_header_bytes.extend_from_slice(&parsed.header.size.to_le_bytes());
    sqpack_header_bytes.extend_from_slice(&parsed.header.version.to_le_bytes());
    sqpack_header_bytes.extend_from_slice(&parsed.header.file_type.to_le_bytes());
    if sqpack_header_bytes.len() < parsed.header.size as usize {
        sqpack_header_bytes.resize(parsed.header.size as usize, 0);
    }

    let total_size = (index_data_offset as usize)
        + index_data_bytes.len()
        + parsed.synonym_bytes.len()
        + parsed.empty_block_bytes.len()
        + parsed.dir_index_bytes.len();
    let mut out = Vec::with_capacity(total_size);
    out.extend_from_slice(&sqpack_header_bytes);
    out.extend_from_slice(&header_bytes);
    debug_assert_eq!(out.len(), index_data_offset as usize);
    out.extend_from_slice(&index_data_bytes);
    out.extend_from_slice(&parsed.synonym_bytes);
    out.extend_from_slice(&parsed.empty_block_bytes);
    out.extend_from_slice(&parsed.dir_index_bytes);

    Ok(out)
}

fn serialise_hash_table(entries: &[IndexEntry]) -> Vec<u8> {
    let mut out = Vec::with_capacity(entries.len() * INDEX_ENTRY_SIZE as usize);
    for e in entries {
        out.extend_from_slice(&e.hash.to_le_bytes());
        out.extend_from_slice(&e.data.to_le_bytes());
        out.extend_from_slice(&e.padding.to_le_bytes());
    }
    out
}

#[cfg(test)]
pub(crate) fn build_empty_index(number_of_data_file: u32) -> Vec<u8> {
    use crate::header::SQPACK_MAGIC;
    let mut out = Vec::new();
    out.extend_from_slice(&SQPACK_MAGIC);
    out.push(0);
    out.extend_from_slice(&[0u8; 3]);
    out.extend_from_slice(&SQPACK_HEADER_SIZE.to_le_bytes());
    out.extend_from_slice(&1u32.to_le_bytes());
    out.extend_from_slice(&2u32.to_le_bytes());
    out.resize(SQPACK_HEADER_SIZE as usize, 0);
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    out.extend_from_slice(&1024u32.to_le_bytes());
    out.extend_from_slice(&1u32.to_le_bytes());
    out.extend_from_slice(&index_data_offset.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.resize(out.len() + 64, 0);
    out.extend_from_slice(&number_of_data_file.to_le_bytes());
    out.extend_from_slice(&index_data_offset.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.resize(out.len() + 64, 0);
    out.extend_from_slice(&index_data_offset.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.resize(out.len() + 64, 0);
    out.extend_from_slice(&index_data_offset.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.resize(out.len() + 64, 0);
    out.extend_from_slice(&0u32.to_le_bytes());
    out.resize(out.len() + 656, 0);
    out.resize(out.len() + 64, 0);
    debug_assert_eq!(out.len(), index_data_offset as usize);
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::index::Index;
    use crate::vfs::InMemoryFs;
    use std::path::PathBuf;

    #[test]
    fn pack_index_data_round_trips_basic() {
        let data = pack_index_data(
            false,
            DatLocation {
                data_file_id: 0,
                byte_offset: 0x100,
            },
        )
        .unwrap();
        assert_eq!(data, 0x20);
    }

    #[test]
    fn pack_index_data_encodes_data_file_id() {
        let data = pack_index_data(
            false,
            DatLocation {
                data_file_id: 3,
                byte_offset: 0x200,
            },
        )
        .unwrap();
        assert_eq!(data, 0x40 | 0x6);
    }

    #[test]
    fn pack_index_data_rejects_misaligned_offset() {
        let err = pack_index_data(
            false,
            DatLocation {
                data_file_id: 0,
                byte_offset: 0x123,
            },
        )
        .unwrap_err();
        assert!(matches!(err, Error::InvalidBlockLayout { .. }));
    }

    #[test]
    fn pack_index_data_rejects_data_file_id_overflow() {
        let err = pack_index_data(
            false,
            DatLocation {
                data_file_id: 8,
                byte_offset: 0x100,
            },
        )
        .unwrap_err();
        assert!(matches!(err, Error::InvalidBlockLayout { .. }));
    }

    #[test]
    fn build_empty_index_parses_via_reader() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let idx = Index::open_in(&path, &fs).unwrap();
        assert_eq!(idx.len(), 0);
    }

    #[test]
    fn insert_entry_rebuilds_index_with_new_row() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let parsed = read_index_file(&fs, &path).unwrap();

        let entry = build_index_entry(0xDEAD_BEEF_CAFE_BABE, 0, 0x200).unwrap();
        let result = insert_entry(&parsed, entry, "exd/test.exh").unwrap();

        fs.insert(&path, result.bytes);
        let idx = Index::open_in(&path, &fs).unwrap();
        assert_eq!(idx.len(), 1);
        let e = idx.get(0xDEAD_BEEF_CAFE_BABE).expect("entry present");
        assert_eq!(e.data_file_id(), 0);
        assert_eq!(e.byte_offset(), 0x200);
    }

    #[test]
    fn insert_entry_re_sorts_by_hash() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let mut parsed = read_index_file(&fs, &path).unwrap();
        parsed
            .entries
            .push(build_index_entry(0x300, 0, 0x100).unwrap());
        parsed
            .entries
            .push(build_index_entry(0x100, 0, 0x200).unwrap());

        let new_entry = build_index_entry(0x200, 0, 0x300).unwrap();
        let result = insert_entry(&parsed, new_entry, "exd/test.exh").unwrap();
        fs.insert(&path, result.bytes);
        let idx = Index::open_in(&path, &fs).unwrap();
        assert_eq!(idx.len(), 3);
        let hashes: Vec<u64> = idx.entries().iter().map(|e| e.hash).collect();
        assert_eq!(hashes, vec![0x100, 0x200, 0x300]);
    }

    #[test]
    fn insert_entry_rejects_duplicate_hash() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let mut parsed = read_index_file(&fs, &path).unwrap();
        parsed
            .entries
            .push(build_index_entry(0x42, 0, 0x100).unwrap());

        let dup = build_index_entry(0x42, 0, 0x200).unwrap();
        let err = insert_entry(&parsed, dup, "exd/dup.exh").unwrap_err();
        match err {
            Error::FileAlreadyExists { game_path } => assert_eq!(game_path, "exd/dup.exh"),
            other => panic!("expected FileAlreadyExists, got {other:?}"),
        }
    }

    #[test]
    fn rebuilt_index_carries_nonzero_sha1_hashes() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let parsed = read_index_file(&fs, &path).unwrap();
        let entry = build_index_entry(0x55, 0, 0x100).unwrap();
        let result = insert_entry(&parsed, entry, "exd/x.exh").unwrap();
        fs.insert(&path, result.bytes);
        let idx = Index::open_in(&path, &fs).unwrap();
        assert_ne!(&idx.index_header().index_data_hash[..20], &[0u8; 20]);
        assert_ne!(&idx.index_header().self_hash[..20], &[0u8; 20]);
    }

    #[test]
    fn rebuilt_index_data_hash_equals_sha1_of_hash_table_region() {
        let bytes = build_empty_index(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
        fs.insert(&path, bytes);
        let parsed = read_index_file(&fs, &path).unwrap();
        let entry = build_index_entry(0xDEAD_CAFE_0000_0080, 0, 0x200).unwrap();
        let result = insert_entry(&parsed, entry, "exd/sha1_check.exh").unwrap();

        let idx_header_start = SQPACK_HEADER_SIZE as usize;
        let data_offset = u32::from_le_bytes(
            result.bytes[idx_header_start + 8..idx_header_start + 12]
                .try_into()
                .unwrap(),
        ) as usize;
        let data_size = u32::from_le_bytes(
            result.bytes[idx_header_start + 12..idx_header_start + 16]
                .try_into()
                .unwrap(),
        ) as usize;
        let data_region = &result.bytes[data_offset..data_offset + data_size];

        let expected = sha1(data_region);

        let stored_hash_start = idx_header_start + 16;
        let stored = &result.bytes[stored_hash_start..stored_hash_start + 20];

        assert_eq!(
            stored,
            &expected[..],
            "index_data_hash must equal sha1(data_region)"
        );
    }

    fn build_index_entry(hash: u64, data_file_id: u8, byte_offset: u64) -> Result<IndexEntry> {
        let data = pack_index_data(
            false,
            DatLocation {
                data_file_id,
                byte_offset,
            },
        )?;
        Ok(IndexEntry {
            hash,
            data,
            padding: 0,
        })
    }
}
