//! Per-tenant identity, configuration, rate limiting, and routing.

use std::sync::{
    Arc, RwLock,
    atomic::{AtomicU64, Ordering},
};

use dashmap::DashMap;

use super::shard::{RegionId, ShardId};
use crate::schema::EntityId;

/// Unique identifier for a tenant.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Default,
    serde::Serialize,
    serde::Deserialize,
)]
pub struct TenantId(pub u64);

impl TenantId {
    pub const DEFAULT: Self = Self(0);
}

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

/// Per-tenant configuration and resource limits.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TenantConfig {
    pub tenant_id: TenantId,
    pub max_signals_per_sec: Option<u32>,
    pub max_entities: Option<u64>,
    pub max_storage_bytes: Option<u64>,
    pub required_regions: Vec<RegionId>,
    pub label: String,
}

impl TenantConfig {
    /// Create a default (unlimited) tenant config.
    #[must_use]
    pub fn default_tenant() -> Self {
        Self {
            tenant_id: TenantId::DEFAULT,
            max_signals_per_sec: None,
            max_entities: None,
            max_storage_bytes: None,
            required_regions: vec![],
            label: "default".to_string(),
        }
    }

    /// Create an unlimited tenant config with a custom label.
    #[must_use]
    pub fn unlimited(id: TenantId, label: impl Into<String>) -> Self {
        Self {
            tenant_id: id,
            max_signals_per_sec: None,
            max_entities: None,
            max_storage_bytes: None,
            required_regions: vec![],
            label: label.into(),
        }
    }
}

/// Token-bucket rate limiter for per-tenant signal throughput.
///
/// Uses `Ordering::Relaxed` for the CAS loop because approximate enforcement
/// is acceptable for rate limiting: a brief over-admission of 1-2 signals under
/// contention is tolerable, and the token bucket self-corrects on the next
/// refill cycle. The alternative (`AcqRel`) would add unnecessary contention
/// on the hot signal-write path.
#[derive(Debug)]
pub struct TenantRateLimiter {
    tokens_bits: AtomicU64,
    refill_rate_per_ns: f64,
    max_tokens: f64,
    last_refill_ns: AtomicU64,
}

impl TenantRateLimiter {
    /// Create a new rate limiter allowing `max_signals_per_sec` signals per second.
    #[must_use]
    pub fn new(max_signals_per_sec: u32) -> Self {
        let rate_per_ns = f64::from(max_signals_per_sec) / 1_000_000_000.0;
        let max_tokens = f64::from(max_signals_per_sec) * 2.0;
        Self {
            tokens_bits: AtomicU64::new(max_tokens.to_bits()),
            refill_rate_per_ns: rate_per_ns,
            max_tokens,
            last_refill_ns: AtomicU64::new(crate::replication::now_ns()),
        }
    }

