//! Standalone Write-Ahead Log for PropertyGraph persistence.
//!
//! Provides the same WAL semantics as the distributed WAL, but works
//! directly with `PropertyGraph` (no partition awareness needed).
//!
//! ## Usage
//!
//! ```ignore
//! use ocg::persistence::wal::{StandaloneWal, StandaloneWalConfig};
//! use ocg::graph::PropertyGraph;
//!
//! let cfg = StandaloneWalConfig {
//!     enabled: true,
//!     dir: "/tmp/my-graph".to_string(),
//!     sync_mode: StandaloneWalSyncMode::Async,
//!     max_entries_before_checkpoint: 1000,
//! };
//!
//! let mut wal = StandaloneWal::open(&cfg).unwrap();
//! ```

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

use crate::error::{CypherError, CypherResult, ExecutionError};
use crate::graph::{PropertyGraph, PropertyValue};

macro_rules! wal_err {
    ($msg:expr) => {
        CypherError::Execution(ExecutionError::Internal($msg.to_string()))
    };
}

/// Sync mode for the standalone WAL.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum StandaloneWalSyncMode {
    Fsync,
    Async,
}

/// Configuration for the standalone WAL.
#[derive(Debug, Clone)]
pub struct StandaloneWalConfig {
    pub enabled: bool,
    pub dir: String,
    pub sync_mode: StandaloneWalSyncMode,
    pub max_entries_before_checkpoint: u64,
}

impl Default for StandaloneWalConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            dir: String::new(),
            sync_mode: StandaloneWalSyncMode::Fsync,
            max_entries_before_checkpoint: 10_000,
        }
    }
}

/// All graph-mutating operations that can be logged.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "op", content = "data")]
pub enum WalOp {
    CreateNode {
        id: u64,
        labels: Vec<String>,
        properties: indexmap::IndexMap<String, PropertyValue>,
    },
    CreateRelationship {
        id: u64,
        src: u64,
        dst: u64,
        rel_type: String,
        properties: indexmap::IndexMap<String, PropertyValue>,
    },
    DeleteNode { id: u64 },
    DeleteRelationship { id: u64 },
    SetNodeProperty { id: u64, key: String, value: PropertyValue },
    SetEdgeProperty { id: u64, key: String, value: PropertyValue },
    RemoveNodeProperty { id: u64, key: String },
    RemoveEdgeProperty { id: u64, key: String },
    AddLabel { id: u64, label: String },
    RemoveLabel { id: u64, label: String },
}

/// A single WAL entry.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct WalEntry {
    lsn: u64,
    ts: String,
    #[serde(flatten)]
    op: WalOp,
}

/// Standalone Write-Ahead Log handle.
pub struct StandaloneWal {
    log_path: PathBuf,
    file: Option<File>,
    sync_mode: StandaloneWalSyncMode,
    max_entries: u64,
    next_lsn: u64,
    entries_since_checkpoint: u64,
}

