//! Community governance policy engine (M10p1).
//!
//! A community can govern *which signal intents count* toward its ranking and
//! *how much* any one contribution may weigh, via a versioned [`GovernancePolicy`].
//! Policies are immutable once registered; a change is a new monotonically
//! versioned policy with an `effective_at_ns` timestamp, so a query can report
//! the exact governing version ([`PolicyMetadata`](crate::query::retrieve::types::PolicyMetadata))
//! and the decision is auditable and reproducible.
//!
//! Enforcement is on the community write path: an excluded intent is rejected
//! (the contribution never enters the community aggregate), and an admitted
//! intent's weight is clamped into the policy's [`WeightingBounds`].

use std::collections::{BTreeMap, BTreeSet};

use dashmap::DashMap;
use serde::{Deserialize, Serialize};

use super::scope::CommunityId;
use crate::schema::TidalError;

/// Bounds on how much a single community contribution may weigh.
///
/// Clamps every admitted contribution into `[min, max]`, preventing any one
/// writer from dominating a community's ranking with an outsized weight.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct WeightingBounds {
    /// Minimum effective weight (a contribution below this is raised to it).
    pub min: f64,
    /// Maximum effective weight (a contribution above this is capped to it).
    pub max: f64,
}

impl WeightingBounds {
    /// Unbounded weighting (the default): `[f64::MIN, f64::MAX]` — no clamping.
    #[must_use]
    pub const fn unbounded() -> Self {
        Self {
            min: f64::MIN,
            max: f64::MAX,
        }
    }

    /// Clamp `weight` into `[min, max]`. If the bounds are inverted (`min > max`)
    /// the lower bound wins, keeping the result deterministic.
    #[must_use]
    pub fn clamp(self, weight: f64) -> f64 {
        if self.min > self.max {
            return self.min;
        }
        weight.clamp(self.min, self.max)
    }
}

impl Default for WeightingBounds {
    fn default() -> Self {
        Self::unbounded()
    }
}

/// A versioned community ranking-governance policy.
///
/// Governs signal eligibility (`allowed_intents` / `excluded_intents`),
/// per-contribution weighting (`weighting`), and the minimum contributor trust
/// (`trust_threshold`) for a community.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GovernancePolicy {
    /// The community this policy governs.
    pub community_id: CommunityId,
    /// Monotonic version; a higher version supersedes lower ones once effective.
    pub version: u32,
    /// Wall-clock ns from which this version is in force.
    pub effective_at_ns: u64,
    /// Intents explicitly allowed. When non-empty, an intent must be present
    /// here to count (allow-list mode). When empty, all non-excluded intents
    /// are allowed (deny-list mode).
    pub allowed_intents: BTreeSet<String>,
    /// Intents explicitly excluded. Takes precedence over `allowed_intents`.
    pub excluded_intents: BTreeSet<String>,
    /// Per-contribution weighting bounds.
    pub weighting: WeightingBounds,
    /// Minimum contributor trust required to count (0.0 = no trust gate).
    /// Declared for forward compatibility; enforced once trust scoring exists.
    pub trust_threshold: f64,
}

impl GovernancePolicy {
    /// Whether `signal_name` (the intent) is admitted by this policy.
    ///
    /// `excluded_intents` always wins; otherwise, a non-empty `allowed_intents`
    /// acts as an allow-list, and an empty one admits everything not excluded.
    #[must_use]
    pub fn admits_intent(&self, signal_name: &str) -> bool {
        if self.excluded_intents.contains(signal_name) {
            return false;
        }
        self.allowed_intents.is_empty() || self.allowed_intents.contains(signal_name)
    }

    /// Clamp `weight` into this policy's [`WeightingBounds`].
    #[must_use]
    pub fn clamp_weight(&self, weight: f64) -> f64 {
        self.weighting.clamp(weight)
    }

    /// Durable storage suffix: `community_be8 || version_be4`. Keying by version
    /// makes each version an independent, immutable row.
    #[must_use]
    pub fn storage_suffix(&self) -> Vec<u8> {
        let mut s = Vec::with_capacity(12);
        s.extend_from_slice(&self.community_id.as_u64().to_be_bytes());
        s.extend_from_slice(&self.version.to_be_bytes());
        s
    }

    /// Serialize to durable bytes.
    ///
    /// A serialize failure is propagated as [`TidalError::Internal`] rather than
    /// swallowed into empty bytes: an empty value deserializes back as a
    /// *missing* policy version (a community's governing rules would silently
    /// disappear on restart), so the durability path must surface the failure.
    /// This struct always serializes cleanly in practice.
    ///
    /// # Errors
    ///
    /// Returns [`TidalError::Internal`] if serialization fails.
    pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
        serde_json::to_vec(self)
            .map_err(|e| TidalError::internal("governance_policy_to_bytes", e.to_string()))
    }

    /// Deserialize from durable bytes; malformed bytes return `None`.
    #[must_use]
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        serde_json::from_slice(bytes).ok()
    }
}

/// Error registering a governance policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyRegisterError {
    /// The new version is not strictly greater than the latest registered version.
    NonMonotonicVersion {
        /// The latest version already registered for the community.
        latest: u32,
        /// The (rejected) version supplied.
        supplied: u32,
    },
}

impl std::fmt::Display for PolicyRegisterError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NonMonotonicVersion { latest, supplied } => write!(
                f,
                "governance policy version {supplied} is not greater than latest {latest}"
            ),
        }
    }
}