    /// Try to consume one token from the bucket.
    ///
    /// # Errors
    ///
    /// Returns `QuotaExceeded` if the token bucket is empty.
    ///
    /// # Concurrency
    ///
    /// Refill and consumption are decoupled so the refill amount cannot be
    /// double-granted under contention. A naive design that stored
    /// `last_refill_ns` *after* the token CAS raced: a second thread could read
    /// the stale `last_refill_ns`, compute the same elapsed-time refill, and
    /// credit it again — granting up to `2 * elapsed * rate` extra tokens. Here,
    /// crediting a refill requires *winning a CAS that advances*
    /// `last_refill_ns` from `last` to `now`. Only the winner adds the elapsed
    /// refill to the bucket; every other concurrent thread re-reads the
    /// now-advanced `last_refill_ns` and so computes an elapsed window of ≈ 0,
    /// making the refill grant exactly-once per real elapsed interval.
    #[allow(clippy::cast_precision_loss)] // ns elapsed fits in f64 mantissa
    pub fn try_acquire(&self) -> crate::Result<()> {
        loop {
            // Re-read time inside the loop so retries use fresh values.
            let now = crate::replication::now_ns();
            let last = self.last_refill_ns.load(Ordering::Relaxed);
            let elapsed_ns = now.saturating_sub(last);

            // Phase 1 — claim the refill window. We may credit
            // `elapsed_ns * rate` tokens only if WE are the thread that advances
            // `last_refill_ns` from `last` to `now`. If the CAS fails another
            // thread already claimed (part of) this window; retry with a fresh
            // `last`, which now yields ≈ 0 elapsed, so no double-grant occurs.
            if elapsed_ns > 0 {
                if self
                    .last_refill_ns
                    .compare_exchange_weak(last, now, Ordering::Relaxed, Ordering::Relaxed)
                    .is_err()
                {
                    // Lost the refill race — re-read time/last on the next pass.
                    continue;
                }
                // Won the window: credit the refill exactly once. This additive
                // CAS loop reads fresh tokens each pass and adds the *fixed*
                // `refill`, so exactly one write commits the credit regardless of
                // contention (it never double-adds within this call).
                let refill = elapsed_ns as f64 * self.refill_rate_per_ns;
                loop {
                    let current = f64::from_bits(self.tokens_bits.load(Ordering::Relaxed));
                    let credited = (current + refill).min(self.max_tokens);
                    if self
                        .tokens_bits
                        .compare_exchange_weak(
                            current.to_bits(),
                            credited.to_bits(),
                            Ordering::Relaxed,
                            Ordering::Relaxed,
                        )
                        .is_ok()
                    {
                        break;
                    }
                }
            }

            // Phase 2 — consume exactly one token. Pure decrement CAS loop with
            // no refill folded in (the refill, if any, is already committed to
            // the bucket above), so retries can never double-credit.
            loop {
                let current = f64::from_bits(self.tokens_bits.load(Ordering::Relaxed));
                if current < 1.0 {
                    return Err(crate::schema::TidalError::QuotaExceeded(
                        "signal rate limit exceeded".into(),
                    ));
                }
                let new_bits = (current - 1.0).to_bits();
                // Relaxed ordering: see struct-level doc comment.
                if self
                    .tokens_bits
                    .compare_exchange_weak(
                        current.to_bits(),
                        new_bits,
                        Ordering::Relaxed,
                        Ordering::Relaxed,
                    )
                    .is_ok()
                {
                    return Ok(());
                }
            }
        }
    }
}

/// On-disk shape of the migration routing checkpoint (dual-write pairs + pins).
///
/// String keys are decimal [`TenantId`]s; dual-write values are
/// `(source_shard, target_shard)` raw `u16`s and pin values are the target
/// `ShardId` `u16`. `Default` (both maps empty) is the safe restore for missing
/// or malformed bytes.
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct MigrationRoutingCheckpoint {
    dual: std::collections::HashMap<String, (u16, u16)>,
    pins: std::collections::HashMap<String, u16>,
}

/// A captured snapshot of a single tenant's routing (its dual-write pair and its
/// shard pin), used for targeted rollback in the migration coordinator.
///
/// Produced by [`TenantRouter::snapshot_tenant_routing`] and restored by
/// [`TenantRouter::restore_tenant_routing`]. Restoring affects only this
/// tenant, never the whole router, so a failed migration persist cannot clobber
/// a concurrent migration's routing for a different tenant (C10).
#[derive(Debug, Clone)]
pub struct TenantRoutingSnapshot {
    tenant_id: TenantId,
    dual_write: Option<(ShardId, ShardId)>,
    pin: Option<ShardId>,
}

/// Describes the shard topology for the cluster.
#[derive(Debug, Clone)]
pub struct ClusterTopology {
    pub shards: Vec<ShardAssignment>,
}

/// Maps a shard to its region.
#[derive(Debug, Clone, Copy)]
pub struct ShardAssignment {
    pub shard_id: ShardId,
    pub region_id: RegionId,
}

impl ClusterTopology {
    /// Create a single-shard, single-region topology.
    #[must_use]
    pub fn single() -> Self {
        Self {
            shards: vec![ShardAssignment {
                shard_id: ShardId::SINGLE,
                region_id: RegionId::SINGLE,
            }],
        }
    }
}

/// Routes entities to shards based on tenant configuration and jump consistent hash.
pub struct TenantRouter {
    tenants: DashMap<TenantId, TenantConfig>,
    rate_limiters: DashMap<TenantId, Arc<TenantRateLimiter>>,
    topology: Arc<RwLock<ClusterTopology>>,
    /// Tenants currently in dual-write migration mode: maps `TenantId` to
    /// `(source_shard, target_shard)`. During dual-write, write operations
    /// target both shards; reads target the source (old) shard until finalization.
    dual_write_map: DashMap<TenantId, (ShardId, ShardId)>,
    /// Explicit per-tenant shard pin. Set after migration finalization to lock
    /// all routing for a tenant to the new target shard regardless of jump hash.
    shard_pin_map: DashMap<TenantId, ShardId>,
}

