// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Per-block view of currently active features.
//!
//! `ActiveFeatures` is the immutable snapshot the bank installs once per block
//! after the subdag-fed clock update, and from which every hot-path gating site
//! reads. The map value is the **current bank slot** at the moment the
//! snapshot was computed; it is informational (debugging, metrics) and is not
//! persisted — historical activation slots would require on-chain state the
//! design intentionally avoids.
//!
//! This type lives in `rialo-s-program-runtime` so it can be threaded
//! through `EnvironmentConfig` → `InvokeContext`, reachable by
//! every builtin processor. The on-chain wire crate
//! `feature-management-program-interface` stays `#![no_std]` and free of
//! the `ahash` dependency.

use ahash::AHashMap;
use rialo_feature_management_interface::state::FeaturesState;

/// Slot number alias used by the runtime view.
///
/// Aliased locally rather than imported from `rialo-s-clock` because the
/// inherited Solana SDK is not the canonical source for this concept in
/// Rialo's runtime.
pub type Slot = u64;

/// Immutable per-block snapshot of active features.
///
/// Built once per bank inside `execute_blob`, immediately after
/// `update_clock`, and held under `Arc` so every consumer in the block sees
/// the same view. There is no mid-block mutation, no lazy refresh, and no
/// cross-block caching.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ActiveFeatures {
    /// Active feature name → bank slot at snapshot time.
    active: AHashMap<String, Slot>,
}

impl ActiveFeatures {
    /// Empty snapshot — useful at genesis and in tests.
    pub fn new() -> Self {
        Self::default()
    }

    /// Build the snapshot for a bank from `FeaturesState`. Every entry name
    /// in the state is added to the snapshot, keyed by `snapshot_slot` (the
    /// current bank slot — informational, not the historical activation
    /// slot, which would require on-chain state the design intentionally
    /// avoids).
    ///
    /// Activation is presence-based: a feature is active iff its name is
    /// in `state.entries()`. No clock consult.
    ///
    /// This is the canonical producer the bank calls inside
    /// `recompute_active_features`. Construction lives here (not in the
    /// caller) so the producer surface stays in this crate; `from_map` can
    /// remain `pub(crate)`.
    pub fn from_features_state(state: &FeaturesState, snapshot_slot: Slot) -> Self {
        let active: AHashMap<String, Slot> = state
            .entries()
            .iter()
            .map(|name| (name.clone(), snapshot_slot))
            .collect();
        Self::from_map(active)
    }

    /// Snapshot from a pre-built map of the **complete** active set.
    ///
    /// This is the canonical producer used by `Bank::recompute_active_features`:
    /// the bank reads `FeaturesState.entries` and hands the full map (name
    /// → current bank slot) here. A feature absent from the map is
    /// inactive — there is no "missing-means-default" fallback. Tests that
    /// want sugar for "mark these names as active in an otherwise empty
    /// snapshot" should use `with_overrides` instead.
    ///
    /// The strict `KNOWN_FEATURES` check lives in
    /// `Bank::can_process_block` — running the same check
    /// here would only fire on test fixtures that legitimately use
    /// fabricated names. The producer paths
    /// (`Self::from_features_state`, `with_overrides`) supply the names;
    /// the readiness check is what halts a release node on an unknown one.
    ///
    /// `pub(crate)` so the only producer is `from_features_state` /
    /// `with_overrides` inside this crate, matching the NORTHSTAR contract
    /// "the only producer of `Arc<ActiveFeatures>` is the per-block
    /// recomputation inside `execute_blob`." Cross-crate construction goes
    /// through `from_features_state`, which takes already-validated
    /// `FeaturesState` bytes.
    pub(crate) fn from_map(active: AHashMap<String, Slot>) -> Self {
        Self { active }
    }

    /// Test affordance — marks the named features as active in an
    /// otherwise-empty snapshot.
    ///
    /// All overrides share the same snapshot slot `0`. Tests that need
    /// distinct slots per feature should build the `AHashMap` themselves
    /// and use `from_map`. Semantics intentionally differ from `from_map`:
    /// `with_overrides` is "start from empty, add these"; `from_map` is
    /// "this *is* the complete active set."
    ///
    /// Gated on `cfg(test)` or the `dev-context-only-utils` feature so the
    /// affordance never reaches a release binary.
    #[cfg(any(test, feature = "dev-context-only-utils"))]
    pub fn with_overrides(features: &[&str]) -> Self {
        let active: AHashMap<String, Slot> = features
            .iter()
            .map(|name| ((*name).to_string(), 0))
            .collect();
        Self::from_map(active)
    }

    /// Returns `true` if `feature` is active in this snapshot.
    pub fn is_active(&self, feature: &str) -> bool {
        self.active.contains_key(feature)
    }

    /// Returns the recorded snapshot slot for `feature`, if active.
    ///
    /// This is the bank slot at the moment the snapshot was computed, not
    /// the historical activation slot.
    pub fn snapshot_slot(&self, feature: &str) -> Option<Slot> {
        self.active.get(feature).copied()
    }

    /// Number of active features in the snapshot.
    pub fn len(&self) -> usize {
        self.active.len()
    }

    /// `true` iff no features are active.
    pub fn is_empty(&self) -> bool {
        self.active.is_empty()
    }

    /// Iterator over `(feature_name, snapshot_slot)`.
    pub fn iter(&self) -> impl Iterator<Item = (&str, Slot)> + '_ {
        self.active.iter().map(|(k, v)| (k.as_str(), *v))
    }
}

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

    fn make(entries: &[(&str, Slot)]) -> ActiveFeatures {
        let mut m = AHashMap::new();
        for (k, v) in entries {
            m.insert((*k).to_string(), *v);
        }
        ActiveFeatures::from_map(m)
    }

    #[test]
    fn test_empty_is_empty() {
        let a = ActiveFeatures::new();
        assert!(a.is_empty());
        assert_eq!(a.len(), 0);
        assert!(!a.is_active("anything"));
        assert_eq!(a.snapshot_slot("anything"), None);
    }

    #[test]
    fn test_is_active_reflects_membership() {
        let a = make(&[("foo", 42), ("bar", 100)]);
        assert!(a.is_active("foo"));
        assert!(a.is_active("bar"));
        assert!(!a.is_active("baz"));
    }

    #[test]
    fn test_snapshot_slot_returns_recorded_value() {
        let a = make(&[("foo", 42)]);
        assert_eq!(a.snapshot_slot("foo"), Some(42));
        assert_eq!(a.snapshot_slot("missing"), None);
    }

    #[test]
    fn test_iter_covers_all_entries() {
        let a = make(&[("foo", 1), ("bar", 2)]);
        let mut seen: Vec<(&str, Slot)> = a.iter().collect();
        seen.sort_by_key(|(k, _)| *k);
        assert_eq!(seen, vec![("bar", 2), ("foo", 1)]);
    }

    #[test]
    fn test_with_overrides_marks_listed_features_active() {
        let a = ActiveFeatures::with_overrides(&["foo", "bar"]);
        assert!(a.is_active("foo"));
        assert!(a.is_active("bar"));
        assert!(!a.is_active("baz"));
        assert_eq!(a.snapshot_slot("foo"), Some(0));
    }

    #[test]
    fn test_with_overrides_empty_yields_empty() {
        let a = ActiveFeatures::with_overrides(&[]);
        assert!(a.is_empty());
    }
}
