use crate::error::{QuarantineError, Result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Read, Write};

/// Magic bytes for identifying quarantine files ("STEM" in ASCII).
pub const MAGIC: &[u8; 4] = b"STEM";

/// Current file format version.
pub const VERSION: u8 = 2;

/// Size of the file header in bytes.
/// Magic (4) + Version (1) + Reserved (3)
pub const HEADER_SIZE: usize = 8;

/// Per-record overhead in bytes (v2 format).
/// payload_len (4) + crc32c (4) + blake3 (32) = 40
pub const RECORD_OVERHEAD: usize = 40;

/// Maximum record size (100 MB).
pub const MAX_RECORD_SIZE: usize = 100 * 1024 * 1024;

/// File header structure.
///
/// Written at the beginning of every quarantine file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileHeader {
    /// Magic bytes "STEM".
    pub magic: [u8; 4],
    /// Format version.
    pub version: u8,
}

impl Default for FileHeader {
    fn default() -> Self {
        Self::new()
    }
}

impl FileHeader {
    /// Create a new file header with default magic and version.
    pub fn new() -> Self {
        Self { magic: *MAGIC, version: VERSION }
    }

    /// Write the header to a writer.
    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
        writer.write_all(&self.magic).map_err(QuarantineError::IoGeneric)?;
        writer.write_u8(self.version).map_err(QuarantineError::IoGeneric)?;
        // Write 3 reserved bytes (padding)
        writer.write_all(&[0u8; 3]).map_err(QuarantineError::IoGeneric)?;
        Ok(())
    }

    /// Read the header from a reader and validate it.
    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
        let mut magic = [0u8; 4];
        reader.read_exact(&mut magic).map_err(QuarantineError::IoGeneric)?;

        if magic != *MAGIC {
            return Err(QuarantineError::IoGeneric(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "Invalid magic bytes",
            )));
        }

        let version = reader.read_u8().map_err(QuarantineError::IoGeneric)?;
        if version != VERSION {
            return Err(QuarantineError::CorruptRecord {
                offset: 0,
                reason: format!(
                    "Unsupported WAL version {} (expected {}). Delete the WAL and re-ingest.",
                    version, VERSION
                ),
            });
        }

        // Skip reserved bytes
        let mut reserved = [0u8; 3];
        reader.read_exact(&mut reserved).map_err(QuarantineError::IoGeneric)?;

        Ok(Self { magic, version })
    }
}

/// Compute CRC32C over the concatenation of len_bytes, blake3, and payload.
///
/// The CRC covers everything except itself, providing fast integrity checking
/// that detects torn writes before the more expensive BLAKE3 verification.
pub fn compute_crc32c(len_bytes: &[u8; 4], blake3: &[u8; 32], payload: &[u8]) -> u32 {
    let crc = crc32c::crc32c_append(0, len_bytes);
    let crc = crc32c::crc32c_append(crc, blake3);
    crc32c::crc32c_append(crc, payload)
}

/// A single log record in the WAL.
///
/// v2 Format: `[payload_len:u32_LE][crc32c:u32][blake3:32][payload:N]`
///
/// - Length first: recovery scanner knows read size before touching checksums
/// - CRC32C second: fast integrity check, rejects torn writes
/// - BLAKE3 before payload: content-addressing hash in fixed 40-byte header
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
    /// BLAKE3 checksum of the payload.
    pub checksum: [u8; 32],
    /// CRC32C integrity check covering len + blake3 + payload.
    pub crc: u32,
    /// The actual data payload.
    pub payload: Vec<u8>,
}

impl Record {
    /// Create a new record from a payload, calculating both checksums.
    pub fn new(payload: Vec<u8>) -> Self {
        let checksum: [u8; 32] = blake3::hash(&payload).into();
        let len_bytes = (payload.len() as u32).to_le_bytes();
        let crc = compute_crc32c(&len_bytes, &checksum, &payload);
        Self { checksum, crc, payload }
    }

