//! Unified planar-graph board representation.
//!
//! See `docs/superpowers/specs/2026-04-28-board-graph-design.md`.

pub mod ids;
pub mod edge;
pub mod primitives;
pub mod queries;
pub mod builders;
pub mod introspect;
pub mod snapshot;
pub mod mutation;
pub mod apply;
pub use builders::BoardBuilder;
pub use snapshot::BoardGraphSnapshot;
pub use mutation::{GraphMutation, GraphMutationBatch, GraphOp, NodeRef, ProposedEdge};

pub use ids::{BoardNodeId, EdgeId, FaceId};
pub use edge::EdgeKind;
pub use view::BoardGraphView;

pub mod distance;
pub mod view;

use std::collections::{BTreeMap, BTreeSet};

/// Coordinate metadata attached to a face. Builders that build a
/// coordinate-bearing topology (e.g. `BoardBuilder::square_grid`) populate
/// this; topology-only graphs leave faces with no coord.
///
/// Cartesian distance metrics (Manhattan/Chebyshev/Euclidean) require this
/// data on both faces; `crate::distance::DistanceMetric::GraphHops` does not.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FaceCoord {
    /// (col, row) on a Cartesian (square-grid) board.
    Cartesian { col: i32, row: i32 },
    /// UC-04-3 — axial hex coordinates. Cube coordinate `s` is derived:
    /// `s = -q - r`. Used by `DistanceMetric::Hex` (cube distance).
    Hex { q: i32, r: i32 },
}

/// Pure topology — coordinates are stored as data on `face_coords`, not
/// computed from FaceId via stride math. Builders register coords as they
/// construct topology; custom plugin-built graphs may register nothing.
/// Mutates over the session via typed events; queries via `queries.rs`.
#[derive(Debug, Clone, Default)]
pub struct BoardGraph {
    pub(crate) nodes: BTreeSet<BoardNodeId>,
    pub(crate) edges: BTreeMap<EdgeId, EdgeKind>,
    pub(crate) faces: BTreeMap<FaceId, Vec<BoardNodeId>>,
    pub(crate) next_node_id: u32,
    pub(crate) next_edge_id: u32,
    pub(crate) next_face_id: u32,
    /// AR-1: per-face coordinate metadata. Populated by coord-aware
    /// builders. Faces created by raw `try_add_face` start without coords.
    pub(crate) face_coords:   BTreeMap<FaceId, FaceCoord>,
    /// O(1) reverse lookup: `coord_to_face[FaceCoord] -> FaceId`. Kept in
    /// sync with `face_coords`.
    pub(crate) coord_to_face: BTreeMap<FaceCoord, FaceId>,
}

impl BoardGraph {
    pub fn new() -> Self { Self::default() }

    pub fn node_count(&self) -> usize { self.nodes.len() }
    pub fn edge_count(&self) -> usize { self.edges.len() }
    pub fn face_count(&self) -> usize { self.faces.len() }

    pub fn contains_node(&self, id: BoardNodeId) -> bool { self.nodes.contains(&id) }
    pub fn contains_edge(&self, id: EdgeId) -> bool { self.edges.contains_key(&id) }
    pub fn contains_face(&self, id: FaceId) -> bool { self.faces.contains_key(&id) }

    pub fn add_node(&mut self) -> BoardNodeId {
        let id = BoardNodeId(self.next_node_id);
        self.next_node_id += 1;
        self.nodes.insert(id);
        id
    }

    pub fn add_bidirectional_edge(&mut self, a: BoardNodeId, b: BoardNodeId) -> EdgeId {
        debug_assert!(self.contains_node(a), "from-node missing: {a:?}");
        debug_assert!(self.contains_node(b), "to-node missing: {b:?}");
        let id = EdgeId(self.next_edge_id);
        self.next_edge_id += 1;
        self.edges.insert(id, EdgeKind::Bidirectional { a, b });
        id
    }

    pub fn add_directed_edge(&mut self, from: BoardNodeId, to: BoardNodeId) -> EdgeId {
        debug_assert!(self.contains_node(from), "from-node missing: {from:?}");
        debug_assert!(self.contains_node(to), "to-node missing: {to:?}");
        let id = EdgeId(self.next_edge_id);
        self.next_edge_id += 1;
        self.edges.insert(id, EdgeKind::Directed { from, to });
        id
    }

    pub fn edge_kind(&self, id: EdgeId) -> Option<&EdgeKind> {
        self.edges.get(&id)
    }