impl StandaloneWal {
    /// Open (or create) the WAL at the configured directory.
    pub fn open(config: &StandaloneWalConfig) -> CypherResult<Self> {
        let dir = PathBuf::from(&config.dir);
        let log_path = dir.join("wal.ndjson");

        if let Some(parent) = log_path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| wal_err!(format!("WAL: create dir {}: {}", parent.display(), e)))?;
        }

        let existing = if log_path.exists() {
            let f = File::open(&log_path)
                .map_err(|e| wal_err!(format!("WAL: open for counting: {}", e)))?;
            BufReader::new(f)
                .lines()
                .filter(|l| l.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false))
                .count() as u64
        } else {
            0
        };

        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&log_path)
            .map_err(|e| wal_err!(format!("WAL: open {}: {}", log_path.display(), e)))?;

        Ok(Self {
            log_path,
            file: Some(file),
            sync_mode: config.sync_mode.clone(),
            max_entries: config.max_entries_before_checkpoint,
            next_lsn: existing + 1,
            entries_since_checkpoint: existing,
        })
    }

    /// Append a WAL operation. Returns the LSN.
    pub fn append(&mut self, op: WalOp) -> CypherResult<u64> {
        let lsn = self.next_lsn;
        let ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();

        let entry = WalEntry { lsn, ts, op };
        let mut line = serde_json::to_string(&entry)
            .map_err(|e| wal_err!(format!("WAL: serialize: {}", e)))?;
        line.push('\n');

        let file = self.file.as_mut().ok_or_else(|| wal_err!("WAL: not open"))?;
        file.write_all(line.as_bytes())
            .map_err(|e| wal_err!(format!("WAL: write: {}", e)))?;

        if matches!(self.sync_mode, StandaloneWalSyncMode::Fsync) {
            file.sync_data()
                .map_err(|e| wal_err!(format!("WAL: fsync: {}", e)))?;
        }

        self.next_lsn += 1;
        self.entries_since_checkpoint += 1;
        Ok(lsn)
    }

    /// Truncate the WAL (call after a successful snapshot).
    pub fn checkpoint(&mut self) -> CypherResult<()> {
        self.file = None;
        File::create(&self.log_path)
            .map_err(|e| wal_err!(format!("WAL: truncate: {}", e)))?;
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.log_path)
            .map_err(|e| wal_err!(format!("WAL: reopen: {}", e)))?;
        self.file = Some(file);
        self.next_lsn = 1;
        self.entries_since_checkpoint = 0;
        Ok(())
    }

    /// Replay all WAL entries into a PropertyGraph.
    pub fn replay(&self, graph: &mut PropertyGraph) -> CypherResult<usize> {
        if !self.log_path.exists() {
            return Ok(0);
        }

        let f = File::open(&self.log_path)
            .map_err(|e| wal_err!(format!("WAL: open for replay: {}", e)))?;
        let reader = BufReader::new(f);
        let mut count = 0usize;

        for (line_no, line_result) in reader.lines().enumerate() {
            let line = line_result
                .map_err(|e| wal_err!(format!("WAL: read line {}: {}", line_no + 1, e)))?;
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            let entry: WalEntry = serde_json::from_str(trimmed)
                .map_err(|e| wal_err!(format!("WAL: parse line {}: {}", line_no + 1, e)))?;

            Self::apply_op(graph, entry.op)?;
            count += 1;
        }

        Ok(count)
    }

    /// Returns true when the log exceeds the checkpoint threshold.
    pub fn needs_checkpoint(&self) -> bool {
        self.entries_since_checkpoint >= self.max_entries
    }

    /// Return the path to the WAL file.
    pub fn log_path(&self) -> &std::path::Path {
        &self.log_path
    }

    fn apply_op(graph: &mut PropertyGraph, op: WalOp) -> CypherResult<()> {
        match op {
            WalOp::CreateNode { id, labels, properties } => {
                use crate::graph::GraphNode;
                let mut node = GraphNode::new(id);
                node.properties = properties;
                for label in labels {
                    node.labels.insert(label);
                }
                graph.add_existing_node(node);
            }
            WalOp::CreateRelationship { id, src, dst, rel_type, properties } => {
                graph.create_relationship_with_id(id, src, dst, rel_type, properties)
                    .map_err(|e| wal_err!(format!("replay CreateRelationship: {}", e)))?;
            }
            WalOp::DeleteNode { id } => {
                let _ = graph.delete_node(id);
            }
            WalOp::DeleteRelationship { id } => {
                let _ = graph.delete_relationship(id);
            }
            WalOp::SetNodeProperty { id, key, value } => {
                let _ = graph.set_node_property(id, key, value);
            }
            WalOp::SetEdgeProperty { id, key, value } => {
                let _ = graph.set_edge_property(id, key, value);
            }
            WalOp::RemoveNodeProperty { id, key } => {
                let _ = graph.remove_node_property(id, &key);
            }
            WalOp::RemoveEdgeProperty { id, key } => {
                let _ = graph.remove_edge_property(id, &key);
            }
            WalOp::AddLabel { id, label } => {
                let _ = graph.add_label(id, label);
            }
            WalOp::RemoveLabel { id, label } => {
                let _ = graph.remove_label(id, &label);
            }
        }
        Ok(())
    }
}

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

    fn make_config(dir: &std::path::Path) -> StandaloneWalConfig {
        StandaloneWalConfig {
            enabled: true,
            dir: dir.to_string_lossy().into_owned(),
            sync_mode: StandaloneWalSyncMode::Async,
            max_entries_before_checkpoint: 10_000,
        }
    }

    #[test]
    fn test_standalone_wal_round_trip() {
        let tmp = TempDir::new().unwrap();
        let cfg = make_config(tmp.path());
        let mut wal = StandaloneWal::open(&cfg).unwrap();

        // Log some operations
        wal.append(WalOp::CreateNode {
            id: 1,
            labels: vec!["Person".to_string()],
            properties: {
                let mut p = indexmap::IndexMap::new();
                p.insert("name".to_string(), PropertyValue::String("Alice".to_string()));
                p
            },
        }).unwrap();
        wal.append(WalOp::CreateNode {
            id: 2,
            labels: vec!["Person".to_string()],
            properties: indexmap::IndexMap::new(),
        }).unwrap();
        wal.append(WalOp::CreateRelationship {
            id: 1, src: 1, dst: 2,
            rel_type: "KNOWS".to_string(),
            properties: indexmap::IndexMap::new(),
        }).unwrap();

        // Replay into a fresh graph
        let mut graph = PropertyGraph::new();
        let replayed = wal.replay(&mut graph).unwrap();

        assert_eq!(replayed, 3);
        assert_eq!(graph.node_count(), 2);
        assert_eq!(graph.edge_count(), 1);
        assert!(graph.get_node(1).is_some());
        assert_eq!(
            graph.get_node(1).unwrap().properties.get("name"),
            Some(&PropertyValue::String("Alice".to_string()))
        );
    }

    #[test]
    fn test_standalone_wal_checkpoint() {
        let tmp = TempDir::new().unwrap();
        let cfg = make_config(tmp.path());
        let mut wal = StandaloneWal::open(&cfg).unwrap();

        wal.append(WalOp::DeleteNode { id: 1 }).unwrap();
        wal.append(WalOp::DeleteNode { id: 2 }).unwrap();
        assert_eq!(wal.entries_since_checkpoint, 2);

        wal.checkpoint().unwrap();
        assert_eq!(wal.entries_since_checkpoint, 0);

        // After checkpoint, replay should find nothing
        let mut graph = PropertyGraph::new();
        assert_eq!(wal.replay(&mut graph).unwrap(), 0);
    }

    #[test]
    fn test_standalone_wal_needs_checkpoint() {
        let tmp = TempDir::new().unwrap();
        let mut cfg = make_config(tmp.path());
        cfg.max_entries_before_checkpoint = 2;
        let mut wal = StandaloneWal::open(&cfg).unwrap();

        assert!(!wal.needs_checkpoint());
        wal.append(WalOp::DeleteNode { id: 1 }).unwrap();
        assert!(!wal.needs_checkpoint());
        wal.append(WalOp::DeleteNode { id: 2 }).unwrap();
        assert!(wal.needs_checkpoint());
    }
}