impl TenantRouter {
    /// Create a new router with the given topology.
    #[must_use]
    pub fn new(topology: Arc<RwLock<ClusterTopology>>) -> Self {
        Self {
            tenants: DashMap::new(),
            rate_limiters: DashMap::new(),
            topology,
            dual_write_map: DashMap::new(),
            shard_pin_map: DashMap::new(),
        }
    }

    /// Register a tenant and its rate limiter (if configured).
    pub fn register_tenant(&self, config: TenantConfig) {
        if let Some(rate) = config.max_signals_per_sec {
            let limiter = Arc::new(TenantRateLimiter::new(rate));
            self.rate_limiters.insert(config.tenant_id, limiter);
        }
        self.tenants.insert(config.tenant_id, config);
    }

    /// Return the rate limiter for a tenant, if one is configured.
    #[must_use]
    pub fn rate_limiter_for(&self, tenant_id: TenantId) -> Option<Arc<TenantRateLimiter>> {
        self.rate_limiters.get(&tenant_id).map(|r| Arc::clone(&*r))
    }

    /// Route an entity to a shard via jump consistent hash.
    ///
    /// If the tenant has been pinned to a shard (after migration finalization),
    /// the pinned shard is returned directly, bypassing the hash.
    ///
    /// # Errors
    ///
    /// Returns an error if no eligible shards exist for the tenant's region constraints.
    pub fn route(
        &self,
        tenant_id: TenantId,
        entity_id: EntityId,
    ) -> crate::Result<ShardAssignment> {
        let topo = self
            .topology
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);

        // After migration finalization a tenant is pinned to its new shard.
        if let Some(pinned) = self.shard_pin_map.get(&tenant_id) {
            let shard_id = *pinned;
            // The pinned shard MUST exist in the topology. Fabricating a
            // `RegionId::SINGLE` assignment for a missing shard would silently
            // mis-route writes to a region the operator never configured (and
            // hide a real topology/pin desync). Surface it as a typed error.
            return find_shard_assignment(&topo, shard_id);
        }

