//! AR-1: `BoardGraph::face_distance` — typed distance computation between
//! two faces, dispatching on `DistanceMetric`.
//!
//! See `docs/superpowers/specs/2026-05-04-ar1-distance-metric-and-face-distance.md`.
//!
//! Cartesian metrics (Manhattan, Chebyshev, EuclideanFloor, EuclideanCeil)
//! consult `BoardGraph::face_coord` for both endpoints. `GraphHops` runs
//! BFS over face-adjacency (faces sharing an edge).

use crate::board::{BoardGraph, FaceCoord};
use crate::board::ids::FaceId;
use crate::distance::{DistanceError, DistanceMetric};

impl BoardGraph {
    /// Distance between two faces under `metric`. Errors are typed so
    /// callers can surface "no coords registered" vs "unreachable" vs
    /// "unknown face" with actionable messages instead of silent
    /// failures.
    pub fn face_distance(
        &self,
        from: FaceId,
        to: FaceId,
        metric: DistanceMetric,
    ) -> Result<u32, DistanceError> {
        if !self.contains_face(from) {
            return Err(DistanceError::UnknownFace(from));
        }
        if !self.contains_face(to) {
            return Err(DistanceError::UnknownFace(to));
        }
        if from == to {
            return Ok(0);
        }
        match metric {
            DistanceMetric::Manhattan => cartesian_distance(self, from, to, manhattan),
            DistanceMetric::Chebyshev => cartesian_distance(self, from, to, chebyshev),
            DistanceMetric::EuclideanFloor => {
                cartesian_distance(self, from, to, euclidean_floor)
            }
            DistanceMetric::EuclideanCeil => {
                cartesian_distance(self, from, to, euclidean_ceil)
            }
            DistanceMetric::GraphHops => graph_hops(self, from, to),
            DistanceMetric::Hex => hex_distance(self, from, to),
            // UC-04-4: the types crate has no plugin registry. The
            // engine resolves Custom via MetricRegistry::dispatch
            // *before* reaching this point; if a caller skips that,
            // surface a typed error instead of silently succeeding.
            DistanceMetric::Custom(key) => Err(DistanceError::UnknownCustomMetric(key)),
        }
    }
}

/// UC-04-3 — hex cube distance under `FaceCoord::Hex` data on both
/// faces. Cube coordinate `s` is derived (`s = -q - r`); the answer is
/// `max(|dq|, |dr|, |ds|)`. Returns `CoordVariantMismatch` if either
/// face has a non-Hex coordinate, and `MissingCoord` if either face is
/// unregistered.
fn hex_distance(g: &BoardGraph, from: FaceId, to: FaceId) -> Result<u32, DistanceError> {
    let coord_a = g.face_coord(from).ok_or(DistanceError::MissingCoord(from))?;
    let coord_b = g.face_coord(to).ok_or(DistanceError::MissingCoord(to))?;
    let (qa, ra) = match coord_a {
        FaceCoord::Hex { q, r } => (q, r),
        _ => return Err(DistanceError::IncompatibleCoord),
    };
    let (qb, rb) = match coord_b {
        FaceCoord::Hex { q, r } => (q, r),
        _ => return Err(DistanceError::IncompatibleCoord),
    };
    let dq = qa - qb;
    let dr = ra - rb;
    let ds = -dq - dr; // s = -q - r ⇒ ds = -dq - dr
    Ok(dq.unsigned_abs().max(dr.unsigned_abs()).max(ds.unsigned_abs()))
}