    /// Calculate the on-disk size of this record.
    pub fn disk_size(&self) -> u64 {
        (RECORD_OVERHEAD + self.payload.len()) as u64
    }

    /// Write the record to a writer in v2 format.
    ///
    /// Layout: `[payload_len:u32_LE][crc32c:u32][blake3:32][payload:N]`
    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
        writer
            .write_u32::<LittleEndian>(self.payload.len() as u32)
            .map_err(QuarantineError::IoGeneric)?;
        writer.write_u32::<LittleEndian>(self.crc).map_err(QuarantineError::IoGeneric)?;
        writer.write_all(&self.checksum).map_err(QuarantineError::IoGeneric)?;
        writer.write_all(&self.payload).map_err(QuarantineError::IoGeneric)?;
        Ok(())
    }

    /// Read a record from a reader in v2 format and verify both checksums.
    ///
    /// CRC32C is checked first (fast reject for torn writes), then BLAKE3.
    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
        let len = reader.read_u32::<LittleEndian>().map_err(QuarantineError::IoGeneric)?;

        if len == 0 || len as usize > MAX_RECORD_SIZE {
            return Err(QuarantineError::IoGeneric(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("Invalid record length: {} bytes", len),
            )));
        }

        let stored_crc = reader.read_u32::<LittleEndian>().map_err(QuarantineError::IoGeneric)?;

        let mut checksum = [0u8; 32];
        reader.read_exact(&mut checksum).map_err(QuarantineError::IoGeneric)?;

        let mut payload = vec![0u8; len as usize];
        reader.read_exact(&mut payload).map_err(QuarantineError::IoGeneric)?;

        // Verify CRC32C first (fast reject for torn writes)
        let len_bytes = len.to_le_bytes();
        let computed_crc = compute_crc32c(&len_bytes, &checksum, &payload);
        if stored_crc != computed_crc {
            return Err(QuarantineError::Crc32cMismatch {
                offset: 0, // caller should adjust
                expected: stored_crc,
                actual: computed_crc,
            });
        }

        // Verify BLAKE3 (content-addressing integrity)
        let calculated: [u8; 32] = blake3::hash(&payload).into();
        if checksum != calculated {
            return Err(QuarantineError::IoGeneric(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "BLAKE3 checksum mismatch",
            )));
        }

        Ok(Self { checksum, crc: stored_crc, payload })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_header_roundtrip() {
        let header = FileHeader::new();
        let mut buffer = Vec::new();

        header.write_to(&mut buffer).expect("write header");
        assert_eq!(buffer.len(), HEADER_SIZE);

        let mut reader = Cursor::new(buffer);
        let read_header = FileHeader::read_from(&mut reader).expect("read header");

        assert_eq!(header, read_header);
    }

    #[test]
    fn test_record_v2_roundtrip() {
        let payload = b"test payload data".to_vec();
        let record = Record::new(payload.clone());
        let mut buffer = Vec::new();

        record.write_to(&mut buffer).expect("write record");
        assert_eq!(buffer.len() as u64, record.disk_size());
        assert_eq!(buffer.len(), RECORD_OVERHEAD + payload.len());

        let mut reader = Cursor::new(buffer);
        let read_record = Record::read_from(&mut reader).expect("read record");

        assert_eq!(record, read_record);
        assert_eq!(read_record.payload, payload);
    }

    #[test]
    fn test_crc32c_detects_payload_corruption() {
        let payload = b"test data".to_vec();
        let record = Record::new(payload);
        let mut buffer = Vec::new();
        record.write_to(&mut buffer).expect("write record");

        // Corrupt a byte in the payload region (after the 40-byte header)
        let last = buffer.len() - 1;
        buffer[last] ^= 0xFF;

        let mut reader = Cursor::new(buffer);
        let result = Record::read_from(&mut reader);

        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            matches!(err, QuarantineError::Crc32cMismatch { .. }),
            "Expected Crc32cMismatch, got: {}",
            err
        );
    }

    #[test]
    fn test_crc32c_detects_length_corruption() {
        let payload = b"test data".to_vec();
        let record = Record::new(payload);
        let mut buffer = Vec::new();
        record.write_to(&mut buffer).expect("write record");

        // Corrupt the length field (first 4 bytes) - change to a valid but wrong length
        // Set length to payload.len() + 1 (still within bounds)
        let corrupted_len = (record.payload.len() as u32 + 1).to_le_bytes();
        buffer[0] = corrupted_len[0];
        buffer[1] = corrupted_len[1];
        buffer[2] = corrupted_len[2];
        buffer[3] = corrupted_len[3];

        let mut reader = Cursor::new(buffer);
        let result = Record::read_from(&mut reader);

        // Should fail - either EOF because length is too long, or CRC mismatch
        assert!(result.is_err());
    }

    #[test]
    fn test_blake3_still_verified() {
        // Create a record, then manually construct a buffer with correct CRC
        // but wrong BLAKE3 to verify the second check works
        let payload = b"original data".to_vec();
        let record = Record::new(payload);
        let mut buffer = Vec::new();
        record.write_to(&mut buffer).expect("write record");

        // Tamper with blake3 hash bytes (bytes 8..40) AND the CRC to match
        // This is contrived but tests that BLAKE3 is independently verified
        let bad_payload = b"tampered data".to_vec();
        let bad_checksum: [u8; 32] = blake3::hash(b"wrong data").into();
        let len_bytes = (bad_payload.len() as u32).to_le_bytes();
        let new_crc = compute_crc32c(&len_bytes, &bad_checksum, &bad_payload);

        let mut tampered = Vec::new();
        tampered.extend_from_slice(&len_bytes);
        tampered.extend_from_slice(&new_crc.to_le_bytes());
        tampered.extend_from_slice(&bad_checksum);
        tampered.extend_from_slice(&bad_payload);

        let mut reader = Cursor::new(tampered);
        let result = Record::read_from(&mut reader);

        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(err_msg.contains("BLAKE3"), "Expected BLAKE3 error, got: {}", err_msg);
    }

    #[test]
    fn test_header_rejects_v1() {
        let mut buffer = Vec::new();
        // Write magic
        buffer.extend_from_slice(MAGIC);
        // Write version 1
        buffer.push(1);
        // Write reserved
        buffer.extend_from_slice(&[0u8; 3]);

        let mut reader = Cursor::new(buffer);
        let result = FileHeader::read_from(&mut reader);

        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("Unsupported WAL version 1"),
            "Expected version error, got: {}",
            err_msg
        );
        assert!(
            err_msg.contains("Delete the WAL"),
            "Expected remediation advice, got: {}",
            err_msg
        );
    }

    #[test]
    fn test_record_disk_size() {
        let payload = vec![0u8; 100];
        let record = Record::new(payload);
        assert_eq!(record.disk_size(), (RECORD_OVERHEAD + 100) as u64);
    }

    #[test]
    fn test_record_empty_payload_rejected() {
        // Empty payload (len=0) should be rejected on read
        let mut buffer = Vec::new();
        buffer.extend_from_slice(&0u32.to_le_bytes()); // len = 0
        buffer.extend_from_slice(&0u32.to_le_bytes()); // crc
        buffer.extend_from_slice(&[0u8; 32]); // blake3

        let mut reader = Cursor::new(buffer);
        let result = Record::read_from(&mut reader);
        assert!(result.is_err());
    }

    #[test]
    fn test_compute_crc32c_deterministic() {
        let len_bytes = [10, 0, 0, 0u8];
        let blake3 = [0xABu8; 32];
        let payload = b"hello world!";

        let crc1 = compute_crc32c(&len_bytes, &blake3, payload);
        let crc2 = compute_crc32c(&len_bytes, &blake3, payload);
        assert_eq!(crc1, crc2);

        // Different payload -> different CRC
        let crc3 = compute_crc32c(&len_bytes, &blake3, b"different");
        assert_ne!(crc1, crc3);
    }
}