        let eligible = self.eligible_shards_for(tenant_id, &topo)?;
        drop(topo);
        Ok(jump_hash_select(entity_id.as_u64(), &eligible))
    }

    /// Return the number of registered tenants.
    #[must_use]
    pub fn tenant_count(&self) -> usize {
        self.tenants.len()
    }

    /// Enter dual-write mode for `tenant_id`: writes are routed to both
    /// `source_shard` and `target_shard` until `finalize_migration` is called.
    ///
    /// Reads continue to route to the source shard (via `route()`) until
    /// finalization pins the tenant to the target.
    pub fn set_dual_write(&self, tenant_id: TenantId, source: ShardId, target: ShardId) {
        self.dual_write_map.insert(tenant_id, (source, target));
    }

    /// Finalize migration for `tenant_id`: clear dual-write mode and pin all
    /// future routing to `target_shard`.
    ///
    /// After this call, `route()` and `write_assignments()` both return
    /// `target_shard` exclusively. The old source shard's copy of the tenant's
    /// data is now safe to reclaim, but this method does **not** reclaim it —
    /// physical source-shard reclamation is a separate, not-yet-wired step (see
    /// [`TenantMigration::mark_complete`](crate::replication::migration::TenantMigration::mark_complete)
    /// and the source-reclamation task in `docs/specs/14-scale-architecture.md`).
    pub fn finalize_migration(&self, tenant_id: TenantId, target: ShardId) {
        self.dual_write_map.remove(&tenant_id);
        self.shard_pin_map.insert(tenant_id, target);
    }

    /// Return all shard assignments that a write for `(tenant_id, entity_id)`
    /// must be sent to.
    ///
    /// - Normal mode: one shard (same as `route()`).
    /// - Dual-write mode: two shards — source and target — so both copies are
    ///   updated during migration with no signal loss.
    ///
    /// On a single-node deployment the caller writes to local storage once;
    /// the returned assignments carry the routing metadata for multi-node use.
    ///
    /// # Errors
    ///
    /// Propagates errors from `route()` (e.g., no eligible shards).
    #[allow(clippy::significant_drop_tightening)] // topology read guard scoped to the dual-write lookup
    pub fn write_assignments(
        &self,
        tenant_id: TenantId,
        entity_id: EntityId,
    ) -> crate::Result<Vec<ShardAssignment>> {
        if let Some(pair) = self.dual_write_map.get(&tenant_id) {
            let (source_shard, target_shard) = *pair;
            let topo = self
                .topology
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            // Both dual-write targets must exist in the topology. A missing
            // shard means the migration was configured against a stale topology;
            // returning an error is correct — fabricating a `RegionId::SINGLE`
            // assignment would drop one side of the dual write with no signal
            // loss visible to the caller.
            let source = find_shard_assignment(&topo, source_shard)?;
            let target = find_shard_assignment(&topo, target_shard)?;
            return Ok(vec![source, target]);
        }
        Ok(vec![self.route(tenant_id, entity_id)?])
    }

    /// Return `true` if `tenant_id` is currently in dual-write migration mode.
    #[must_use]
    pub fn is_dual_write(&self, tenant_id: TenantId) -> bool {
        self.dual_write_map.contains_key(&tenant_id)
    }

    /// Return the pinned target shard for `tenant_id` after migration, if any.
    #[must_use]
    pub fn pinned_shard(&self, tenant_id: TenantId) -> Option<ShardId> {
        self.shard_pin_map.get(&tenant_id).map(|r| *r)
    }

    /// Snapshot ONLY `tenant_id`'s routing (its dual-write pair + its pin), for a
    /// targeted rollback.
    ///
    /// Unlike [`to_checkpoint_bytes`](Self::to_checkpoint_bytes) (which snapshots
    /// the WHOLE router), this captures a single tenant so a migration that fails
    /// to persist can restore exactly its own prior routing without touching any
    /// other tenant's entry. A full-router restore would clobber a *concurrent*
    /// migration's just-applied (and possibly already-persisted) routing for a
    /// different tenant, because all migrations share one router with only a
    /// per-migration state lock (C10).
    #[must_use]
    pub fn snapshot_tenant_routing(&self, tenant_id: TenantId) -> TenantRoutingSnapshot {
        TenantRoutingSnapshot {
            tenant_id,
            dual_write: self.dual_write_map.get(&tenant_id).map(|r| *r),
            pin: self.shard_pin_map.get(&tenant_id).map(|r| *r),
        }
    }

    /// Restore ONLY the snapshotted tenant's routing, leaving every other tenant
    /// untouched.
    ///
    /// Sets the tenant's dual-write pair and pin back to exactly what
    /// [`snapshot_tenant_routing`](Self::snapshot_tenant_routing) captured —
    /// removing whichever side was absent in the snapshot. This is the targeted
    /// counterpart to the rollback path in the migration coordinator: a failed
    /// persist for tenant A must not disturb tenant B's concurrent routing (C10).
    pub fn restore_tenant_routing(&self, snapshot: &TenantRoutingSnapshot) {
        match snapshot.dual_write {
            Some(pair) => {
                self.dual_write_map.insert(snapshot.tenant_id, pair);
            }
            None => {
                self.dual_write_map.remove(&snapshot.tenant_id);
            }
        }
        match snapshot.pin {
            Some(shard) => {
                self.shard_pin_map.insert(snapshot.tenant_id, shard);
            }
            None => {
                self.shard_pin_map.remove(&snapshot.tenant_id);
            }
        }
    }

    // ── Migration routing durability (Accuracy-W) ───────────────────────────
    //
    // The dual-write and shard-pin maps decide where a tenant's writes go during
    // and after a migration. They used to live ONLY in memory, so a crash
    // mid-migration silently reverted a finalized tenant to jump-hash routing —
    // re-scattering its data to the wrong shard. These maps are now serialized to
    // a single durable row and restored on open.

    /// Serialize the migration routing state (dual-write pairs + shard pins) for
    /// the durable checkpoint.
    ///
    /// JSON with string keys (the [`TenantId`] as a decimal string) because JSON
    /// object keys must be strings; values are the raw `ShardId` `u16`s. The two
    /// maps are written as one object so a single row captures the whole routing
    /// state atomically.
    ///
    /// # Panics
    ///
    /// Panics only if `serde_json` fails to serialize a `HashMap` of plain
    /// integers, which is unreachable (both key and value types are always
    /// representable).
    #[must_use]
    pub fn to_checkpoint_bytes(&self) -> Vec<u8> {
        let dual: std::collections::HashMap<String, (u16, u16)> = self
            .dual_write_map
            .iter()
            .map(|r| (r.key().0.to_string(), (r.value().0.0, r.value().1.0)))
            .collect();
        let pins: std::collections::HashMap<String, u16> = self
            .shard_pin_map
            .iter()
            .map(|r| (r.key().0.to_string(), r.value().0))
            .collect();
        let snap = MigrationRoutingCheckpoint { dual, pins };
        serde_json::to_vec(&snap).expect("migration routing serialization is infallible")
    }

    /// Restore the migration routing state from checkpoint bytes, replacing any
    /// current in-memory routing.
    ///
    /// Malformed bytes restore nothing (the maps are left empty), which is the
    /// safe degradation: a tenant with no pin/dual-write falls back to jump-hash
    /// routing, identical to a never-migrated tenant. Returns the number of
    /// tenants whose routing was restored (dual-write + pinned).
    #[must_use]
    pub fn restore_from_checkpoint_bytes(&self, bytes: &[u8]) -> usize {
        let snap: MigrationRoutingCheckpoint = serde_json::from_slice(bytes).unwrap_or_default();
        self.dual_write_map.clear();
        self.shard_pin_map.clear();
        for (tid, (src, tgt)) in snap.dual {
            if let Ok(id) = tid.parse::<u64>() {
                self.dual_write_map
                    .insert(TenantId(id), (ShardId(src), ShardId(tgt)));
            }
        }
        for (tid, shard) in snap.pins {
            if let Ok(id) = tid.parse::<u64>() {
                self.shard_pin_map.insert(TenantId(id), ShardId(shard));
            }
        }
        self.dual_write_map.len() + self.shard_pin_map.len()
    }

    fn eligible_shards_for(
        &self,
        tenant_id: TenantId,
        topo: &ClusterTopology,
    ) -> crate::Result<Vec<ShardAssignment>> {
        let required_regions: Vec<RegionId> = self
            .tenants
            .get(&tenant_id)
            .map(|c| c.required_regions.clone())
            .unwrap_or_default();

        let eligible: Vec<ShardAssignment> = if required_regions.is_empty() {
            topo.shards.clone()
        } else {
            topo.shards
                .iter()
                .filter(|s| required_regions.contains(&s.region_id))
                .copied()
                .collect()
        };

        if eligible.is_empty() {
            return Err(crate::TidalError::internal(
                "route",
                "no eligible shards for tenant",
            ));
        }
        Ok(eligible)
    }
}

