//! Per-archive include/exclude filter. Consulted during non-primary write
//! fanout (Task 20). Primary always writes regardless of filter.

use std::collections::HashSet;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::types::SessionMetadata;

/// Declarative filter applied to non-primary fanout writes.
///
/// A registration's filter decides whether a given session should be
/// replicated to that archive. The primary write target ignores the filter
/// and always writes. A default filter (`ArchiveFilter::default()`) is
/// unrestricted and allows every session.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ArchiveFilter {
    /// If `Some`, only sessions whose `connector_uid` is in the set are
    /// accepted. If `None`, any connector is allowed (subject to other rules).
    #[serde(default)]
    pub include_connectors: Option<HashSet<Uuid>>,
    /// Connector UIDs that are explicitly rejected. Takes precedence over
    /// `include_connectors`.
    #[serde(default)]
    pub exclude_connectors: HashSet<Uuid>,
    /// If non-empty, the session must carry at least one of these tags.
    #[serde(default)]
    pub include_tags: HashSet<String>,
    /// Tags that cause the session to be rejected.
    #[serde(default)]
    pub exclude_tags: HashSet<String>,
    /// When `false`, sessions whose `metadata.hidden == true` are rejected.
    #[serde(default = "default_include_hidden")]
    pub include_hidden: bool,
}

fn default_include_hidden() -> bool {
    true
}

impl Default for ArchiveFilter {
    fn default() -> Self {
        Self {
            include_connectors: None,
            exclude_connectors: HashSet::new(),
            include_tags: HashSet::new(),
            exclude_tags: HashSet::new(),
            include_hidden: true,
        }
    }
}

impl ArchiveFilter {
    /// Returns true when this session should be written to the archive.
    pub fn allows(&self, session: &SessionMetadata, connector_uid: &Uuid) -> bool {
        // Exclude rules win.
        if self.exclude_connectors.contains(connector_uid) {
            return false;
        }
        if let Some(inc) = &self.include_connectors {
            if !inc.contains(connector_uid) {
                return false;
            }
        }
        if session.tags.iter().any(|t| self.exclude_tags.contains(t)) {
            return false;
        }
        if !self.include_tags.is_empty()
            && !session.tags.iter().any(|t| self.include_tags.contains(t))
        {
            return false;
        }
        if !self.include_hidden
            && session.metadata.get("hidden") == Some(&serde_json::Value::Bool(true))
        {
            return false;
        }
        true
    }

    /// A filter that allows everything is equivalent to no filter.
    pub fn is_unrestricted(&self) -> bool {
        self.include_connectors.is_none()
            && self.exclude_connectors.is_empty()
            && self.include_tags.is_empty()
            && self.exclude_tags.is_empty()
            && self.include_hidden
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::SessionMetadata;

    fn make_session(tags: Vec<String>, hidden: bool) -> SessionMetadata {
        let mut s = SessionMetadata::stub(Uuid::now_v7());
        s.tags = tags;
        s.metadata = if hidden {
            serde_json::json!({ "hidden": true })
        } else {
            serde_json::Value::Null
        };
        s
    }

    #[test]
    fn default_allows_all() {
        let f = ArchiveFilter::default();
        let s = make_session(vec![], false);
        let uid = Uuid::new_v4();
        assert!(f.allows(&s, &uid));
        assert!(f.is_unrestricted());
    }

    #[test]
    fn exclude_connector_rejects() {
        let excluded = Uuid::new_v4();
        let mut f = ArchiveFilter::default();
        f.exclude_connectors.insert(excluded);
        let s = make_session(vec![], false);
        assert!(!f.allows(&s, &excluded));
        assert!(f.allows(&s, &Uuid::new_v4()));
        assert!(!f.is_unrestricted());
    }

    #[test]
    fn include_connector_only_allows_listed() {
        let allowed = Uuid::new_v4();
        let mut f = ArchiveFilter::default();
        f.include_connectors = Some(HashSet::from_iter([allowed]));
        let s = make_session(vec![], false);
        assert!(f.allows(&s, &allowed));
        assert!(!f.allows(&s, &Uuid::new_v4()));
    }

    #[test]
    fn tag_intersection_semantics() {
        let mut f = ArchiveFilter::default();
        f.include_tags = HashSet::from_iter(["prod".into()]);
        let s_prod = make_session(vec!["prod".into()], false);
        let s_dev = make_session(vec!["dev".into()], false);
        let uid = Uuid::new_v4();
        assert!(f.allows(&s_prod, &uid));
        assert!(!f.allows(&s_dev, &uid));
    }

    #[test]
    fn exclude_tag_wins_over_include() {
        let mut f = ArchiveFilter::default();
        f.include_tags = HashSet::from_iter(["prod".into()]);
        f.exclude_tags = HashSet::from_iter(["sensitive".into()]);
        let s = make_session(vec!["prod".into(), "sensitive".into()], false);
        let uid = Uuid::new_v4();
        assert!(!f.allows(&s, &uid));
    }

    #[test]
    fn include_hidden_false_rejects_hidden_sessions() {
        let mut f = ArchiveFilter::default();
        f.include_hidden = false;
        let s_hidden = make_session(vec![], true);
        let s_visible = make_session(vec![], false);
        let uid = Uuid::new_v4();
        assert!(!f.allows(&s_hidden, &uid));
        assert!(f.allows(&s_visible, &uid));
    }

    #[test]
    fn default_include_hidden_accepts_hidden_sessions() {
        let f = ArchiveFilter::default();
        let s = make_session(vec![], true);
        let uid = Uuid::new_v4();
        assert!(f.allows(&s, &uid));
    }

    #[test]
    fn toml_roundtrip_default() {
        // Serializing via TOML is exercised through ArchiveConfig, but a
        // plain JSON roundtrip on the struct catches serde attribute typos.
        let f = ArchiveFilter::default();
        let json = serde_json::to_string(&f).unwrap();
        let back: ArchiveFilter = serde_json::from_str(&json).unwrap();
        assert_eq!(f, back);
    }
}
