//! Signal ledger: top-level coordinator for hot-tier decay scores and
//! warm-tier bucketed counters across all active entities.
//!
//! `SignalLedger` is the single entry point for signal state management.
//! It owns a `DashMap<(EntityId, SignalTypeId), EntitySignalEntry>` that
//! provides concurrent access to per-entity signal state.
//!
//! # WAL integration
//!
//! Every `record_signal()` call first appends the event to the WAL via the
//! `WalWriter` trait. Only after the WAL confirms durability does the ledger
//! update in-memory state. This ensures signals survive crashes.
//!
//! # Concurrency
//!
//! Multiple threads can write signals to different entities simultaneously.
//! Writes to the same entity contend on the `DashMap` shard lock only for entry
//! lookup; the actual state update (CAS on hot tier, atomic increment on warm
//! tier) is lock-free once the entry reference is obtained.

pub mod core;
pub mod types;

pub use core::SignalLedger;

pub use types::{EntitySignalEntry, NoopWalWriter, WalWriter};

use std::collections::HashMap;

use super::SignalTypeId;
use crate::schema::{DecayModel, Schema};

/// Derive the canonical `signal_name -> SignalTypeId` and `SignalTypeId -> lambdas`
/// maps from a validated schema.
///
/// This is the single source of truth for how signal types are numbered and how
/// their decay rates are cached. Signal types are enumerated in **alphabetical
/// order by name** and assigned sequential [`SignalTypeId`] values (0, 1, 2, …).
/// Each type's lambda vector is taken from its decay model: a single-element
/// vector for [`DecayModel::Exponential`], an empty vector for
/// [`DecayModel::Linear`] / [`DecayModel::Permanent`] (no exponential rate).
///
/// Both [`SignalLedger::new`] and `CohortSignalLedger::new` MUST build their id
/// and lambda maps through this helper. The cohort ledger keys its entries by
/// `(cohort, entity, signal_type_id)` and shares the global ledger's schema, so
/// the two ledgers must assign **identical** ids: any drift would silently
/// misattribute cohort signals to the wrong global signal type. Centralizing the
/// derivation here makes that drift impossible by construction.
#[must_use]
pub(crate) fn derive_signal_ids_and_lambdas(
    schema: &Schema,
) -> (
    HashMap<String, SignalTypeId>,
    HashMap<SignalTypeId, Vec<f64>>,
) {
    let mut signal_list: Vec<&crate::schema::SignalTypeDef> = schema.signals().collect();
    signal_list.sort_by_key(|s| s.name());

    let mut signal_name_to_id = HashMap::with_capacity(signal_list.len());
    let mut signal_lambdas = HashMap::with_capacity(signal_list.len());

    for (idx, sig) in signal_list.iter().enumerate() {
        // idx is bounded by the schema's signal count (validated to fit u16).
        let type_id = SignalTypeId::new(idx as u16);
        signal_name_to_id.insert(sig.name().to_owned(), type_id);

        let lambdas = match sig.decay() {
            DecayModel::Exponential { lambda, .. } => vec![*lambda],
            DecayModel::Linear { .. } | DecayModel::Permanent => vec![],
        };
        signal_lambdas.insert(type_id, lambdas);
    }

    (signal_name_to_id, signal_lambdas)
}