/// Look up the [`ShardAssignment`] for `shard_id` in `topo`.
///
/// Shared by [`TenantRouter::route`] (pinned path) and
/// [`TenantRouter::write_assignments`] (dual-write path), which both need to map
/// an already-decided [`ShardId`] back to its full assignment.
///
/// # Errors
///
/// Returns [`crate::TidalError::internal`] if no shard in the topology matches
/// `shard_id`. A missing shard indicates a pin / dual-write target that points
/// at a region the topology no longer contains; callers must surface this rather
/// than fabricating a placeholder region (which would silently mis-route).
fn find_shard_assignment(
    topo: &ClusterTopology,
    shard_id: ShardId,
) -> crate::Result<ShardAssignment> {
    topo.shards
        .iter()
        .find(|s| s.shard_id == shard_id)
        .copied()
        .ok_or_else(|| {
            crate::TidalError::internal(
                "route",
                format!("shard {shard_id} not present in cluster topology"),
            )
        })
}

/// Jump consistent hash (Lamping & Veach, 2014).
///
/// The casts between i64/u64/f64 are inherent to the algorithm's use of
/// signed intermediates and floating-point arithmetic. They are safe because:
/// - `n` (shard count) is always small (< 2^16 in practice)
/// - `b` and `j` are bounded by `n`
/// - The final `b as usize` is non-negative since the loop ensures `b >= 0`
#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)]
fn jump_hash_select(key: u64, shards: &[ShardAssignment]) -> ShardAssignment {
    debug_assert!(
        !shards.is_empty(),
        "jump_hash_select requires non-empty shards"
    );
    let n = shards.len() as u64;
    let mut k = key;
    let mut b: i64 = -1;
    let mut j: i64 = 0;
    while j < n as i64 {
        b = j;
        k = k.wrapping_mul(2_862_933_555_777_941_757).wrapping_add(1);
        j = ((b + 1) as f64 * (f64::from(1u32 << 31) / ((k >> 33) + 1) as f64)) as i64;
    }
    shards[b as usize]
}

// NOTE: per-tenant physical storage/WAL isolation is intentionally NOT
// implemented here. On a single node hosting multiple tenants, `tenant_id`
// influences rate limiting and (multi-node) routing only — all tenants' signals
// land in one shared keyspace/WAL. The former `tenant_wal_dir` helper (which
// built `/data/tenants/{id}/wal` paths) was dead code with no production caller;
// keeping it implied a physical-isolation guarantee the write path never
// provided, so it was removed (W17). Physical per-tenant keyspace/WAL isolation
// and enforcement of `TenantConfig::max_storage_bytes`/`max_entities` are a
// separate, not-yet-wired task tracked in `docs/specs/14-scale-architecture.md`.

