//! Commit-time application of a `GraphMutationBatch` to a `BoardGraph`.
//!
//! Resolves every `NodeRef::LocalIndex` to its allocated `BoardNodeId`,
//! cascades `RemoveNode` to also remove edges/faces touching that
//! node, and produces a `Vec<GraphOp>` log of what was applied.

use crate::board::{BoardGraph, EdgeKind};
use crate::board::ids::{EdgeId, FaceId, BoardNodeId};
use crate::board::mutation::{GraphMutation, GraphMutationBatch, GraphOp, NodeRef, ProposedEdge};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyError {
    UnknownLocalIndex(u32),
    UnknownExistingNode(BoardNodeId),
    UnknownEdge(EdgeId),
    UnknownFace(FaceId),
}

pub fn apply_batch(graph: &mut BoardGraph, batch: &GraphMutationBatch)
    -> Result<Vec<GraphOp>, ApplyError>
{
    let mut log = Vec::new();
    let mut local_ids: Vec<BoardNodeId> = Vec::new();  // index → allocated BoardNodeId
    for op in &batch.ops {
        match op {
            GraphMutation::AddNode => {
                let id = graph.add_node();
                local_ids.push(id);
                log.push(GraphOp::NodeAdded { id });
            }
            GraphMutation::AddEdge { kind } => {
                let resolved = resolve_proposed_edge(kind, &local_ids, graph)?;
                let id = match resolved {
                    EdgeKind::Bidirectional { a, b } => graph.add_bidirectional_edge(a, b),
                    EdgeKind::Directed { from, to } => graph.add_directed_edge(from, to),
                };
                log.push(GraphOp::EdgeAdded { id, kind: resolved });
            }
            GraphMutation::AddFace { bounding_nodes } => {
                let resolved = bounding_nodes.iter()
                    .map(|n| resolve_node_ref(n, &local_ids, graph))
                    .collect::<Result<Vec<_>, _>>()?;
                let id = graph.try_add_face(resolved.clone())
                    .map_err(|_| ApplyError::UnknownLocalIndex(0))?;
                log.push(GraphOp::FaceAdded { id, bounding_nodes: resolved });
            }
            GraphMutation::RemoveNode(n) => {
                if !graph.contains_node(*n) {
                    return Err(ApplyError::UnknownExistingNode(*n));
                }
                // Cascade: remove every edge touching n, every face whose
                // boundary contains n, then n itself. Order is
                // edges → faces → node.
                let edges_to_remove: Vec<EdgeId> = graph.edges_at(*n);
                for e in edges_to_remove {
                    graph.edges.remove(&e);
                    log.push(GraphOp::EdgeRemoved { id: e });
                }
                let faces_to_remove: Vec<FaceId> = graph.faces_at_node(*n);
                for f in faces_to_remove {
                    graph.faces.remove(&f);
                    log.push(GraphOp::FaceRemoved { id: f });
                }
                graph.nodes.remove(n);
                log.push(GraphOp::NodeRemoved { id: *n });
            }
            GraphMutation::RemoveEdge(e) => {
                if graph.edges.remove(e).is_none() {
                    return Err(ApplyError::UnknownEdge(*e));
                }
                log.push(GraphOp::EdgeRemoved { id: *e });
            }
            GraphMutation::RemoveFace(f) => {
                if graph.faces.remove(f).is_none() {
                    return Err(ApplyError::UnknownFace(*f));
                }
                log.push(GraphOp::FaceRemoved { id: *f });
            }
        }
    }
    Ok(log)
}

impl BoardGraph {
    /// Replay a committed `GraphOp` onto this graph. Used by clients
    /// (and any other read-side mirror) to keep their copy in sync
    /// with the server's authoritative graph after the server applies
    /// a `GraphMutationBatch` and broadcasts the resulting ops.
    ///
    /// Adds bump the `next_*_id` cursor past the op's id when it's
    /// ahead of the current value — the server-allocated id may have
    /// been issued from a different baseline than this mirror's
    /// counter (typically equal in practice, but defensive).
    ///
    /// Removes are unconditional — the server has already validated
    /// that the id existed at commit time, and any cascades arrive as
    /// their own subsequent ops in the same batch.
    pub fn apply_op(&mut self, op: &GraphOp) {
        match op {
            GraphOp::NodeAdded { id } => {
                self.nodes.insert(*id);
                if id.0 + 1 > self.next_node_id { self.next_node_id = id.0 + 1; }
            }
            GraphOp::EdgeAdded { id, kind } => {
                self.edges.insert(*id, kind.clone());
                if id.0 + 1 > self.next_edge_id { self.next_edge_id = id.0 + 1; }
            }
            GraphOp::FaceAdded { id, bounding_nodes } => {
                self.faces.insert(*id, bounding_nodes.clone());
                if id.0 + 1 > self.next_face_id { self.next_face_id = id.0 + 1; }
            }
            GraphOp::NodeRemoved { id } => { self.nodes.remove(id); }
            GraphOp::EdgeRemoved { id } => { self.edges.remove(id); }
            GraphOp::FaceRemoved { id } => { self.faces.remove(id); }
        }
    }

    /// Replay a slice of committed ops in order.
    pub fn apply_ops(&mut self, ops: &[GraphOp]) {
        for op in ops { self.apply_op(op); }
    }
}

