//! Simple Write-Ahead Log for vector operations in SocialStoreV2
//!
//! Provides crash recovery for vector index operations (index_entity, remove_entity_vector).
//! Uses a simple JSON Lines format for simplicity and human readability.
//!
//! # File Format
//!
//! Each line is a JSON object:
//! ```json
//! {"op":"Index","entity_id":"abc123","embedding":[0.1,0.2,...],"timestamp":1700000000000}
//! {"op":"Remove","entity_id":"abc123","timestamp":1700000001000}
//! ```
//!
//! # Recovery
//!
//! On recovery, entries are replayed in order:
//! - `Index`: Insert embedding into HNSW index
//! - `Remove`: Delete embedding from HNSW index

use std::path::{Path, PathBuf};
use std::io::{self, Write, BufWriter, BufRead, BufReader};
use std::fs::{File, OpenOptions};

use serde::{Serialize, Deserialize};

/// Vector WAL operation type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VectorWALOp {
    /// Index an entity with embedding
    Index,
    /// Remove an entity's vector
    Remove,
}

/// A single entry in the Vector WAL
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorWALEntry {
    /// Operation type
    pub op: VectorWALOp,
    
    /// Entity ID being indexed/removed
    pub entity_id: String,
    
    /// Embedding vector (only for Index operations)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub embedding: Option<Vec<f32>>,
    
    /// Timestamp when entry was created (ms since epoch)
    pub timestamp: i64,
}

/// Simple WAL writer/reader for vector operations
///
/// Operates independently from the graph WAL, matching the
/// "two independent storage systems" architecture decision.
pub struct VectorWAL {
    /// WAL file path
    path: PathBuf,
    
    /// Buffered writer (lazy-initialized on first write)
    writer: Option<BufWriter<File>>,
    
    /// Whether WAL is enabled
    enabled: bool,
    
    /// Entry count since last truncation
    entry_count: u64,
}

impl VectorWAL {
    /// Create a new Vector WAL at the specified path
    ///
    /// Creates parent directories if they don't exist.
    pub fn new(path: impl Into<PathBuf>) -> io::Result<Self> {
        let path = path.into();
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        Ok(Self {
            path,
            writer: None,
            enabled: true,
            entry_count: 0,
        })
    }
    
    /// Create a disabled WAL (for testing)
    pub fn disabled() -> Self {
        Self {
            path: PathBuf::new(),
            writer: None,
            enabled: false,
            entry_count: 0,
        }
    }
    
    /// Check if WAL is enabled
    pub fn is_enabled(&self) -> bool {
        self.enabled
    }
    
    /// Get WAL file path
    pub fn path(&self) -> &Path {
        &self.path
    }
    
    /// Get entry count since last truncation
    pub fn entry_count(&self) -> u64 {
        self.entry_count
    }
    
    /// Ensure the writer is initialized (lazy open)
    fn ensure_writer(&mut self) -> io::Result<()> {
        if !self.enabled {
            return Ok(());
        }
        if self.writer.is_none() {
            let file = OpenOptions::new()
                .create(true)
                .append(true)
                .open(&self.path)?;
            self.writer = Some(BufWriter::with_capacity(32 * 1024, file));
        }
        Ok(())
    }
    
    /// Append an index operation to the WAL
    pub fn append_index(&mut self, entity_id: &str, embedding: &[f32]) -> io::Result<()> {
        if !self.enabled {
            return Ok(());
        }
        self.ensure_writer()?;
        
        let entry = VectorWALEntry {
            op: VectorWALOp::Index,
            entity_id: entity_id.to_string(),
            embedding: Some(embedding.to_vec()),
            timestamp: current_timestamp(),
        };
        
        let json = serde_json::to_string(&entry)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
        
        if let Some(ref mut writer) = self.writer {
            writeln!(writer, "{}", json)?;
            writer.flush()?;
        }
        
        self.entry_count += 1;
        Ok(())
    }
    
    /// Append a remove operation to the WAL
    pub fn append_remove(&mut self, entity_id: &str) -> io::Result<()> {
        if !self.enabled {
            return Ok(());
        }
        self.ensure_writer()?;
        
        let entry = VectorWALEntry {
            op: VectorWALOp::Remove,
            entity_id: entity_id.to_string(),
            embedding: None,
            timestamp: current_timestamp(),
        };
        
        let json = serde_json::to_string(&entry)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
        
        if let Some(ref mut writer) = self.writer {
            writeln!(writer, "{}", json)?;
            writer.flush()?;
        }
        
        self.entry_count += 1;
        Ok(())
    }
    
    /// Read all entries from the WAL file
    pub fn read_all(&self) -> io::Result<Vec<VectorWALEntry>> {
        if !self.enabled || !self.path.exists() {
            return Ok(Vec::new());
        }
        
        let file = File::open(&self.path)?;
        let reader = BufReader::new(file);
        let mut entries = Vec::new();
        
        for line in reader.lines() {
            let line = line?;
            if line.trim().is_empty() {
                continue;
            }
            if let Ok(entry) = serde_json::from_str::<VectorWALEntry>(&line) {
                entries.push(entry);
            } else {
                tracing::warn!("Skipping invalid vector WAL entry: {}", &line[..line.len().min(100)]);
            }
        }
        
        Ok(entries)
    }
    
