use std::fmt;

use crate::schema::EntityId;

/// Uniquely identifies a shard within the cluster.
///
/// A shard owns a contiguous range of `EntityId`s for a given `EntityKind`.
/// `ShardId(0)` is the default single-node shard.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct ShardId(pub u16);

impl ShardId {
    /// The default single-node shard.
    pub const SINGLE: Self = Self(0);
}

impl fmt::Display for ShardId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "s{}", self.0)
    }
}

/// Uniquely identifies a region in the cluster.
///
/// `RegionId(0)` is the default single-node region.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct RegionId(pub u16);

impl RegionId {
    /// The default single-node region.
    pub const SINGLE: Self = Self(0);
}

impl fmt::Display for RegionId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "r{}", self.0)
    }
}

// ── EntityIdRange ──────────────────────────────────────────────────────────

/// A contiguous, half-open range of `EntityId`s: `[start, end)`.
///
/// The sentinel value `end = u64::MAX` means the range extends to include
/// *all* entity IDs >= `start`, including `u64::MAX` itself. This is necessary
/// because a true half-open range cannot represent "up to and including the
/// maximum u64 value" without overflow.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EntityIdRange {
    /// Inclusive lower bound.
    pub start: u64,
    /// Exclusive upper bound. `u64::MAX` is a sentinel meaning "includes the last entity".
    pub end: u64,
}

impl EntityIdRange {
    /// Returns `true` if the given raw entity ID falls within this range.
    ///
    /// When `end == u64::MAX`, the range covers `[start, u64::MAX]` (inclusive
    /// on both sides) to handle the sentinel convention.
    #[must_use]
    pub const fn contains(&self, id: u64) -> bool {
        id >= self.start && (self.end == u64::MAX || id < self.end)
    }

    /// The full `u64` space -- the default for single-shard deployments.
    #[must_use]
    pub const fn full() -> Self {
        Self {
            start: 0,
            end: u64::MAX,
        }
    }
}

// ── RoutingStrategy ────────────────────────────────────────────────────────

/// Strategy for mapping an `EntityId` to a `ShardId`.
///
/// - `Single`: all entities live on `ShardId(0)`. This is the default for
///   single-node tidalDB and introduces zero overhead.
/// - `Hash`: deterministic `FNV-1a(entity_id) % num_shards` routing. Good for
///   uniform distribution but makes range scans across shards expensive.
/// - `Range`: each shard owns a contiguous `[start, end)` of entity IDs. Good
///   for ordered scans but requires careful split-point selection.
#[derive(Debug, Clone)]
pub enum RoutingStrategy {
    /// All entities route to the default single shard.
    Single,
    /// Hash-based routing: `FNV-1a(entity_id) % num_shards`.
    Hash { num_shards: u16 },
    /// Range-based routing: each shard owns a contiguous range of `EntityId`s.
    /// Ranges must be sorted, non-overlapping, and cover the full `u64` space.
    Range(Vec<(ShardId, EntityIdRange)>),
}

// ── RouterError ────────────────────────────────────────────────────────────

/// Errors that can occur when constructing a `ShardRouter`.
///
/// All variants are construction-time errors -- once a router is built,
/// `route()` is infallible.
#[derive(Debug, thiserror::Error)]
pub enum RouterError {
    #[error("shard count must be > 0")]
    ZeroShards,
    #[error("range list is empty")]
    EmptyRanges,
    #[error("gap in range coverage: expected start {expected}, found {found}")]
    Gap { expected: u64, found: u64 },
    #[error("empty range starting at {start}")]
    EmptyRange { start: u64 },
    #[error("ranges don't cover full u64 space: ends at {ends_at}")]
    IncompleteCoverage { ends_at: u64 },
}

// ── ShardRouter ────────────────────────────────────────────────────────────

/// Routes `EntityId`s to `ShardId`s according to a configured strategy.
///
/// Construction validates all invariants (no gaps, full coverage, non-zero
/// shard count) so that `route()` is infallible and branch-free on the hot
/// path for `Single` and `Hash` strategies.
#[derive(Debug, Clone)]
pub struct ShardRouter {
    strategy: RoutingStrategy,
}

impl ShardRouter {
    /// Single-shard router. All entities map to `ShardId(0)`.
    #[must_use]
    pub const fn single() -> Self {
        Self {
            strategy: RoutingStrategy::Single,
        }
    }

