use indexmap::set::{IntoIter as IndexSetIntoIter, Iter as IndexSetIter};

use super::{
    Action, ConditionalPushRule, FlattenedJson, PatternedPushRule, PushConditionRoomCtx, Ruleset,
    SimplePushRule, condition,
};
use crate::{OwnedRoomId, OwnedUserId, push::action::SoundTweakValue};

/// The kinds of push rules that are available.
#[derive(Clone, Debug)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum AnyPushRule {
    /// Rules that override all other kinds.
    Override(ConditionalPushRule),

    /// Content-specific rules.
    Content(PatternedPushRule),

    /// Post-content specific rules.
    #[cfg(feature = "unstable-msc4306")]
    PostContent(ConditionalPushRule),

    /// Room-specific rules.
    Room(SimplePushRule<OwnedRoomId>),

    /// Sender-specific rules.
    Sender(SimplePushRule<OwnedUserId>),

    /// Lowest priority rules.
    Underride(ConditionalPushRule),
}

impl AnyPushRule {
    /// Convert `AnyPushRule` to `AnyPushRuleRef`.
    pub fn as_ref(&self) -> AnyPushRuleRef<'_> {
        match self {
            Self::Override(o) => AnyPushRuleRef::Override(o),
            Self::Content(c) => AnyPushRuleRef::Content(c),
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(c) => AnyPushRuleRef::PostContent(c),
            Self::Room(r) => AnyPushRuleRef::Room(r),
            Self::Sender(s) => AnyPushRuleRef::Sender(s),
            Self::Underride(u) => AnyPushRuleRef::Underride(u),
        }
    }

    /// Get the `enabled` flag of the push rule.
    pub fn enabled(&self) -> bool {
        self.as_ref().enabled()
    }

    /// Get the `actions` of the push rule.
    pub fn actions(&self) -> &[Action] {
        self.as_ref().actions()
    }

    /// Whether an event that matches the push rule should be highlighted.
    pub fn triggers_highlight(&self) -> bool {
        self.as_ref().triggers_highlight()
    }

    /// Whether an event that matches the push rule should trigger a notification (either in-app or
    /// remote / push).
    pub fn triggers_notification(&self) -> bool {
        self.as_ref().triggers_notification()
    }

    /// Whether an event that matches the push rule should trigger a remote notification.
    #[cfg(feature = "unstable-msc3768")]
    pub fn triggers_remote_notification(&self) -> bool {
        self.as_ref().triggers_remote_notification()
    }

    /// The sound that should be played when an event matches the push rule, if any.
    pub fn triggers_sound(&self) -> Option<&SoundTweakValue> {
        self.as_ref().triggers_sound()
    }

    /// Get the `rule_id` of the push rule.
    pub fn rule_id(&self) -> &str {
        self.as_ref().rule_id()
    }

    /// Whether the push rule is a server-default rule.
    pub fn is_server_default(&self) -> bool {
        self.as_ref().is_server_default()
    }

    /// Check if the push rule applies to the event.
    ///
    /// # Arguments
    ///
    /// * `event` - The flattened JSON representation of a room message event.
    /// * `context` - The context of the room at the time of the event.
    pub async fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
        self.as_ref().applies(event, context).await
    }
}

/// Iterator type for `Ruleset`
#[derive(Debug)]
pub struct RulesetIntoIter {
    content: IndexSetIntoIter<PatternedPushRule>,
    #[cfg(feature = "unstable-msc4306")]
    postcontent: IndexSetIntoIter<ConditionalPushRule>,
    override_: IndexSetIntoIter<ConditionalPushRule>,
    room: IndexSetIntoIter<SimplePushRule<OwnedRoomId>>,
    sender: IndexSetIntoIter<SimplePushRule<OwnedUserId>>,
    underride: IndexSetIntoIter<ConditionalPushRule>,
}

impl Iterator for RulesetIntoIter {
    type Item = AnyPushRule;

    fn next(&mut self) -> Option<Self::Item> {
        let it = self
            .override_
            .next()
            .map(AnyPushRule::Override)
            .or_else(|| self.content.next().map(AnyPushRule::Content));

        #[cfg(feature = "unstable-msc4306")]
        let it = it.or_else(|| self.postcontent.next().map(AnyPushRule::PostContent));

        it.or_else(|| self.room.next().map(AnyPushRule::Room))
            .or_else(|| self.sender.next().map(AnyPushRule::Sender))
            .or_else(|| self.underride.next().map(AnyPushRule::Underride))
    }
}

impl IntoIterator for Ruleset {
    type Item = AnyPushRule;
    type IntoIter = RulesetIntoIter;

    fn into_iter(self) -> Self::IntoIter {
        RulesetIntoIter {
            content: self.content.into_iter(),
            #[cfg(feature = "unstable-msc4306")]
            postcontent: self.postcontent.into_iter(),
            override_: self.override_.into_iter(),
            room: self.room.into_iter(),
            sender: self.sender.into_iter(),
            underride: self.underride.into_iter(),
        }
    }
}