/// In-memory registry of versioned community governance policies.
///
/// One `BTreeMap<version, policy>` per community (mirrors the ranking
/// `ProfileRegistry` versioning). The *active* policy at time `T` is the highest
/// version whose `effective_at_ns <= T`.
#[derive(Debug, Default)]
pub struct GovernanceRegistry {
    by_community: DashMap<CommunityId, BTreeMap<u32, GovernancePolicy>>,
}

impl GovernanceRegistry {
    /// Construct an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            by_community: DashMap::new(),
        }
    }

    /// Whether any governance policy is registered.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.by_community.is_empty()
    }

    /// Register a policy version. Versions must be strictly monotonic per
    /// community.
    ///
    /// # Errors
    ///
    /// Returns [`PolicyRegisterError::NonMonotonicVersion`] if `policy.version`
    /// is not greater than the community's latest registered version.
    pub fn register(&self, policy: GovernancePolicy) -> Result<(), PolicyRegisterError> {
        let mut versions = self.by_community.entry(policy.community_id).or_default();
        if let Some((&latest, _)) = versions.iter().next_back()
            && policy.version <= latest
        {
            return Err(PolicyRegisterError::NonMonotonicVersion {
                latest,
                supplied: policy.version,
            });
        }
        versions.insert(policy.version, policy);
        drop(versions); // release the DashMap shard guard before returning
        Ok(())
    }

    /// Insert a policy without the monotonic check (used during checkpoint
    /// restore, where rows are already-validated history).
    pub fn insert_restored(&self, policy: GovernancePolicy) {
        self.by_community
            .entry(policy.community_id)
            .or_default()
            .insert(policy.version, policy);
    }

    /// The active policy for `community` at `now_ns`: the highest version whose
    /// `effective_at_ns <= now_ns`. `None` if the community has no policy yet in
    /// force.
    #[must_use]
    pub fn active_at(&self, community: CommunityId, now_ns: u64) -> Option<GovernancePolicy> {
        let versions = self.by_community.get(&community)?;
        versions
            .values()
            .rev()
            .find(|p| p.effective_at_ns <= now_ns)
            .cloned()
    }
}

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

    fn policy(community: u64, version: u32, effective_at_ns: u64) -> GovernancePolicy {
        GovernancePolicy {
            community_id: CommunityId(community),
            version,
            effective_at_ns,
            allowed_intents: BTreeSet::new(),
            excluded_intents: BTreeSet::from(["skip_for_now".to_string()]),
            weighting: WeightingBounds { min: 0.0, max: 2.0 },
            trust_threshold: 0.0,
        }
    }

    #[test]
    fn excluded_intent_is_rejected() {
        let p = policy(1, 1, 0);
        assert!(!p.admits_intent("skip_for_now"));
        assert!(p.admits_intent("like")); // deny-list mode admits non-excluded
    }

    #[test]
    fn allow_list_mode_only_admits_listed() {
        let mut p = policy(1, 1, 0);
        p.allowed_intents = BTreeSet::from(["like".to_string()]);
        assert!(p.admits_intent("like"));
        assert!(!p.admits_intent("view")); // not in allow-list
        // Excluded still wins even over allow-list.
        p.allowed_intents.insert("skip_for_now".to_string());
        assert!(!p.admits_intent("skip_for_now"));
    }

    #[test]
    fn weight_is_clamped_to_bounds() {
        let p = policy(1, 1, 0);
        assert!((p.clamp_weight(5.0) - 2.0).abs() < 1e-9); // capped to max
        assert!((p.clamp_weight(-1.0) - 0.0).abs() < 1e-9); // raised to min
        assert!((p.clamp_weight(1.5) - 1.5).abs() < 1e-9); // within bounds
    }

    #[test]
    fn unbounded_does_not_clamp() {
        let b = WeightingBounds::unbounded();
        assert!((b.clamp(123.0) - 123.0).abs() < 1e-9);
        assert!((b.clamp(-123.0) + 123.0).abs() < 1e-9);
    }

    #[test]
    fn registry_enforces_monotonic_versions() {
        let reg = GovernanceRegistry::new();
        assert!(reg.register(policy(1, 1, 0)).is_ok());
        assert!(reg.register(policy(1, 2, 100)).is_ok());
        // Re-registering v2 or lower fails.
        assert!(reg.register(policy(1, 2, 200)).is_err());
        assert!(reg.register(policy(1, 1, 200)).is_err());
        // Different community is independent.
        assert!(reg.register(policy(2, 1, 0)).is_ok());
    }

    #[test]
    fn active_at_picks_highest_effective_version() {
        let reg = GovernanceRegistry::new();
        reg.register(policy(1, 1, 100)).unwrap();
        reg.register(policy(1, 2, 200)).unwrap();
        // Before any effective time -> None.
        assert!(reg.active_at(CommunityId(1), 50).is_none());
        // Between v1 and v2 effective times -> v1.
        assert_eq!(reg.active_at(CommunityId(1), 150).unwrap().version, 1);
        // After v2 effective time -> v2.
        assert_eq!(reg.active_at(CommunityId(1), 250).unwrap().version, 2);
        // Unknown community -> None.
        assert!(reg.active_at(CommunityId(99), 250).is_none());
    }

    #[test]
    fn policy_serde_roundtrip() {
        let p = policy(7, 3, 12345);
        let bytes = p.to_bytes().unwrap();
        assert_eq!(GovernancePolicy::from_bytes(&bytes), Some(p.clone()));
        assert_eq!(p.storage_suffix().len(), 12);
    }
}