// ── Migration routing checkpoint persistence ────────────────────────────────

/// Storage key for the single migration-routing checkpoint row (entity id 0,
/// [`Tag::MigrationRouting`](crate::storage::Tag::MigrationRouting)).
#[must_use]
fn migration_routing_key() -> Vec<u8> {
    crate::storage::encode_key(
        EntityId::new(0),
        crate::storage::Tag::MigrationRouting,
        b"route",
    )
}

/// Durably persist the tenant router's migration routing state (dual-write pairs
/// + shard pins).
///
/// Called from the migration coordinator on every routing transition (enter
/// dual-write, finalize) so a crash mid-migration cannot silently revert a
/// finalized tenant to jump-hash routing. Writes one row and fsyncs.
///
/// # Errors
///
/// Returns `TidalError::Storage` if the write or flush fails.
pub(crate) fn checkpoint_migration_routing(
    storage: &dyn crate::storage::StorageEngine,
    router: &TenantRouter,
) -> crate::Result<()> {
    let bytes = router.to_checkpoint_bytes();
    let mut batch = crate::storage::WriteBatch::new();
    batch.put(migration_routing_key(), bytes);
    storage.write_batch(batch)?;
    storage.flush()?;
    Ok(())
}

/// Restore the tenant router's migration routing state from its durable
/// checkpoint on open.
///
/// On first boot / no checkpoint / ephemeral mode / corrupt bytes this restores
/// nothing (every tenant uses jump-hash routing), which is correct — identical
/// to a cluster that never migrated a tenant. Returns the number of tenants
/// whose routing was restored.
///
/// # Errors
///
/// Returns `TidalError::Storage` if the checkpoint read fails (the caller may
/// treat this as non-fatal and start with empty routing).
pub(crate) fn restore_migration_routing(
    storage: &dyn crate::storage::StorageEngine,
    router: &TenantRouter,
) -> crate::Result<usize> {
    match storage.get(&migration_routing_key())? {
        Some(bytes) => {
            let n = router.restore_from_checkpoint_bytes(&bytes);
            if n > 0 {
                tracing::info!(
                    tenants = n,
                    "migration routing state restored from checkpoint"
                );
            }
            Ok(n)
        }
        None => Ok(0),
    }
}

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

    #[test]
    fn tenant_id_default_is_zero() {
        assert_eq!(TenantId::DEFAULT.0, 0);
    }

    #[test]
    fn tenant_id_display() {
        assert_eq!(TenantId(42).to_string(), "tenant:42");
    }

    #[test]
    fn rate_limiter_allows_burst() {
        let limiter = TenantRateLimiter::new(100);
        for _ in 0..200 {
            assert!(limiter.try_acquire().is_ok());
        }
        assert!(limiter.try_acquire().is_err());
    }

    /// WARNING (REPL): under contention the limiter must not over-grant tokens.
    /// Many threads hammering the bucket may collectively succeed at most
    /// `max_tokens` (the initial burst) plus the refill accrued over the very
    /// short test window — never the multiplied-refill amount the old
    /// store-after-CAS race allowed.
    #[test]
    fn rate_limiter_no_over_grant_under_contention() {
        use std::sync::Arc;
        use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};

        // 50 signals/sec → max_tokens = 100 (2x burst). At 50/sec the refill is
        // 50 tokens per second = 0.05 tokens/ms; the whole test runs in well
        // under 100 ms, so the legitimate refill cap is tiny.
        let limiter = Arc::new(TenantRateLimiter::new(50));
        let max_tokens = 100u64; // = max_signals_per_sec * 2

        let granted = Arc::new(AtomicU64::new(0));
        let start = std::time::Instant::now();

        let mut handles = vec![];
        for _ in 0..8 {
            let l = Arc::clone(&limiter);
            let g = Arc::clone(&granted);
            handles.push(std::thread::spawn(move || {
                // Each thread tries far more than the bucket holds.
                for _ in 0..200 {
                    if l.try_acquire().is_ok() {
                        g.fetch_add(1, AtomicOrdering::Relaxed);
                    }
                }
            }));
        }
        for h in handles {
            h.join().unwrap();
        }

        let elapsed_ms = start.elapsed().as_millis() as u64;
        // Legitimate refill over the test window, generously rounded up.
        // 50 tokens/sec = 0.05 tok/ms = elapsed_ms / 20, plus a small slack.
        let refill_bound = elapsed_ms / 20 + 2;
        let total = granted.load(AtomicOrdering::Relaxed);
        assert!(
            total <= max_tokens + refill_bound,
            "over-grant detected: granted {total} > burst {max_tokens} + refill {refill_bound} \
             (elapsed {elapsed_ms} ms)"
        );
        // Sanity: the full burst should be grantable (no spurious starvation).
        assert!(
            total >= max_tokens,
            "under-granted: only {total} of {max_tokens}"
        );
    }

    #[test]
    fn cluster_topology_single() {
        let t = ClusterTopology::single();
        assert_eq!(t.shards.len(), 1);
        assert_eq!(t.shards[0].shard_id, ShardId::SINGLE);
    }

    #[test]
    fn tenant_router_register_and_count() {
        let topo = Arc::new(RwLock::new(ClusterTopology::single()));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        assert_eq!(router.tenant_count(), 1);
    }

    #[test]
    fn tenant_router_rate_limiter_cached() {
        let topo = Arc::new(RwLock::new(ClusterTopology::single()));
        let router = TenantRouter::new(topo);
        let mut cfg = TenantConfig::default_tenant();
        cfg.max_signals_per_sec = Some(50);
        router.register_tenant(cfg);
        let l1 = router.rate_limiter_for(TenantId::DEFAULT).unwrap();
        let l2 = router.rate_limiter_for(TenantId::DEFAULT).unwrap();
        assert!(Arc::ptr_eq(&l1, &l2));
    }

    #[test]
    fn tenant_router_route_single_shard() {
        let topo = Arc::new(RwLock::new(ClusterTopology::single()));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        let assignment = router.route(TenantId::DEFAULT, EntityId::new(42)).unwrap();
        assert_eq!(assignment.shard_id, ShardId::SINGLE);
    }

    #[test]
    fn jump_hash_distributes_uniformly() {
        let shards: Vec<ShardAssignment> = (0..4u16)
            .map(|i| ShardAssignment {
                shard_id: ShardId(i),
                region_id: RegionId::SINGLE,
            })
            .collect();
        let mut counts = [0usize; 4];
        for i in 0..4000u64 {
            let a = jump_hash_select(i, &shards);
            counts[a.shard_id.0 as usize] += 1;
        }
        for c in counts {
            assert!(
                c > 800 && c < 1200,
                "bucket count {c} out of expected range"
            );
        }
    }

    #[test]
    fn route_pinned_to_present_shard_returns_assignment() {
        // Topology has two shards in distinct regions; pin to shard 1.
        let topo = Arc::new(RwLock::new(ClusterTopology {
            shards: vec![
                ShardAssignment {
                    shard_id: ShardId(0),
                    region_id: RegionId(0),
                },
                ShardAssignment {
                    shard_id: ShardId(1),
                    region_id: RegionId(5),
                },
            ],
        }));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        router.finalize_migration(TenantId::DEFAULT, ShardId(1));

        let assignment = router.route(TenantId::DEFAULT, EntityId::new(42)).unwrap();
        assert_eq!(assignment.shard_id, ShardId(1));
        // The real region must be returned, NOT a fabricated RegionId::SINGLE.
        assert_eq!(assignment.region_id, RegionId(5));
    }

    #[test]
    fn route_pinned_to_absent_shard_errors() {
        // Pin to a shard that does not exist in the topology.
        let topo = Arc::new(RwLock::new(ClusterTopology::single()));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        router.finalize_migration(TenantId::DEFAULT, ShardId(99));

        let result = router.route(TenantId::DEFAULT, EntityId::new(1));
        assert!(
            result.is_err(),
            "pinning to an absent shard must error, not fabricate a region"
        );
    }

    #[test]
    fn write_assignments_dual_write_absent_target_errors() {
        // Source exists; target does not. Dual-write must error rather than
        // silently dropping one side of the write.
        let topo = Arc::new(RwLock::new(ClusterTopology::single()));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        router.set_dual_write(TenantId::DEFAULT, ShardId::SINGLE, ShardId(99));

        let result = router.write_assignments(TenantId::DEFAULT, EntityId::new(1));
        assert!(
            result.is_err(),
            "dual-write to an absent target shard must error"
        );
    }

    #[test]
    fn write_assignments_dual_write_present_targets_ok() {
        let topo = Arc::new(RwLock::new(ClusterTopology {
            shards: vec![
                ShardAssignment {
                    shard_id: ShardId(0),
                    region_id: RegionId(0),
                },
                ShardAssignment {
                    shard_id: ShardId(1),
                    region_id: RegionId(1),
                },
            ],
        }));
        let router = TenantRouter::new(topo);
        router.register_tenant(TenantConfig::default_tenant());
        router.set_dual_write(TenantId::DEFAULT, ShardId(0), ShardId(1));

        let assignments = router
            .write_assignments(TenantId::DEFAULT, EntityId::new(1))
            .unwrap();
        assert_eq!(assignments.len(), 2);
        assert_eq!(assignments[0].shard_id, ShardId(0));
        assert_eq!(assignments[1].shard_id, ShardId(1));
        assert_eq!(assignments[1].region_id, RegionId(1));
    }

    // ── Migration routing durability (Accuracy-W) ───────────────────────────

    fn two_shard_router() -> TenantRouter {
        let topo = Arc::new(RwLock::new(ClusterTopology {
            shards: vec![
                ShardAssignment {
                    shard_id: ShardId(0),
                    region_id: RegionId(0),
                },
                ShardAssignment {
                    shard_id: ShardId(1),
                    region_id: RegionId(1),
                },
            ],
        }));
        TenantRouter::new(topo)
    }

    #[test]
    fn migration_routing_checkpoint_roundtrips_dual_write_and_pins() {
        let router = two_shard_router();
        router.set_dual_write(TenantId(5), ShardId(0), ShardId(1));
        router.finalize_migration(TenantId(9), ShardId(1)); // pins 9 -> shard 1

        let bytes = router.to_checkpoint_bytes();

        // Restore into a fresh router (different identity, same topology).
        let restored = two_shard_router();
        let n = restored.restore_from_checkpoint_bytes(&bytes);
        assert_eq!(n, 2, "one dual-write tenant + one pinned tenant restored");

        assert!(
            restored.is_dual_write(TenantId(5)),
            "dual-write routing must survive the checkpoint"
        );
        assert_eq!(
            restored.pinned_shard(TenantId(9)),
            Some(ShardId(1)),
            "the finalized pin must survive the checkpoint (no revert to jump-hash)"
        );
        // The pinned tenant routes to its target shard, NOT a jump-hash result.
        let assignment = restored.route(TenantId(9), EntityId::new(123)).unwrap();
        assert_eq!(assignment.shard_id, ShardId(1));
    }

    #[test]
    fn migration_routing_restore_replaces_current_state() {
        let router = two_shard_router();
        // Pre-existing (stale) routing that the restore must overwrite.
        router.set_dual_write(TenantId(1), ShardId(1), ShardId(0));

        // Checkpoint from a DIFFERENT router with a single pinned tenant.
        let source = two_shard_router();
        source.finalize_migration(TenantId(2), ShardId(1));
        let bytes = source.to_checkpoint_bytes();

        let _ = router.restore_from_checkpoint_bytes(&bytes);
        assert!(
            !router.is_dual_write(TenantId(1)),
            "restore must clear stale in-memory routing"
        );
        assert_eq!(router.pinned_shard(TenantId(2)), Some(ShardId(1)));
    }

    #[test]
    fn migration_routing_malformed_bytes_restore_empty() {
        let router = two_shard_router();
        let n = router.restore_from_checkpoint_bytes(b"not json");
        assert_eq!(
            n, 0,
            "malformed bytes restore nothing (safe jump-hash fallback)"
        );
        assert!(!router.is_dual_write(TenantId(1)));
        assert_eq!(router.pinned_shard(TenantId(1)), None);
    }

    #[test]
    fn migration_routing_persist_and_restore_through_storage() {
        use crate::storage::InMemoryBackend;

        let storage = InMemoryBackend::new();
        let router = two_shard_router();
        router.finalize_migration(TenantId(7), ShardId(1));

        super::checkpoint_migration_routing(&storage, &router).unwrap();

        // A fresh router restores the pin from the durable row.
        let restored = two_shard_router();
        let n = super::restore_migration_routing(&storage, &restored).unwrap();
        assert_eq!(n, 1);
        assert_eq!(restored.pinned_shard(TenantId(7)), Some(ShardId(1)));
    }

    #[test]
    fn migration_routing_restore_missing_row_is_empty() {
        use crate::storage::InMemoryBackend;

        let storage = InMemoryBackend::new();
        let router = two_shard_router();
        let n = super::restore_migration_routing(&storage, &router).unwrap();
        assert_eq!(n, 0, "no checkpoint row → nothing restored");
    }
}
