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::index2::{INDEX2_ENTRY_SIZE, Index2Entry};
use crate::vfs::Vfs;
use crate::write::sha1::{pad_to_64, sha1};

/// Encodes a [`DatLocation`] into the 32-bit `data` field of a `.index2` entry.
///
/// The encoding is `(byte_offset / 8) | (data_file_id << 1)`; unlike the `.index`
/// variant there is no synonym bit. Returns an error under the same conditions as
/// [`pack_index_data`](crate::write::index_writer::pack_index_data).
pub(crate) fn pack_index2_data(location: DatLocation) -> Result<u32> {
    let DatLocation {
        data_file_id,
        byte_offset,
    } = location;
    if data_file_id > 7 {
        return Err(Error::InvalidBlockLayout {
            context: "index2_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: "index2_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: "index2_entry.byte_offset",
        message: format!("byte_offset/8 = {raw_offset} exceeds u32::MAX"),
    })?;
    if raw_offset_u32 & 0xF != 0 {
        return Err(Error::InvalidBlockLayout {
            context: "index2_entry.byte_offset",
            message: format!(
                "byte_offset {byte_offset} → low-nibble of byte_offset/8 ({raw_offset_u32:#x}) is non-zero"
            ),
        });
    }
    let file_id_bits = u32::from(data_file_id & 0b111) << 1;
    Ok(raw_offset_u32 | file_id_bits)
}

/// Parsed representation of a `.index2` file; mirrors [`ParsedIndex`](crate::write::index_writer::ParsedIndex)
/// but holds [`Index2Entry`] records and uses 32-bit hashes.
#[derive(Debug)]
pub(crate) struct ParsedIndex2 {
    /// 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<Index2Entry>,
    /// 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 `.index2` file into a [`ParsedIndex2`].
pub(crate) fn read_index2_file(vfs: &dyn Vfs, path: &Path) -> Result<ParsedIndex2> {
    let handle = vfs.open_read(path).map_err(|e| Error::io_at(path, e))?;
    let mut reader = BufReader::new(handle);
    read_index2_inner(&mut reader)
}

fn read_index2_inner<R: Read + Seek>(reader: &mut R) -> Result<ParsedIndex2> {
    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 % INDEX2_ENTRY_SIZE != 0 {
        return Err(Error::MisalignedIndexData {
            size: index_header.index_data_size,
            entry_size: INDEX2_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 / INDEX2_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 = Index2Entry::read(reader).map_err(|e| Error::decode("index2_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(ParsedIndex2 {
        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: &ParsedIndex2,
    new_entry: Index2Entry,
    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_index2(parsed, &entries)?,
    })
}

/// Re-serialises a [`ParsedIndex2`] with its current entry list, recomputing all SHA-1 hashes.
pub(crate) fn serialise_via_insert(parsed: &ParsedIndex2) -> Result<Vec<u8>> {
    serialise_index2(parsed, &parsed.entries)
}

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

    let index_data_offset = parsed
        .header
        .size
        .checked_add(parsed.index_header.size)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "index2.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: "index2.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: "index2.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: "index2.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: "index2.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: "index2.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: "index2.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 index2 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: &[Index2Entry]) -> Vec<u8> {
    let mut out = Vec::with_capacity(entries.len() * INDEX2_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
}

#[cfg(test)]
pub(crate) fn build_empty_index2(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::index2::Index2;
    use crate::vfs::InMemoryFs;
    use std::path::PathBuf;

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

    #[test]
    fn pack_index2_data_encodes_data_file_id() {
        let data = pack_index2_data(DatLocation {
            data_file_id: 2,
            byte_offset: 0x100,
        })
        .unwrap();
        assert_eq!(data, 0x20 | 0x4);
    }

    #[test]
    fn insert_entry_rebuilds_index2_with_new_row() {
        let bytes = build_empty_index2(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index2");
        fs.insert(&path, bytes);
        let parsed = read_index2_file(&fs, &path).unwrap();
        let entry = Index2Entry {
            hash: 0xCAFE_BABE,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x200,
            })
            .unwrap(),
        };
        let result = insert_entry(&parsed, entry, "exd/test.exh").unwrap();
        fs.insert(&path, result.bytes);
        let idx = Index2::open_in(&path, &fs).unwrap();
        assert_eq!(idx.len(), 1);
        let e = idx.get(0xCAFE_BABE).unwrap();
        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_index2(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index2");
        fs.insert(&path, bytes);
        let mut parsed = read_index2_file(&fs, &path).unwrap();
        parsed.entries.push(Index2Entry {
            hash: 0x300,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x100,
            })
            .unwrap(),
        });
        parsed.entries.push(Index2Entry {
            hash: 0x100,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x200,
            })
            .unwrap(),
        });
        let new_entry = Index2Entry {
            hash: 0x200,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x300,
            })
            .unwrap(),
        };
        let result = insert_entry(&parsed, new_entry, "exd/test.exh").unwrap();
        fs.insert(&path, result.bytes);
        let idx = Index2::open_in(&path, &fs).unwrap();
        let hashes: Vec<u32> = 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_index2(1);
        let fs = InMemoryFs::new();
        let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index2");
        fs.insert(&path, bytes);
        let mut parsed = read_index2_file(&fs, &path).unwrap();
        parsed.entries.push(Index2Entry {
            hash: 0x42,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x100,
            })
            .unwrap(),
        });
        let dup = Index2Entry {
            hash: 0x42,
            data: pack_index2_data(DatLocation {
                data_file_id: 0,
                byte_offset: 0x200,
            })
            .unwrap(),
        };
        let err = insert_entry(&parsed, dup, "exd/dup.exh").unwrap_err();
        assert!(matches!(err, Error::FileAlreadyExists { .. }));
    }
}
