//! Boundary traits and data types for the signal ledger.
//!
//! Contains the `WalWriter` trait (the boundary between the signals subsystem
//! and the WAL), its no-op test implementation, and the `EntitySignalEntry`
//! combined hot + warm state container.

use std::fmt;

use super::super::{SignalTypeId, hot::HotSignalState, warm::BucketedCounter};
use crate::schema::{EntityId, Timestamp};

// ── WAL boundary ─────────────────────────────────────────────────────────────

/// Trait boundary for WAL integration.
///
/// `m1p2` provides the real implementation. `m1p4` tests use `NoopWalWriter`.
/// The `SignalLedger` calls `append_signal()` before updating in-memory state,
/// ensuring WAL-first durability semantics.
pub trait WalWriter: Send + Sync {
    /// Append a signal event to the WAL.
    ///
    /// Returns `Ok(())` when the event is durably committed. After this
    /// returns, in-memory state is updated.
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Durability` if the WAL write fails.
    fn append_signal(
        &self,
        signal_type_id: SignalTypeId,
        entity_id: EntityId,
        weight: f64,
        timestamp: Timestamp,
    ) -> crate::Result<()>;

    /// Append a signal event carrying the M9 governance envelope (scope,
    /// interned writer agent, share-policy version, membership epoch).
    ///
    /// The default implementation drops the governance metadata and delegates
    /// to [`append_signal`](Self::append_signal); the live WAL bridge overrides
    /// it to persist a v3 [`EventRecord`](crate::wal::format::EventRecord). This
    /// keeps the common (local, ungoverned) path untouched while letting scoped
    /// writes flow through without every `WalWriter` implementor opting in.
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Durability` if the WAL write fails.
    // The governance envelope is four flat wire fields; bundling them into a
    // struct here would only add indirection at the lowest WAL boundary.
    #[allow(clippy::too_many_arguments)]
    fn append_signal_scoped(
        &self,
        signal_type_id: SignalTypeId,
        entity_id: EntityId,
        weight: f64,
        timestamp: Timestamp,
        _scope: u8,
        _writer_agent: u16,
        _share_policy_version: u16,
        _membership_epoch: u32,
    ) -> crate::Result<()> {
        self.append_signal(signal_type_id, entity_id, weight, timestamp)
    }
}

/// No-op WAL writer for testing. Always succeeds without writing anything.
pub struct NoopWalWriter;

impl WalWriter for NoopWalWriter {
    fn append_signal(
        &self,
        _signal_type_id: SignalTypeId,
        _entity_id: EntityId,
        _weight: f64,
        _timestamp: Timestamp,
    ) -> crate::Result<()> {
        Ok(())
    }
}

impl fmt::Debug for NoopWalWriter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("NoopWalWriter")
    }
}

// ── Entry ─────────────────────────────────────────────────────────────────────

/// Combined hot-tier and warm-tier state for one entity-signal pair.
pub struct EntitySignalEntry {
    /// Running exponentially-decayed score (hot tier).
    pub hot: HotSignalState,
    /// Bucketed windowed event counters (warm tier).
    pub warm: BucketedCounter,
}

impl fmt::Debug for EntitySignalEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("EntitySignalEntry")
            .field("hot", &self.hot)
            .field("warm", &self.warm)
            .finish()
    }
}