    /// Truncate the WAL file (after snapshot or when safe)
    pub fn truncate(&mut self) -> io::Result<()> {
        if !self.enabled {
            return Ok(());
        }
        
        // Close current writer
        self.close()?;
        
        // Truncate file
        if self.path.exists() {
            File::create(&self.path)?;
        }
        
        self.entry_count = 0;
        Ok(())
    }
    
    /// Flush and sync to disk
    pub fn sync(&mut self) -> io::Result<()> {
        if let Some(ref mut writer) = self.writer {
            writer.flush()?;
            writer.get_ref().sync_data()?;
        }
        Ok(())
    }
    
    /// Close the WAL writer
    pub fn close(&mut self) -> io::Result<()> {
        if let Some(ref mut writer) = self.writer {
            writer.flush()?;
        }
        self.writer = None;
        Ok(())
    }
}

impl Drop for VectorWAL {
    fn drop(&mut self) {
        let _ = self.close();
    }
}

/// Get current timestamp in milliseconds
fn current_timestamp() -> i64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as i64
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;
    
    #[test]
    fn test_vector_wal_basic() {
        let dir = tempdir().unwrap();
        let wal_path = dir.path().join("vector.wal");
        
        // Write entries
        {
            let mut wal = VectorWAL::new(&wal_path).unwrap();
            
            wal.append_index("entity-1", &[0.1, 0.2, 0.3]).unwrap();
            wal.append_index("entity-2", &[0.4, 0.5, 0.6]).unwrap();
            wal.append_remove("entity-1").unwrap();
            wal.close().unwrap();
        }
        
        // Read entries
        let wal = VectorWAL::new(&wal_path).unwrap();
        let entries = wal.read_all().unwrap();
        
        assert_eq!(entries.len(), 3);
        assert_eq!(entries[0].op, VectorWALOp::Index);
        assert_eq!(entries[0].entity_id, "entity-1");
        assert_eq!(entries[0].embedding.as_ref().unwrap(), &[0.1, 0.2, 0.3]);
        
        assert_eq!(entries[1].op, VectorWALOp::Index);
        assert_eq!(entries[1].entity_id, "entity-2");
        
        assert_eq!(entries[2].op, VectorWALOp::Remove);
        assert_eq!(entries[2].entity_id, "entity-1");
        assert!(entries[2].embedding.is_none());
    }
    
    #[test]
    fn test_vector_wal_truncate() {
        let dir = tempdir().unwrap();
        let wal_path = dir.path().join("vector.wal");
        
        let mut wal = VectorWAL::new(&wal_path).unwrap();
        wal.append_index("entity-1", &[0.1, 0.2, 0.3]).unwrap();
        assert_eq!(wal.entry_count(), 1);
        
        wal.truncate().unwrap();
        assert_eq!(wal.entry_count(), 0);
        
        let entries = wal.read_all().unwrap();
        assert!(entries.is_empty());
    }
    
    #[test]
    fn test_vector_wal_disabled() {
        let wal = VectorWAL::disabled();
        assert!(!wal.is_enabled());
        
        let entries = wal.read_all().unwrap();
        assert!(entries.is_empty());
    }
    
    #[test]
    fn test_vector_wal_recovery_idempotent() {
        let dir = tempdir().unwrap();
        let wal_path = dir.path().join("vector.wal");
        
        // Simulate: index entity-1, index entity-2, remove entity-1, re-index entity-1 with new embedding
        {
            let mut wal = VectorWAL::new(&wal_path).unwrap();
            wal.append_index("entity-1", &[0.1, 0.2, 0.3]).unwrap();
            wal.append_index("entity-2", &[0.4, 0.5, 0.6]).unwrap();
            wal.append_remove("entity-1").unwrap();
            wal.append_index("entity-1", &[0.7, 0.8, 0.9]).unwrap();
            wal.close().unwrap();
        }
        
        // Read and verify the replay order
        let wal = VectorWAL::new(&wal_path).unwrap();
        let entries = wal.read_all().unwrap();
        
        assert_eq!(entries.len(), 4);
        
        // After replay: entity-1 should have [0.7, 0.8, 0.9], entity-2 should have [0.4, 0.5, 0.6]
        // The caller (SocialStoreV2::recover_vector_index) is responsible for applying in order
    }
    
    #[test]
    fn test_vector_wal_append_after_reopen() {
        let dir = tempdir().unwrap();
        let wal_path = dir.path().join("vector.wal");
        
        // First session
        {
            let mut wal = VectorWAL::new(&wal_path).unwrap();
            wal.append_index("entity-1", &[0.1, 0.2]).unwrap();
            wal.close().unwrap();
        }
        
        // Second session (reopen and append)
        {
            let mut wal = VectorWAL::new(&wal_path).unwrap();
            wal.append_index("entity-2", &[0.3, 0.4]).unwrap();
            wal.close().unwrap();
        }
        
        // Read all
        let wal = VectorWAL::new(&wal_path).unwrap();
        let entries = wal.read_all().unwrap();
        
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].entity_id, "entity-1");
        assert_eq!(entries[1].entity_id, "entity-2");
    }
}