    /// Iterator over all node ids in ascending order.
    pub fn nodes_iter(&self) -> impl Iterator<Item = BoardNodeId> + '_ {
        self.nodes.iter().copied()
    }

    /// Iterator over all face ids in ascending order.
    pub fn faces_iter(&self) -> impl Iterator<Item = FaceId> + '_ {
        self.faces.keys().copied()
    }

    /// Iterator yielding (EdgeId, &EdgeKind) pairs in ascending edge-id order.
    pub fn edges_iter(&self) -> impl Iterator<Item = (EdgeId, &EdgeKind)> + '_ {
        self.edges.iter().map(|(id, kind)| (*id, kind))
    }

    /// AR-1: register a `FaceCoord` for `face`. Builders call this as
    /// they create coord-bearing topology. Replaces any existing coord
    /// for that face. The `coord_to_face` reverse index is kept in sync.
    ///
    /// Panics in debug if `face` is not in the graph; release builds
    /// silently no-op (no observable harm — face simply lacks a coord).
    pub fn register_face_coord(&mut self, face: FaceId, coord: FaceCoord) {
        debug_assert!(
            self.contains_face(face),
            "register_face_coord called for unknown face {face:?}"
        );
        if !self.contains_face(face) {
            return;
        }
        // Remove any prior reverse-index entry pointing at this face.
        if let Some(prior) = self.face_coords.insert(face, coord) {
            self.coord_to_face.remove(&prior);
        }
        self.coord_to_face.insert(coord, face);
    }

    /// AR-1: O(1) reverse lookup. Returns the FaceId at the given
    /// Cartesian coord, if any face was registered there.
    pub fn face_at_cartesian(&self, col: i32, row: i32) -> Option<FaceId> {
        self.coord_to_face
            .get(&FaceCoord::Cartesian { col, row })
            .copied()
    }

    /// UC-04-3: O(1) reverse lookup for hex-axial coords. Returns the
    /// FaceId at axial `(q, r)`, if any face was registered there.
    /// Builders like `BoardBuilder::hex_grid(radius)` populate this as
    /// they construct topology.
    pub fn face_at_hex(&self, q: i32, r: i32) -> Option<FaceId> {
        self.coord_to_face
            .get(&FaceCoord::Hex { q, r })
            .copied()
    }

    /// AR-1: returns the registered coord for a face, if any.
    pub fn face_coord(&self, face: FaceId) -> Option<FaceCoord> {
        self.face_coords.get(&face).copied()
    }

    /// A version-like fingerprint that changes on any mutation.
    /// Cheap to compute; not cryptographically unique but distinguishes
    /// any two graphs that differ in element count or id ordering.
    pub fn version(&self) -> u64 {
        let n = self.next_node_id as u64;
        let e = self.next_edge_id as u64;
        let f = self.next_face_id as u64;
        let count = (self.nodes.len() as u64)
            ^ (self.edges.len() as u64).wrapping_shl(20)
            ^ (self.faces.len() as u64).wrapping_shl(40);
        n.wrapping_mul(0x9E37_79B9_7F4A_7C15)
            ^ e.wrapping_mul(0x9E37_79B9_7F4A_7C15).rotate_left(20)
            ^ f.wrapping_mul(0x9E37_79B9_7F4A_7C15).rotate_left(40)
            ^ count
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphError {
    UnknownNode(BoardNodeId),
    UnknownEdge(EdgeId),
    UnknownFace(FaceId),
    OpenBoundary { gap_after: BoardNodeId, expected_to: BoardNodeId },
}

impl BoardGraph {
    #[expect(clippy::indexing_slicing, reason = "invariant: loop bounds 0..len ensure i and (i+1)%len are always in range")]
    pub fn try_add_face(&mut self, bounding_nodes: Vec<BoardNodeId>) -> Result<FaceId, GraphError> {
        if bounding_nodes.len() < 3 {
            return Err(GraphError::OpenBoundary {
                gap_after: BoardNodeId(0),
                expected_to: BoardNodeId(0),
            });
        }
        for &n in &bounding_nodes {
            if !self.contains_node(n) {
                return Err(GraphError::UnknownNode(n));
            }
        }
        // Validate boundary is closed: every consecutive pair (and the wrap)
        // must share an edge in the graph.
        for i in 0..bounding_nodes.len() {
            let cur  = bounding_nodes[i];
            let next = bounding_nodes[(i + 1) % bounding_nodes.len()];
            let connected = self.edges.values().any(|kind| match *kind {
                EdgeKind::Bidirectional { a, b } => (a == cur && b == next) || (a == next && b == cur),
                EdgeKind::Directed      { from, to } => (from == cur && to == next) || (from == next && to == cur),
            });
            if !connected {
                return Err(GraphError::OpenBoundary { gap_after: cur, expected_to: next });
            }
        }
        let id = FaceId(self.next_face_id);
        self.next_face_id += 1;
        self.faces.insert(id, bounding_nodes);
        Ok(id)
    }

    /// Convenience wrapper that delegates to `try_add_face`.
    pub fn add_face(&mut self, bounding_nodes: Vec<BoardNodeId>) -> Result<FaceId, GraphError> {
        self.try_add_face(bounding_nodes)
    }

    pub fn face_bounding(&self, id: FaceId) -> Option<&Vec<BoardNodeId>> {
        self.faces.get(&id)
    }
}

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

    #[test]
    fn version_bumps_on_mutation() {
        let mut g = BoardGraph::new();
        let v0 = g.version();
        g.add_node();
        let v1 = g.version();
        g.add_node();
        let v2 = g.version();
        assert_ne!(v0, v1);
        assert_ne!(v1, v2);
    }
}
