//! Share contract: which signal intents a user opts into sharing with a community.
//!
//! A [`SharePolicy`] is the explicit opt-in that turns a shippable signal into a
//! community contribution. The default ([`SharePolicy::local_only`]) shares
//! nothing — sharing is never implicit. [`SignalScope::Local`] events are never
//! eligible regardless of policy.
//!
//! The intent vocabulary ([`ShareIntent`]) mirrors the engagement-feedback
//! signals in `ai-lookup/services/signals.md`. Governance policies (M10p1) key
//! on the signal-type *name* string; [`ShareIntent::as_str`] /
//! [`ShareIntent::from_signal_name`] bridge the two keyspaces.

use std::collections::BTreeSet;

use super::scope::SignalScope;

/// A signal intent that may be selectively allowed to ship to a community.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum ShareIntent {
    /// "Not right now" — soft skip.
    SkipForNow,
    /// "Not for me" — soft negative.
    NotForMe,
    /// Explicit low-quality flag.
    LowQuality,
    /// Hide this item.
    Hide,
    /// Mute the source.
    Mute,
    /// Block the source.
    Block,
    /// Save / bookmark.
    Save,
    /// Passive view.
    View,
    /// Positive like.
    Like,
}

impl ShareIntent {
    /// The canonical signal-type name for this intent.
    ///
    /// This is the bridge to the governance keyspace (M10p1), which keys on
    /// signal-type name strings.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::SkipForNow => "skip_for_now",
            Self::NotForMe => "not_for_me",
            Self::LowQuality => "low_quality",
            Self::Hide => "hide",
            Self::Mute => "mute",
            Self::Block => "block",
            Self::Save => "save",
            Self::View => "view",
            Self::Like => "like",
        }
    }

    /// Map a signal-type name to its [`ShareIntent`], if one exists.
    ///
    /// Names with no fixed intent (custom app signals) return `None` and are
    /// treated as non-shareable by [`SharePolicy::is_share_eligible`].
    #[must_use]
    pub fn from_signal_name(name: &str) -> Option<Self> {
        Some(match name {
            "skip_for_now" => Self::SkipForNow,
            "not_for_me" => Self::NotForMe,
            "low_quality" => Self::LowQuality,
            "hide" => Self::Hide,
            "mute" => Self::Mute,
            "block" => Self::Block,
            "save" => Self::Save,
            "view" => Self::View,
            "like" => Self::Like,
            _ => return None,
        })
    }
}

/// Whether a share policy keeps everything local or contributes to a community.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum ShareMode {
    /// Nothing leaves the node.
    #[default]
    LocalOnly,
    /// Allowed intents contribute to joined communities.
    CommunityShare,
}

/// Versioned per-user share contract: which intents may ship, in what mode.
///
/// Canonical definition referenced by [membership](super::membership) (M9p2) and
/// the governance policy engine (M10p1). `version` lets a result expose the
/// `share_policy_version` that governed a contribution.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SharePolicy {
    /// Monotonic version of this share contract; stamped into provenance.
    pub version: u16,
    /// Intents the user has opted into sharing.
    pub allowed_intents: BTreeSet<ShareIntent>,
    /// Local-only vs community-share.
    pub mode: ShareMode,
}

impl SharePolicy {
    /// A policy that shares nothing (the default).
    #[must_use]
    pub const fn local_only() -> Self {
        Self {
            version: 0,
            allowed_intents: BTreeSet::new(),
            mode: ShareMode::LocalOnly,
        }
    }

    /// Build a community-share policy at `version` that admits `intents`.
    #[must_use]
    pub fn community_share(version: u16, intents: impl IntoIterator<Item = ShareIntent>) -> Self {
        Self {
            version,
            allowed_intents: intents.into_iter().collect(),
            mode: ShareMode::CommunityShare,
        }
    }

    /// Whether a `(scope, intent)` pair is eligible to ship.
    ///
    /// [`SignalScope::Local`] is **never** eligible regardless of policy.
    /// Otherwise eligibility requires [`ShareMode::CommunityShare`] **and**
    /// `intent` in [`allowed_intents`](Self::allowed_intents).
    #[must_use]
    pub fn is_share_eligible(&self, scope: SignalScope, intent: ShareIntent) -> bool {
        if !scope.is_shippable() {
            return false;
        }
        matches!(self.mode, ShareMode::CommunityShare) && self.allowed_intents.contains(&intent)
    }

    /// Whether a `(scope, signal_name)` pair is eligible to ship.
    ///
    /// Convenience wrapper that resolves the signal name to a [`ShareIntent`];
    /// names with no fixed intent are never eligible.
    #[must_use]
    pub fn is_signal_share_eligible(&self, scope: SignalScope, signal_name: &str) -> bool {
        ShareIntent::from_signal_name(signal_name)
            .is_some_and(|intent| self.is_share_eligible(scope, intent))
    }
}

impl Default for SharePolicy {
    fn default() -> Self {
        Self::local_only()
    }
}

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

    #[test]
    fn default_shares_nothing() {
        let p = SharePolicy::default();
        assert_eq!(p.mode, ShareMode::LocalOnly);
        assert!(p.allowed_intents.is_empty());
        assert!(!p.is_share_eligible(SignalScope::Community(CommunityId(1)), ShareIntent::Like));
    }

    #[test]
    fn local_scope_never_eligible_even_when_allowed() {
        let p = SharePolicy::community_share(1, [ShareIntent::Like, ShareIntent::View]);
        // Like is allowed, but Local scope is never shippable.
        assert!(!p.is_share_eligible(SignalScope::Local, ShareIntent::Like));
    }

    #[test]
    fn community_share_admits_only_allowed_intents() {
        let p = SharePolicy::community_share(3, [ShareIntent::LowQuality, ShareIntent::NotForMe]);
        let scope = SignalScope::Community(CommunityId(9));
        assert!(p.is_share_eligible(scope, ShareIntent::LowQuality));
        assert!(p.is_share_eligible(scope, ShareIntent::NotForMe));
        assert!(!p.is_share_eligible(scope, ShareIntent::View));
        assert_eq!(p.version, 3);
    }

    #[test]
    fn local_only_mode_blocks_all() {
        let mut p = SharePolicy::local_only();
        p.allowed_intents.insert(ShareIntent::Like);
        // Mode is still LocalOnly, so even an "allowed" intent does not ship.
        assert!(!p.is_share_eligible(SignalScope::Community(CommunityId(1)), ShareIntent::Like));
    }

    #[test]
    fn signal_name_bridge() {
        let p = SharePolicy::community_share(1, [ShareIntent::LowQuality]);
        let scope = SignalScope::Community(CommunityId(1));
        assert!(p.is_signal_share_eligible(scope, "low_quality"));
        assert!(!p.is_signal_share_eligible(scope, "view"));
        // Unknown custom signal -> no intent -> never eligible.
        assert!(!p.is_signal_share_eligible(scope, "custom_app_signal"));
    }

    #[test]
    fn intent_name_roundtrip() {
        for intent in [
            ShareIntent::SkipForNow,
            ShareIntent::NotForMe,
            ShareIntent::LowQuality,
            ShareIntent::Hide,
            ShareIntent::Mute,
            ShareIntent::Block,
            ShareIntent::Save,
            ShareIntent::View,
            ShareIntent::Like,
        ] {
            assert_eq!(ShareIntent::from_signal_name(intent.as_str()), Some(intent));
        }
        assert_eq!(ShareIntent::from_signal_name("nope"), None);
    }
}
