//! Shared checkpoint file format for index persistence.
//!
//! This module provides common utilities for reading and writing checkpoint files
//! with consistent header format, CRC32C integrity, and atomic write semantics.
//!
//! # File Format
//!
//! All checkpoint files share this header structure:
//!
//! ```text
//! [MAGIC:4][VERSION:1][RESERVED:3][PAYLOAD_LEN:u64_LE][CRC32C:u32][PAYLOAD:N]
//! ```
//!
//! - **MAGIC:** 4-byte identifier (e.g., "VHCK", "VISG", "VISD")
//! - **VERSION:** 1-byte format version
//! - **RESERVED:** 3 bytes for future use
//! - **PAYLOAD_LEN:** Little-endian u64 payload length
//! - **CRC32C:** CRC32C checksum of payload
//! - **PAYLOAD:** rkyv-serialized data

use crate::error::{Result, StorageError};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;

/// Maximum payload size (1GB) to prevent memory exhaustion from malicious files.
pub const MAX_PAYLOAD_SIZE: u64 = 1024 * 1024 * 1024;

/// Header size in bytes (magic + version + reserved).
pub const HEADER_SIZE: usize = 8;

/// Reads and validates a checkpoint file header.
///
/// # Arguments
/// * `file` - Open file to read from
/// * `expected_magic` - Expected 4-byte magic identifier
/// * `expected_version` - Expected format version
///
/// # Returns
/// The payload bytes if validation succeeds.
///
/// # Errors
/// Returns error if magic/version mismatch, payload too large, or CRC mismatch.
pub fn read_checkpoint<R: Read>(
    file: &mut R,
    expected_magic: &[u8; 4],
    expected_version: u8,
) -> Result<Vec<u8>> {
    // Read and verify magic
    let mut magic = [0u8; 4];
    file.read_exact(&mut magic)?;
    if &magic != expected_magic {
        return Err(StorageError::InputValidation(format!(
            "Invalid magic: expected {:?}, got {:?}",
            expected_magic, magic
        )));
    }

    // Read version
    let mut version = [0u8; 1];
    file.read_exact(&mut version)?;
    if version[0] != expected_version {
        return Err(StorageError::InputValidation(format!(
            "Unsupported version: expected {}, got {}",
            expected_version, version[0]
        )));
    }

    // Skip reserved bytes
    let mut reserved = [0u8; 3];
    file.read_exact(&mut reserved)?;

    // Read payload length
    let payload_len = file.read_u64::<LittleEndian>()?;

    // Validate payload size to prevent memory exhaustion
    if payload_len > MAX_PAYLOAD_SIZE {
        return Err(StorageError::InputValidation(format!(
            "Payload too large: {} bytes exceeds max {} bytes",
            payload_len, MAX_PAYLOAD_SIZE
        )));
    }

    // Read CRC32C
    let expected_crc = file.read_u32::<LittleEndian>()?;

    // Read payload
    let mut payload = vec![0u8; payload_len as usize];
    file.read_exact(&mut payload)?;

    // Verify CRC32C
    let actual_crc = crc32c::crc32c(&payload);
    if actual_crc != expected_crc {
        return Err(StorageError::InputValidation(format!(
            "CRC mismatch: expected {:08x}, got {:08x}",
            expected_crc, actual_crc
        )));
    }

    Ok(payload)
}

/// Writes a checkpoint file with header and CRC32C integrity.
///
/// # Arguments
/// * `file` - Open file to write to
/// * `magic` - 4-byte magic identifier
/// * `version` - Format version
/// * `payload` - Serialized payload data
pub fn write_checkpoint<W: Write>(
    file: &mut W,
    magic: &[u8; 4],
    version: u8,
    payload: &[u8],
) -> Result<()> {
    // Write header
    file.write_all(magic)?;
    file.write_all(&[version])?;
    file.write_all(&[0u8; 3])?; // Reserved

    // Write payload length and CRC
    let crc = crc32c::crc32c(payload);
    file.write_u64::<LittleEndian>(payload.len() as u64)?;
    file.write_u32::<LittleEndian>(crc)?;

    // Write payload
    file.write_all(payload)?;

    Ok(())
}

