//! Disk format for vector index snapshots.
//!
//! # File Format
//!
//! The vector index snapshot consists of two files:
//!
//! ## Graph File (`{version}.hnsw.graph`)
//!
//! Contains the HNSW graph structure with navigation links.
//!
//! ```text
//! [MAGIC:4 "VISG"][VERSION:1][RESERVED:3][METADATA:N (rkyv)]
//! [PAYLOAD_LEN:u64_LE][CRC32C:u32][GRAPH_DATA:N]
//! ```
//!
//! ## Data File (`{version}.hnsw.data`)
//!
//! Contains the actual vector embeddings.
//!
//! ```text
//! [MAGIC:4 "VISD"][VERSION:1][RESERVED:3]
//! [PAYLOAD_LEN:u64_LE][CRC32C:u32][VECTORS_DATA:N]
//! ```
//!
//! # Integrity
//!
//! - CRC32C for fast corruption detection
//! - BLAKE3 for content-addressed integrity

use stemedb_core::types::Hash;

/// Magic bytes for graph file.
pub const GRAPH_MAGIC: &[u8; 4] = b"VISG";

/// Magic bytes for data file.
pub const DATA_MAGIC: &[u8; 4] = b"VISD";

/// Current format version.
pub const FORMAT_VERSION: u8 = 1;

// Note: HEADER_SIZE and MAX_PAYLOAD_SIZE are defined in checkpoint_format.rs

/// Metadata about a vector index snapshot.
///
/// Stored at the start of the graph file after the header.
#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub struct SnapshotMetadata {
    /// Monotonically increasing version number.
    pub version: u64,

    /// WAL offset where this snapshot starts (inclusive).
    pub wal_offset_start: u64,

    /// WAL offset where this snapshot ends (exclusive).
    pub wal_offset_end: u64,

    /// Unix timestamp when this snapshot was created.
    pub created_at: u64,

    /// Number of vectors in the snapshot.
    pub vector_count: u64,

    /// Dimension of vectors in this index.
    pub dimension: usize,

    /// BLAKE3 hash of the graph data for integrity verification.
    pub graph_hash: Hash,

    /// BLAKE3 hash of the vector data for integrity verification.
    pub data_hash: Hash,
}

impl SnapshotMetadata {
    /// Create new snapshot metadata.
    pub fn new(
        version: u64,
        wal_offset_start: u64,
        wal_offset_end: u64,
        vector_count: u64,
        dimension: usize,
        graph_hash: Hash,
        data_hash: Hash,
    ) -> Self {
        let created_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_or(0, |d| d.as_secs());

        Self {
            version,
            wal_offset_start,
            wal_offset_end,
            created_at,
            vector_count,
            dimension,
            graph_hash,
            data_hash,
        }
    }
}

/// Entry in the ID mapping table.
///
/// Maps internal HNSW IDs to assertion hashes.
#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub struct IdMappingEntry {
    /// Internal HNSW ID.
    pub internal_id: usize,
    /// Assertion hash.
    pub hash: Hash,
    /// Unix timestamp when this vector was added.
    pub timestamp: u64,
}

/// Complete ID mapping table for the index.
#[derive(Debug, Clone, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub struct IdMappingTable {
    /// All mappings, indexed by internal ID.
    pub entries: Vec<IdMappingEntry>,
}

impl IdMappingTable {
    /// Create a new empty mapping table.
    pub fn new() -> Self {
        Self { entries: Vec::new() }
    }

    /// Add a mapping entry.
    pub fn push(&mut self, entry: IdMappingEntry) {
        self.entries.push(entry);
    }

    /// Get the number of mappings.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Check if the table is empty.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

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

    #[test]
    fn test_snapshot_metadata_roundtrip() {
        let meta = SnapshotMetadata::new(1, 0, 1000, 500, 128, [1u8; 32], [2u8; 32]);

        let bytes = crate::serde_helpers::serialize(&meta).expect("serialize");
        let recovered: SnapshotMetadata =
            crate::serde_helpers::deserialize(&bytes).expect("deserialize");

        assert_eq!(meta.version, recovered.version);
        assert_eq!(meta.wal_offset_start, recovered.wal_offset_start);
        assert_eq!(meta.wal_offset_end, recovered.wal_offset_end);
        assert_eq!(meta.vector_count, recovered.vector_count);
        assert_eq!(meta.dimension, recovered.dimension);
        assert_eq!(meta.graph_hash, recovered.graph_hash);
        assert_eq!(meta.data_hash, recovered.data_hash);
    }

    #[test]
    fn test_id_mapping_table() {
        let mut table = IdMappingTable::new();
        assert!(table.is_empty());

        table.push(IdMappingEntry { internal_id: 0, hash: [1u8; 32], timestamp: 100 });

        table.push(IdMappingEntry { internal_id: 1, hash: [2u8; 32], timestamp: 200 });

        assert_eq!(table.len(), 2);

        let bytes = crate::serde_helpers::serialize(&table).expect("serialize");
        let recovered: IdMappingTable =
            crate::serde_helpers::deserialize(&bytes).expect("deserialize");

        assert_eq!(recovered.len(), 2);
        assert_eq!(recovered.entries[0].internal_id, 0);
        assert_eq!(recovered.entries[1].hash, [2u8; 32]);
    }
}
