//! AR-1: Typed distance metric system for spatial range checks.
//!
//! See `docs/superpowers/specs/2026-05-04-ar1-distance-metric-and-face-distance.md`.
//!
//! `DistanceMetric` is set by plugin authors per ability (`RangedData.metric`)
//! or system-wide (`SessionConfig.default_distance_metric`). The pipeline
//! resolves per-ability override → system default → typed error if neither.
//!
//! Cartesian metrics (Manhattan, Chebyshev, EuclideanFloor, EuclideanCeil)
//! require `FaceCoord::Cartesian` data registered on the BoardGraph. Builders
//! like `BoardBuilder::square_grid(cols, rows)` register coords automatically;
//! plugins that mutate the graph by hand may need to register coords manually
//! if they want Cartesian metrics on those faces. `GraphHops` is topology-only
//! and works on any graph.

use crate::board::ids::FaceId;
use std::borrow::Cow;

/// UC-04-4 — namespaced key for a plugin-registered distance metric.
/// Mirrors `PoolId`/`PhaseId`: `const fn new` enforces the
/// `"plugin:key"` namespace convention at compile time.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MetricKey(Cow<'static, str>);

impl MetricKey {
    /// Compile-time-namespaced metric key. Const-eval asserts a `:`
    /// separator so writing `MetricKey::new("manhattan")` is a build
    /// error.
    #[allow(
        clippy::indexing_slicing,
        reason = "const-fn byte search; `i < bytes.len()` loop bound \
                  guarantees bytes[i] is in range. `.get(i)` is not \
                  const-stable for slices."
    )]
    pub const fn new(key: &'static str) -> Self {
        let bytes = key.as_bytes();
        let mut i = 0;
        let mut found = false;
        while i < bytes.len() {
            if bytes[i] == b':' {
                found = true;
                break;
            }
            i += 1;
        }
        assert!(found, "MetricKey requires a 'plugin:key' namespace separator");
        Self(Cow::Borrowed(key))
    }

    /// Runtime-built key (e.g. composed from session settings). Panics
    /// if the input lacks the namespace separator.
    pub fn from_owned(key: String) -> Self {
        assert!(
            key.contains(':'),
            "MetricKey requires a 'plugin:key' namespace separator"
        );
        Self(Cow::Owned(key))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for MetricKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

/// Selects which arithmetic to apply when computing distance between two
/// faces. Pick at the call site (per ability) or via system default.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DistanceMetric {
    /// `|Δcol| + |Δrow|`. Requires Cartesian coords on both faces.
    /// Diagonal step = 2. Standard for orthogonal-grid games (chess
    /// rook-like, checkers).
    Manhattan,
    /// `max(|Δcol|, |Δrow|)`. Requires Cartesian coords on both faces.
    /// Diagonal step = 1. Standard for D&D 5e movement and "king-move"
    /// grid games.
    Chebyshev,
    /// `floor(sqrt(Δcol² + Δrow²))`. Requires Cartesian coords on both
    /// faces. Permissive rounding — distance 4.7 reports as 4 ("favors
    /// action").
    EuclideanFloor,
    /// `ceil(sqrt(Δcol² + Δrow²))`. Requires Cartesian coords on both
    /// faces. Strict rounding — distance 4.1 reports as 5 ("favors
    /// deny"). Both Euclidean variants exist because some game systems
    /// (e.g. Gloomhaven) use both.
    EuclideanCeil,
    /// BFS hop count over face adjacency (faces sharing an edge).
    /// Topology-only; works on any BoardGraph. Use for path-based games
    /// where the geometry is the graph (e.g. Conway's Life, Candyland).
    GraphHops,
    /// UC-04-3 — hex cube distance: `max(|dq|, |dr|, |ds|)` where
    /// `s = -q - r`. Requires both faces to carry `FaceCoord::Hex`
    /// data on the BoardGraph.
    Hex,
    /// UC-04-4 — plugin-registered metric. Engine resolves through
    /// `MetricRegistry::dispatch` before delegating; calling
    /// `BoardGraph::face_distance` directly with this variant returns
    /// `UnknownCustomMetric` (the types crate has no registry).
    Custom(MetricKey),
}

/// Why a distance computation could not be answered. Surfaced at the
/// pipeline as `PipelineError::Denied` with an explicit, actionable
/// message — never silently swallowed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DistanceError {
    /// A Cartesian metric was requested but the face has no
    /// `FaceCoord::Cartesian` registered. Likely cause: the graph was
    /// built by a custom path that forgot to register coords; or a
    /// non-Cartesian topology is being measured with a Cartesian metric.
    MissingCoord(FaceId),
    /// The two faces' `FaceCoord` variants are not the same kind (e.g.
    /// one Cartesian, one future Hex). No meaningful distance.
    IncompatibleCoord,
    /// `GraphHops`: the faces are in different connected components.
    Unreachable,
    /// The face id is not in the graph at all.
    UnknownFace(FaceId),
    /// UC-04-4 — a `DistanceMetric::Custom(key)` reached
    /// `BoardGraph::face_distance` without passing through the
    /// engine's `MetricRegistry::dispatch`. The caller must resolve
    /// custom metrics via the registry first.
    UnknownCustomMetric(MetricKey),
}

/// 0400: type alias for boxed distance-metric closures.
/// `Arc<dyn Fn(&BoardGraph, FaceId, FaceId) -> Result<u32, DistanceError> + Send + Sync>`.
/// Used by `ComposeEntry::DistanceMetric` and `SetupContext::register_distance_metric`.
pub type DistanceMetricFn = std::sync::Arc<
    dyn Fn(
            &crate::board::BoardGraph,
            crate::board::ids::FaceId,
            crate::board::ids::FaceId,
        ) -> Result<u32, DistanceError>
        + Send
        + Sync,
>;

impl std::fmt::Display for DistanceError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DistanceError::MissingCoord(id) => write!(
                f,
                "face {:?} has no Cartesian coord registered; \
                 either use a coord-aware builder (e.g. \
                 BoardBuilder::square_grid) or pick DistanceMetric::GraphHops",
                id
            ),
            DistanceError::IncompatibleCoord => write!(
                f,
                "the two faces have incompatible FaceCoord variants \
                 (e.g. Cartesian vs Hex); cannot compute distance"
            ),
            DistanceError::Unreachable => write!(
                f,
                "GraphHops: faces are in disconnected components"
            ),
            DistanceError::UnknownFace(id) => write!(f, "face {:?} not in graph", id),
            DistanceError::UnknownCustomMetric(key) => write!(
                f,
                "DistanceMetric::Custom({key}) is not registered in the \
                 session's MetricRegistry; plugins register via \
                 SetupContext::register_distance_metric"
            ),
        }
    }
}

impl std::error::Error for DistanceError {}

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

    #[test]
    fn uc_04_4_metric_key_namespace_asserts() {
        // const-eval is exercised by the const fn used at runtime.
        let k = MetricKey::new("dnd5e:five_foot_diagonal");
        assert_eq!(k.as_str(), "dnd5e:five_foot_diagonal");
    }

    #[test]
    #[should_panic(expected = "MetricKey requires a 'plugin:key' namespace separator")]
    fn uc_04_4_metric_key_from_owned_rejects_unnamespaced() {
        let _ = MetricKey::from_owned("no_colon".to_string());
    }

    #[test]
    fn uc_04_4_metric_key_from_owned_accepts_namespaced() {
        let k = MetricKey::from_owned("plugin:custom".to_string());
        assert_eq!(k.as_str(), "plugin:custom");
    }
}