/// Performs an atomic write with fsync and rename.
///
/// 1. Writes to temp file
/// 2. fsync temp file
/// 3. Rename to final path (atomic on POSIX)
/// 4. fsync parent directory
///
/// # Arguments
/// * `final_path` - The final destination path
/// * `write_fn` - Closure that writes content to the temp file
///
/// # Errors
/// Returns error if write fails. Cleans up temp file on failure.
pub fn atomic_write<F>(final_path: &Path, write_fn: F) -> Result<()>
where
    F: FnOnce(&mut File) -> Result<()>,
{
    // Build temp path by appending .tmp to filename
    let temp_path = {
        let mut temp = final_path.to_path_buf();
        let file_name = final_path.file_name().and_then(|n| n.to_str()).unwrap_or("checkpoint");
        temp.set_file_name(format!("{}.tmp", file_name));
        temp
    };

    // Write to temp file (clean up on any error)
    {
        let mut file = File::create(&temp_path)?;
        if let Err(e) = write_fn(&mut file) {
            // Clean up temp file on write error
            let _ = fs::remove_file(&temp_path);
            return Err(e);
        }
        if let Err(e) = file.sync_all() {
            let _ = fs::remove_file(&temp_path);
            return Err(e.into());
        }
    }

    // Atomic rename (clean up temp file on failure)
    if let Err(e) = fs::rename(&temp_path, final_path) {
        // Best effort cleanup of temp file
        let _ = fs::remove_file(&temp_path);
        return Err(e.into());
    }

    // fsync parent directory
    if let Some(parent) = final_path.parent() {
        if let Ok(dir) = File::open(parent) {
            let _ = dir.sync_all();
        }
    }

    Ok(())
}

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

    const TEST_MAGIC: &[u8; 4] = b"TEST";
    const TEST_VERSION: u8 = 1;

    #[test]
    fn test_roundtrip() {
        let payload = b"Hello, checkpoint!";

        // Write to buffer
        let mut buffer = Vec::new();
        write_checkpoint(&mut buffer, TEST_MAGIC, TEST_VERSION, payload).expect("write");

        // Read back
        let mut cursor = Cursor::new(buffer);
        let recovered = read_checkpoint(&mut cursor, TEST_MAGIC, TEST_VERSION).expect("read");

        assert_eq!(recovered, payload);
    }

    #[test]
    fn test_wrong_magic() {
        let payload = b"test";
        let mut buffer = Vec::new();
        write_checkpoint(&mut buffer, TEST_MAGIC, TEST_VERSION, payload).expect("write");

        let mut cursor = Cursor::new(buffer);
        let result = read_checkpoint(&mut cursor, b"XXXX", TEST_VERSION);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Invalid magic"));
    }

    #[test]
    fn test_wrong_version() {
        let payload = b"test";
        let mut buffer = Vec::new();
        write_checkpoint(&mut buffer, TEST_MAGIC, TEST_VERSION, payload).expect("write");

        let mut cursor = Cursor::new(buffer);
        let result = read_checkpoint(&mut cursor, TEST_MAGIC, 99);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Unsupported version"));
    }

    #[test]
    fn test_crc_corruption() {
        let payload = b"test payload";
        let mut buffer = Vec::new();
        write_checkpoint(&mut buffer, TEST_MAGIC, TEST_VERSION, payload).expect("write");

        // Corrupt a byte in the payload area (after header + len + crc)
        let payload_start = HEADER_SIZE + 8 + 4; // header + u64 len + u32 crc
        buffer[payload_start] ^= 0xFF;

        let mut cursor = Cursor::new(buffer);
        let result = read_checkpoint(&mut cursor, TEST_MAGIC, TEST_VERSION);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("CRC mismatch"));
    }

    #[test]
    fn test_atomic_write() {
        let tmp = TempDir::new().expect("create temp dir");
        let final_path = tmp.path().join("test.checkpoint");

        atomic_write(&final_path, |file| {
            write_checkpoint(file, TEST_MAGIC, TEST_VERSION, b"atomic content")
        })
        .expect("atomic write");

        // Verify file exists
        assert!(final_path.exists());

        // Verify content
        let mut file = File::open(&final_path).expect("open");
        let payload = read_checkpoint(&mut file, TEST_MAGIC, TEST_VERSION).expect("read");
        assert_eq!(payload, b"atomic content");

        // Verify no temp file left behind
        let temp_path = tmp.path().join("test.checkpoint.tmp");
        assert!(!temp_path.exists());
    }

    #[test]
    fn test_atomic_write_cleanup_on_error() {
        let tmp = TempDir::new().expect("create temp dir");
        let final_path = tmp.path().join("test.checkpoint");

        // This should fail during write
        let result = atomic_write(&final_path, |_file| {
            Err(StorageError::InputValidation("intentional failure".to_string()))
        });

        assert!(result.is_err());

        // Verify no files left behind
        assert!(!final_path.exists());
        let temp_path = tmp.path().join("test.checkpoint.tmp");
        assert!(!temp_path.exists());
    }

    #[test]
    fn test_empty_payload() {
        let payload: &[u8] = b"";

        let mut buffer = Vec::new();
        write_checkpoint(&mut buffer, TEST_MAGIC, TEST_VERSION, payload).expect("write");

        let mut cursor = Cursor::new(buffer);
        let recovered = read_checkpoint(&mut cursor, TEST_MAGIC, TEST_VERSION).expect("read");

        assert!(recovered.is_empty());
    }
}