/// Helper: load Cartesian coords for both faces, error if either is
/// missing or if the variants don't match. Then apply `f` to the deltas.
fn cartesian_distance(
    g: &BoardGraph,
    from: FaceId,
    to: FaceId,
    f: fn(i32, i32) -> u32,
) -> Result<u32, DistanceError> {
    let a = g.face_coord(from).ok_or(DistanceError::MissingCoord(from))?;
    let b = g.face_coord(to).ok_or(DistanceError::MissingCoord(to))?;
    match (a, b) {
        (
            FaceCoord::Cartesian { col: ca, row: ra },
            FaceCoord::Cartesian { col: cb, row: rb },
        ) => Ok(f(ca - cb, ra - rb)),
        // UC-04-3: mismatched or non-Cartesian variants under a
        // Cartesian metric → typed mismatch error.
        _ => Err(DistanceError::IncompatibleCoord),
    }
}

fn manhattan(dcol: i32, drow: i32) -> u32 {
    dcol.unsigned_abs() + drow.unsigned_abs()
}

fn chebyshev(dcol: i32, drow: i32) -> u32 {
    dcol.unsigned_abs().max(drow.unsigned_abs())
}

fn euclidean_floor(dcol: i32, drow: i32) -> u32 {
    let d = ((dcol as f64).powi(2) + (drow as f64).powi(2)).sqrt();
    d.floor() as u32
}

fn euclidean_ceil(dcol: i32, drow: i32) -> u32 {
    let d = ((dcol as f64).powi(2) + (drow as f64).powi(2)).sqrt();
    d.ceil() as u32
}