/// Reference to any kind of push rule.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum AnyPushRuleRef<'a> {
    /// Rules that override all other kinds.
    Override(&'a ConditionalPushRule),

    /// Content-specific rules.
    Content(&'a PatternedPushRule),

    /// Post-content specific rules.
    #[cfg(feature = "unstable-msc4306")]
    PostContent(&'a ConditionalPushRule),

    /// Room-specific rules.
    Room(&'a SimplePushRule<OwnedRoomId>),

    /// Sender-specific rules.
    Sender(&'a SimplePushRule<OwnedUserId>),

    /// Lowest priority rules.
    Underride(&'a ConditionalPushRule),
}

impl<'a> AnyPushRuleRef<'a> {
    /// Convert `AnyPushRuleRef` to `AnyPushRule` by cloning the inner value.
    pub fn to_owned(self) -> AnyPushRule {
        match self {
            Self::Override(o) => AnyPushRule::Override(o.clone()),
            Self::Content(c) => AnyPushRule::Content(c.clone()),
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(c) => AnyPushRule::PostContent(c.clone()),
            Self::Room(r) => AnyPushRule::Room(r.clone()),
            Self::Sender(s) => AnyPushRule::Sender(s.clone()),
            Self::Underride(u) => AnyPushRule::Underride(u.clone()),
        }
    }

    /// Get the `enabled` flag of the push rule.
    pub fn enabled(self) -> bool {
        match self {
            Self::Override(rule) => rule.enabled,
            Self::Underride(rule) => rule.enabled,
            Self::Content(rule) => rule.enabled,
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(rule) => rule.enabled,
            Self::Room(rule) => rule.enabled,
            Self::Sender(rule) => rule.enabled,
        }
    }

    /// Get the `actions` of the push rule.
    pub fn actions(self) -> &'a [Action] {
        match self {
            Self::Override(rule) => &rule.actions,
            Self::Underride(rule) => &rule.actions,
            Self::Content(rule) => &rule.actions,
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(rule) => &rule.actions,
            Self::Room(rule) => &rule.actions,
            Self::Sender(rule) => &rule.actions,
        }
    }

    /// Whether an event that matches the push rule should be highlighted.
    pub fn triggers_highlight(self) -> bool {
        self.actions().iter().any(|a| a.is_highlight())
    }

    /// Whether an event that matches the push rule should trigger a notification (either in-app or
    /// remote / push).
    pub fn triggers_notification(self) -> bool {
        self.actions().iter().any(|a| a.should_notify())
    }

    /// Whether an event that matches the push rule should trigger a remote notification.
    #[cfg(feature = "unstable-msc3768")]
    pub fn triggers_remote_notification(self) -> bool {
        self.actions().iter().any(|a| a.should_notify_remote())
    }

    /// The sound that should be played when an event matches the push rule, if any.
    pub fn triggers_sound(self) -> Option<&'a SoundTweakValue> {
        self.actions().iter().find_map(|a| a.sound())
    }

    /// Get the `rule_id` of the push rule.
    pub fn rule_id(self) -> &'a str {
        match self {
            Self::Override(rule) => &rule.rule_id,
            Self::Underride(rule) => &rule.rule_id,
            Self::Content(rule) => &rule.rule_id,
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(rule) => &rule.rule_id,
            Self::Room(rule) => rule.rule_id.as_ref(),
            Self::Sender(rule) => rule.rule_id.as_ref(),
        }
    }

    /// Whether the push rule is a server-default rule.
    pub fn is_server_default(self) -> bool {
        match self {
            Self::Override(rule) => rule.default,
            Self::Underride(rule) => rule.default,
            Self::Content(rule) => rule.default,
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(rule) => rule.default,
            Self::Room(rule) => rule.default,
            Self::Sender(rule) => rule.default,
        }
    }

    /// Check if the push rule applies to the event.
    ///
    /// # Arguments
    ///
    /// * `event` - The flattened JSON representation of a room message event.
    /// * `context` - The context of the room at the time of the event.
    pub async fn applies(self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
            return false;
        }

        match self {
            Self::Override(rule) => rule.applies(event, context).await,
            Self::Underride(rule) => rule.applies(event, context).await,
            Self::Content(rule) => rule.applies_to("content.body", event, context),
            #[cfg(feature = "unstable-msc4306")]
            Self::PostContent(rule) => rule.applies(event, context).await,
            Self::Room(rule) => {
                rule.enabled
                    && condition::check_event_match(
                        event,
                        "room_id",
                        rule.rule_id.as_ref(),
                        context,
                    )
            }
            Self::Sender(rule) => {
                rule.enabled
                    && condition::check_event_match(event, "sender", rule.rule_id.as_ref(), context)
            }
        }
    }
}

/// Iterator type for `Ruleset`
#[derive(Debug)]
pub struct RulesetIter<'a> {
    content: IndexSetIter<'a, PatternedPushRule>,
    #[cfg(feature = "unstable-msc4306")]
    postcontent: IndexSetIter<'a, ConditionalPushRule>,
    override_: IndexSetIter<'a, ConditionalPushRule>,
    room: IndexSetIter<'a, SimplePushRule<OwnedRoomId>>,
    sender: IndexSetIter<'a, SimplePushRule<OwnedUserId>>,
    underride: IndexSetIter<'a, ConditionalPushRule>,
}

impl<'a> Iterator for RulesetIter<'a> {
    type Item = AnyPushRuleRef<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        let it = self
            .override_
            .next()
            .map(AnyPushRuleRef::Override)
            .or_else(|| self.content.next().map(AnyPushRuleRef::Content));

        #[cfg(feature = "unstable-msc4306")]
        let it = it.or_else(|| self.postcontent.next().map(AnyPushRuleRef::PostContent));

        it.or_else(|| self.room.next().map(AnyPushRuleRef::Room))
            .or_else(|| self.sender.next().map(AnyPushRuleRef::Sender))
            .or_else(|| self.underride.next().map(AnyPushRuleRef::Underride))
    }
}

impl<'a> IntoIterator for &'a Ruleset {
    type Item = AnyPushRuleRef<'a>;
    type IntoIter = RulesetIter<'a>;

    fn into_iter(self) -> Self::IntoIter {
        RulesetIter {
            content: self.content.iter(),
            #[cfg(feature = "unstable-msc4306")]
            postcontent: self.postcontent.iter(),
            override_: self.override_.iter(),
            room: self.room.iter(),
            sender: self.sender.iter(),
            underride: self.underride.iter(),
        }
    }
}