    /// Hash-based router. `num_shards` must be > 0.
    ///
    /// # Errors
    ///
    /// Returns `RouterError::ZeroShards` if `num_shards == 0`.
    pub const fn hash(num_shards: u16) -> Result<Self, RouterError> {
        if num_shards == 0 {
            return Err(RouterError::ZeroShards);
        }
        Ok(Self {
            strategy: RoutingStrategy::Hash { num_shards },
        })
    }

    /// Range-based router. Ranges must be sorted by start, contiguous,
    /// non-empty, and cover the full `u64` space (last range must end at
    /// `u64::MAX`).
    ///
    /// # Errors
    ///
    /// Returns a `RouterError` variant describing the first validation failure.
    pub fn range(mut ranges: Vec<(ShardId, EntityIdRange)>) -> Result<Self, RouterError> {
        Self::validate_ranges(&mut ranges)?;
        Ok(Self {
            strategy: RoutingStrategy::Range(ranges),
        })
    }

    /// Map an entity to its owning shard. Infallible after construction.
    #[must_use]
    pub fn route(&self, entity_id: EntityId) -> ShardId {
        let raw = entity_id.as_u64();
        match &self.strategy {
            RoutingStrategy::Single => ShardId::SINGLE,
            RoutingStrategy::Hash { num_shards } => {
                let h = fnv1a_hash(raw);
                // SAFETY (arithmetic): num_shards > 0 is validated at construction.
                #[allow(clippy::cast_possible_truncation)]
                let idx = (h % u64::from(*num_shards)) as u16;
                ShardId(idx)
            }
            RoutingStrategy::Range(ranges) => {
                // Binary search for the range containing `raw`.
                // Ranges are sorted by start and validated to be contiguous,
                // so exactly one range will match.
                let idx = ranges.partition_point(|(_, r)| r.start <= raw);
                // partition_point returns the first index where `r.start > raw`,
                // so the containing range is at idx - 1.
                // idx is always >= 1 because ranges[0].start == 0 <= raw.
                let (shard, _) = &ranges[idx - 1];
                *shard
            }
        }
    }

    /// Returns all shard IDs managed by this router, in sorted order.
    #[must_use]
    pub fn all_shards(&self) -> Vec<ShardId> {
        match &self.strategy {
            RoutingStrategy::Single => vec![ShardId::SINGLE],
            RoutingStrategy::Hash { num_shards } => (0..*num_shards).map(ShardId).collect(),
            RoutingStrategy::Range(ranges) => {
                let mut shards: Vec<ShardId> = ranges.iter().map(|(s, _)| *s).collect();
                shards.sort();
                shards.dedup();
                shards
            }
        }
    }

    /// Validate that ranges are sorted, contiguous, non-empty, and cover the
    /// full `u64` space. Sorts the input by `start` before checking.
    fn validate_ranges(ranges: &mut [(ShardId, EntityIdRange)]) -> Result<(), RouterError> {
        if ranges.is_empty() {
            return Err(RouterError::EmptyRanges);
        }

        // Sort by range start so callers don't need to pre-sort.
        ranges.sort_by_key(|(_, r)| r.start);

        let mut expected_start: u64 = 0;

        for (_, range) in ranges.iter() {
            // Check for gaps.
            if range.start != expected_start {
                return Err(RouterError::Gap {
                    expected: expected_start,
                    found: range.start,
                });
            }
            // Check for empty ranges (start == end, and end != u64::MAX which is sentinel).
            if range.start == range.end {
                return Err(RouterError::EmptyRange { start: range.start });
            }
            // An empty range can also occur if start > end, but with u64 and
            // the sentinel convention, start < end OR end == u64::MAX is
            // the only valid case. start > end is unreachable after the gap
            // check because expected_start only advances forward.
            expected_start = range.end;
        }

        // The last range must end at u64::MAX (the sentinel for "covers to the end").
        if expected_start != u64::MAX {
            return Err(RouterError::IncompleteCoverage {
                ends_at: expected_start,
            });
        }

        Ok(())
    }
}

/// FNV-1a hash of a `u64` value, used for hash-based shard routing.
///
/// FNV-1a is chosen for its simplicity, speed, and excellent distribution
/// on integer keys. It is *not* cryptographic -- we only need uniform
/// distribution across shard buckets.
fn fnv1a_hash(value: u64) -> u64 {
    const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
    const FNV_PRIME: u64 = 1_099_511_628_211;
    let mut hash = FNV_OFFSET;
    for byte in &value.to_le_bytes() {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(FNV_PRIME);
    }
    hash
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[path = "shard_tests.rs"]
mod tests;