/// BFS over face-adjacency: two faces are adjacent iff they share an
/// edge (the existing `faces_at_edge` query encodes this). Returns the
/// hop count, or `Unreachable` for disconnected components.
fn graph_hops(g: &BoardGraph, from: FaceId, to: FaceId) -> Result<u32, DistanceError> {
    use std::collections::{HashSet, VecDeque};

    // Precompute face↔face adjacency once: for each edge, the faces it
    // belongs to are mutual neighbors.
    let mut adjacency: std::collections::BTreeMap<FaceId, Vec<FaceId>> =
        std::collections::BTreeMap::new();
    for edge_id in g.edges.keys().copied() {
        let faces = g.faces_at_edge(edge_id);
        for &fa in &faces {
            for &fb in &faces {
                if fa != fb {
                    adjacency.entry(fa).or_default().push(fb);
                }
            }
        }
    }

    let mut seen: HashSet<FaceId> = HashSet::from([from]);
    let mut queue: VecDeque<(FaceId, u32)> = VecDeque::from([(from, 0u32)]);
    while let Some((face, dist)) = queue.pop_front() {
        if face == to {
            return Ok(dist);
        }
        if let Some(neighbors) = adjacency.get(&face) {
            for &n in neighbors {
                if seen.insert(n) {
                    queue.push_back((n, dist + 1));
                }
            }
        }
    }
    Err(DistanceError::Unreachable)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board::builders::BoardBuilder;
    use crate::board::FaceCoord;
    use std::collections::BTreeMap;

    /// Build a hex_grid(radius) and a lookup table from axial (q, r) to FaceId.
    fn hex_lookup(radius: u32) -> (BoardGraph, BTreeMap<(i32, i32), FaceId>) {
        let g = BoardBuilder::hex_grid(radius).graph;
        let mut map = BTreeMap::new();
        for face in g.faces_iter() {
            if let Some(FaceCoord::Hex { q, r }) = g.face_coord(face) {
                map.insert((q, r), face);
            }
        }
        (g, map)
    }

    #[test]
    fn uc_04_3_hex_distance_neighbours_returns_one() {
        let (g, by_qr) = hex_lookup(2);
        let center = by_qr[&(0, 0)];
        // All 6 ring-1 neighbours by axial directions: (1,0),(0,1),(-1,1),(-1,0),(0,-1),(1,-1)
        let ring1 = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)];
        for qr in ring1 {
            let n = by_qr[&qr];
            let d = g.face_distance(center, n, DistanceMetric::Hex).unwrap();
            assert_eq!(d, 1, "neighbour at axial {:?} should be distance 1", qr);
        }
    }

    #[test]
    fn uc_04_3_hex_distance_to_self_returns_zero() {
        let (g, by_qr) = hex_lookup(2);
        let center = by_qr[&(0, 0)];
        assert_eq!(
            g.face_distance(center, center, DistanceMetric::Hex).unwrap(),
            0
        );
    }

    #[test]
    fn uc_04_3_hex_distance_radius2_corner_returns_two() {
        let (g, by_qr) = hex_lookup(2);
        let center = by_qr[&(0, 0)];
        // Corner at (q=2, r=0): dq=2, dr=0, ds=-2 → max=2.
        let corner = by_qr[&(2, 0)];
        assert_eq!(
            g.face_distance(center, corner, DistanceMetric::Hex).unwrap(),
            2
        );
        // Corner at (q=2, r=-2): dq=2, dr=-2, ds=0 → max=2.
        let corner2 = by_qr[&(2, -2)];
        assert_eq!(
            g.face_distance(center, corner2, DistanceMetric::Hex).unwrap(),
            2
        );
        // Corner at (q=-2, r=2): max = 2.
        let corner3 = by_qr[&(-2, 2)];
        assert_eq!(
            g.face_distance(center, corner3, DistanceMetric::Hex).unwrap(),
            2
        );
    }

    #[test]
    fn uc_04_3_hex_metric_on_cartesian_face_errors() {
        let g_cart = BoardBuilder::square_grid(3, 3).graph;
        let face_a = g_cart.faces_iter().next().unwrap();
        let face_b = g_cart.faces_iter().nth(1).unwrap();
        // Both faces are Cartesian-coordinate-bearing; asking for Hex metric
        // must surface as IncompatibleCoord (not silent fall-through).
        match g_cart.face_distance(face_a, face_b, DistanceMetric::Hex) {
            Err(DistanceError::IncompatibleCoord) => {}
            other => panic!("expected IncompatibleCoord, got {other:?}"),
        }
    }

    #[test]
    fn uc_04_4_face_distance_custom_returns_unknown_custom_metric() {
        use crate::distance::MetricKey;
        // Even on a valid graph with valid faces, the types crate
        // can't resolve Custom — engine must dispatch via the registry.
        let g = BoardBuilder::square_grid(2, 2).graph;
        let mut faces = g.faces_iter();
        let a = faces.next().unwrap();
        let b = faces.next().unwrap();
        let key = MetricKey::new("test:identity");
        match g.face_distance(a, b, DistanceMetric::Custom(key.clone())) {
            Err(DistanceError::UnknownCustomMetric(k)) => assert_eq!(k, key),
            other => panic!("expected UnknownCustomMetric, got {other:?}"),
        }
    }

    #[test]
    fn uc_04_3_hex_metric_missing_coord_errors() {
        // Build a graph with two faces but no FaceCoord registered.
        let mut g = BoardGraph::new();
        let n: Vec<_> = (0..4).map(|_| g.add_node()).collect();
        g.add_bidirectional_edge(n[0], n[1]);
        g.add_bidirectional_edge(n[1], n[2]);
        g.add_bidirectional_edge(n[2], n[3]);
        g.add_bidirectional_edge(n[3], n[0]);
        let face_a = g.try_add_face(vec![n[0], n[1], n[2], n[3]]).unwrap();

        let n2: Vec<_> = (0..4).map(|_| g.add_node()).collect();
        g.add_bidirectional_edge(n2[0], n2[1]);
        g.add_bidirectional_edge(n2[1], n2[2]);
        g.add_bidirectional_edge(n2[2], n2[3]);
        g.add_bidirectional_edge(n2[3], n2[0]);
        let face_b = g.try_add_face(vec![n2[0], n2[1], n2[2], n2[3]]).unwrap();

        match g.face_distance(face_a, face_b, DistanceMetric::Hex) {
            Err(DistanceError::MissingCoord(id)) => {
                assert!(id == face_a || id == face_b);
            }
            other => panic!("expected MissingCoord, got {other:?}"),
        }
    }
}
