//! Undo log for rollback of multi-statement Cypher scripts.
//!
//! Each mutation recorded in the undo log captures the inverse operation
//! needed to restore the graph to its previous state.  When a statement
//! in a multi-statement script fails, the executor applies the undo log
//! in reverse order to roll back all prior mutations.

use std::collections::HashSet;

use crate::graph::{GraphBackend, GraphEdge, GraphNode, PropertyValue};

/// A single inverse operation that undoes a graph mutation.
#[derive(Debug, Clone)]
pub enum UndoEntry {
    /// Undo a CREATE node — delete it (detach to remove any edges created alongside).
    DeleteNode { id: u64, detach: bool },

    /// Undo a CREATE relationship — delete it.
    DeleteRelationship { id: u64 },

    /// Undo a DELETE node — re-create it with the captured state.
    RestoreNode {
        node: GraphNode,
    },

    /// Undo a DELETE relationship — re-create it with the captured state.
    RestoreRelationship {
        edge: GraphEdge,
        start_id: u64,
        end_id: u64,
    },

    /// Undo a SET property — restore the old value (or remove if it was absent).
    RestoreNodeProperty {
        node_id: u64,
        key: String,
        old_value: Option<PropertyValue>,
    },

    /// Undo a SET edge property.
    RestoreEdgeProperty {
        edge_id: u64,
        key: String,
        old_value: Option<PropertyValue>,
    },

    /// Undo an ADD label — remove it.
    RemoveLabel { node_id: u64, label: String },

    /// Undo a REMOVE label — add it back.
    AddLabel { node_id: u64, label: String },
}

/// Accumulates undo entries for a batch of mutations.
#[derive(Debug, Default)]
pub struct UndoLog {
    entries: Vec<UndoEntry>,
}

impl UndoLog {
    pub fn new() -> Self {
        Self { entries: Vec::new() }
    }

    /// Record an undo entry.
    pub fn push(&mut self, entry: UndoEntry) {
        self.entries.push(entry);
    }

    /// Record that a node was created (undo = detach-delete it).
    pub fn record_node_created(&mut self, id: u64) {
        self.entries.push(UndoEntry::DeleteNode { id, detach: true });
    }

    /// Record that a relationship was created (undo = delete it).
    pub fn record_relationship_created(&mut self, id: u64) {
        self.entries.push(UndoEntry::DeleteRelationship { id });
    }

    /// Record that a node property was set (capture old value for rollback).
    pub fn record_node_property_set(
        &mut self,
        node_id: u64,
        key: String,
        old_value: Option<PropertyValue>,
    ) {
        self.entries.push(UndoEntry::RestoreNodeProperty {
            node_id,
            key,
            old_value,
        });
    }

    /// Record that an edge property was set (capture old value for rollback).
    pub fn record_edge_property_set(
        &mut self,
        edge_id: u64,
        key: String,
        old_value: Option<PropertyValue>,
    ) {
        self.entries.push(UndoEntry::RestoreEdgeProperty {
            edge_id,
            key,
            old_value,
        });
    }

    /// Record that a label was added to a node.
    pub fn record_label_added(&mut self, node_id: u64, label: String) {
        self.entries.push(UndoEntry::RemoveLabel { node_id, label });
    }

    /// Record that a label was removed from a node.
    pub fn record_label_removed(&mut self, node_id: u64, label: String) {
        self.entries.push(UndoEntry::AddLabel { node_id, label });
    }

    /// Returns true if the log is empty.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Apply all undo entries in reverse order to roll back mutations.
    ///
    /// Best-effort: individual undo steps that fail are logged but do not
    /// stop the rollback from continuing.  This is necessary because the
    /// graph may be in a partially-mutated state.
    pub fn rollback<G: GraphBackend>(&self, graph: &mut G) {
        // Collect node IDs that we are about to delete during rollback
        // (these are nodes that were *created* and need to be undone).
        let nodes_to_delete: HashSet<u64> = self.entries.iter().filter_map(|e| {
            if let UndoEntry::DeleteNode { id, .. } = e { Some(*id) } else { None }
        }).collect();

        for entry in self.entries.iter().rev() {
            match entry {
                UndoEntry::DeleteNode { id, detach } => {
                    if *detach {
                        let _ = graph.detach_delete_node(*id);
                    } else {
                        let _ = graph.delete_node(*id);
                    }
                }
                UndoEntry::DeleteRelationship { id } => {
                    let _ = graph.delete_relationship(*id);
                }
                UndoEntry::RestoreNode { node } => {
                    // Re-create the node with its original labels and properties.
                    let labels: Vec<String> = node.labels.iter().cloned().collect();
                    graph.create_node(labels, node.properties.clone());
                }
                UndoEntry::RestoreRelationship { edge, start_id, end_id } => {
                    // Skip if either endpoint is being deleted in this rollback
                    // (the node restore hasn't happened yet in reverse order, or
                    // the node was created-then-deleted in the same batch).
                    if nodes_to_delete.contains(start_id) || nodes_to_delete.contains(end_id) {
                        continue;
                    }
                    let _ = graph.create_relationship(
                        *start_id,
                        *end_id,
                        edge.rel_type.clone(),
                        edge.properties.clone(),
                    );
                }
                UndoEntry::RestoreNodeProperty { node_id, key, old_value } => {
                    if nodes_to_delete.contains(node_id) {
                        continue;
                    }
                    match old_value {
                        Some(val) => {
                            let _ = graph.set_node_property(*node_id, key.clone(), val.clone());
                        }
                        None => {
                            let _ = graph.remove_node_property(*node_id, key);
                        }
                    }
                }
                UndoEntry::RestoreEdgeProperty { edge_id, key, old_value } => {
                    match old_value {
                        Some(val) => {
                            let _ = graph.set_edge_property(*edge_id, key.clone(), val.clone());
                        }
                        None => {
                            let _ = graph.remove_edge_property(*edge_id, key);
                        }
                    }
                }
                UndoEntry::RemoveLabel { node_id, label } => {
                    if nodes_to_delete.contains(node_id) {
                        continue;
                    }
                    let _ = graph.remove_label(*node_id, label);
                }
                UndoEntry::AddLabel { node_id, label } => {
                    if nodes_to_delete.contains(node_id) {
                        continue;
                    }
                    let _ = graph.add_label(*node_id, label.clone());
                }
            }
        }
    }
}
