//! Signal provenance: the audit anchor for a ranking-affecting signal.
//!
//! [`SignalProvenance`] records *who* wrote a signal, in *what scope*, under
//! which *share-policy version* and *membership epoch*, ordered by HLC. M9 uses
//! the scope + epoch to drive shipping and purge; M10p3 builds the
//! explainability / remove-by-scope surfaces on top of the full record.
//!
//! The struct is `Copy` so it can ride alongside hot-path signal state without
//! allocation. The writer agent is an *interned* `u16` (not an `AgentId`
//! string) precisely to keep provenance cheap and fixed-width on the WAL wire;
//! `0` means a direct (non-agent) user write.

use super::scope::SignalScope;
use crate::replication::{crdt::hlc::HlcTimestamp, shard::ShardId};

/// Interned writer-agent id. `0` is reserved for direct (non-agent) user writes.
pub const WRITER_DIRECT: u16 = 0;

/// Audit anchor attached to a ranking-affecting signal.
///
/// All fields are fixed-width and `Copy`. The interned [`writer_agent`] maps to
/// an [`AgentId`](crate::session::AgentId) via the agent interner (M10p2); the
/// other fields are stamped at write time from the active scope, share policy,
/// and membership epoch.
///
/// [`writer_agent`]: SignalProvenance::writer_agent
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SignalProvenance {
    /// Interned writer agent id; [`WRITER_DIRECT`] (0) = direct user write.
    pub writer_agent: u16,
    /// The node/shard that originated the write (per-node attribution for purge).
    pub origin_shard: ShardId,
    /// The scope this signal was written in.
    pub scope: SignalScope,
    /// The share-policy version in effect when this signal was written.
    pub share_policy_version: u16,
    /// The membership epoch in effect (0 when not community-scoped).
    pub membership_epoch: u32,
    /// Causal timestamp for deterministic ordering across nodes.
    pub hlc: HlcTimestamp,
}

impl SignalProvenance {
    /// Construct provenance for a direct local user write.
    #[must_use]
    pub const fn local(origin_shard: ShardId, hlc: HlcTimestamp) -> Self {
        Self {
            writer_agent: WRITER_DIRECT,
            origin_shard,
            scope: SignalScope::Local,
            share_policy_version: 0,
            membership_epoch: 0,
            hlc,
        }
    }

    /// Construct provenance with an explicit scope/policy/epoch.
    #[must_use]
    pub const fn new(
        writer_agent: u16,
        origin_shard: ShardId,
        scope: SignalScope,
        share_policy_version: u16,
        membership_epoch: u32,
        hlc: HlcTimestamp,
    ) -> Self {
        Self {
            writer_agent,
            origin_shard,
            scope,
            share_policy_version,
            membership_epoch,
            hlc,
        }
    }

    /// Whether this signal was written by an agent (vs a direct user write).
    #[must_use]
    pub const fn is_agent_write(self) -> bool {
        self.writer_agent != WRITER_DIRECT
    }
}

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

    fn hlc() -> HlcTimestamp {
        HlcTimestamp::zero()
    }

    #[test]
    fn local_provenance_is_direct_and_local() {
        let p = SignalProvenance::local(ShardId(3), hlc());
        assert_eq!(p.writer_agent, WRITER_DIRECT);
        assert!(!p.is_agent_write());
        assert_eq!(p.scope, SignalScope::Local);
        assert_eq!(p.membership_epoch, 0);
        assert_eq!(p.origin_shard, ShardId(3));
    }

    #[test]
    fn agent_write_detected() {
        let p = SignalProvenance::new(7, ShardId(1), SignalScope::Agent, 2, 5, hlc());
        assert!(p.is_agent_write());
        assert_eq!(p.writer_agent, 7);
        assert_eq!(p.share_policy_version, 2);
        assert_eq!(p.membership_epoch, 5);
    }

    #[test]
    fn provenance_is_copy() {
        fn assert_copy<T: Copy>() {}
        assert_copy::<SignalProvenance>();
    }
}