fn resolve_node_ref(r: &NodeRef, local_ids: &[BoardNodeId], graph: &BoardGraph)
    -> Result<BoardNodeId, ApplyError>
{
    match r {
        NodeRef::Existing(id) => {
            if !graph.contains_node(*id) {
                return Err(ApplyError::UnknownExistingNode(*id));
            }
            Ok(*id)
        }
        NodeRef::LocalIndex(i) => local_ids
            .get(*i as usize)
            .copied()
            .ok_or(ApplyError::UnknownLocalIndex(*i)),
    }
}

fn resolve_proposed_edge(kind: &ProposedEdge, local_ids: &[BoardNodeId], graph: &BoardGraph)
    -> Result<EdgeKind, ApplyError>
{
    Ok(match kind {
        ProposedEdge::Bidirectional { a, b } => EdgeKind::Bidirectional {
            a: resolve_node_ref(a, local_ids, graph)?,
            b: resolve_node_ref(b, local_ids, graph)?,
        },
        ProposedEdge::Directed { from, to } => EdgeKind::Directed {
            from: resolve_node_ref(from, local_ids, graph)?,
            to:   resolve_node_ref(to, local_ids, graph)?,
        },
    })
}

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

    #[test]
    fn add_node_node_edge_with_local_index_resolves() {
        let mut g = BoardGraph::new();
        let batch = GraphMutationBatch { ops: vec![
            GraphMutation::AddNode,
            GraphMutation::AddNode,
            GraphMutation::AddEdge { kind: ProposedEdge::Bidirectional {
                a: NodeRef::LocalIndex(0),
                b: NodeRef::LocalIndex(1),
            }},
        ]};
        let log = apply_batch(&mut g, &batch).unwrap();
        assert_eq!(g.node_count(), 2);
        assert_eq!(g.edge_count(), 1);
        assert!(matches!(log[0], GraphOp::NodeAdded { .. }));
        assert!(matches!(log[1], GraphOp::NodeAdded { .. }));
        assert!(matches!(log[2], GraphOp::EdgeAdded { .. }));
    }

    #[test]
    fn remove_node_cascades_edges_and_faces() {
        let mut g = BoardGraph::new();
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        g.add_bidirectional_edge(a, b);
        g.add_bidirectional_edge(b, c);
        g.add_bidirectional_edge(a, c);
        let _f = g.try_add_face(vec![a, b, c]).unwrap();

        let batch = GraphMutationBatch { ops: vec![GraphMutation::RemoveNode(b)] };
        let log = apply_batch(&mut g, &batch).unwrap();

        // The face references b; cascade should have removed it.
        assert_eq!(g.face_count(), 0);
        // Edges touching b (a-b and b-c) gone; a-c remains.
        assert_eq!(g.edge_count(), 1);
        // b is gone.
        assert!(!g.contains_node(b));
        assert!(g.contains_node(a));
        assert!(g.contains_node(c));
        // Log order: EdgeRemoved (for each touching edge) → FaceRemoved → NodeRemoved.
        let last = log.last().unwrap();
        assert!(matches!(last, GraphOp::NodeRemoved { id } if *id == b));
    }

    #[test]
    fn local_index_out_of_range_errors() {
        let mut g = BoardGraph::new();
        let batch = GraphMutationBatch { ops: vec![
            GraphMutation::AddEdge { kind: ProposedEdge::Bidirectional {
                a: NodeRef::LocalIndex(0),
                b: NodeRef::LocalIndex(1),
            }},
        ]};
        let res = apply_batch(&mut g, &batch);
        assert!(matches!(res, Err(ApplyError::UnknownLocalIndex(_))));
    }

    #[test]
    fn apply_ops_replays_committed_log_into_empty_graph() {
        // Build a small graph + capture the op log via apply_batch.
        let mut server = BoardGraph::new();
        let batch = GraphMutationBatch { ops: vec![
            GraphMutation::AddNode,
            GraphMutation::AddNode,
            GraphMutation::AddEdge { kind: ProposedEdge::Bidirectional {
                a: NodeRef::LocalIndex(0),
                b: NodeRef::LocalIndex(1),
            }},
        ]};
        let log = apply_batch(&mut server, &batch).unwrap();

        // Replay the log on a fresh client mirror; it should match the server.
        let mut client = BoardGraph::new();
        client.apply_ops(&log);
        assert_eq!(client.node_count(), server.node_count());
        assert_eq!(client.edge_count(), server.edge_count());
        assert_eq!(client.face_count(), server.face_count());
    }

    #[test]
    fn apply_ops_round_trips_a_built_grid() {
        // Square grid is the realistic case: lots of nodes + edges + faces
        // built via the public constructors. Snapshot it, mutate the snapshot
        // via apply_ops on a fresh graph, confirm parity.
        let server = crate::board::BoardBuilder::square_grid(2, 2).graph;
        let snap = crate::board::BoardGraphSnapshot::from_graph("", &server);
        let restored = snap.into_graph();
        assert_eq!(restored.node_count(), server.node_count());
        assert_eq!(restored.edge_count(), server.edge_count());
        assert_eq!(restored.face_count(), server.face_count());
    }

    #[test]
    fn apply_op_advances_next_id_cursors() {
        let mut g = BoardGraph::new();
        // Replay an op naming a high id; subsequent local add_node should
        // not collide.
        g.apply_op(&GraphOp::NodeAdded { id: BoardNodeId(7) });
        let next = g.add_node();
        assert!(next.0 >= 8, "expected next_node_id > 7, got {}", next.0);
    }

    #[test]
    fn apply_op_remove_clears_local_state() {
        let mut g = BoardGraph::new();
        let n = g.add_node();
        g.apply_op(&GraphOp::NodeRemoved { id: n });
        assert!(!g.contains_node(n));
    }
}
