use chrono::{DateTime, Utc};
use nostr_rs_types::types::{filter::TagValue, Addr, Event, Filter, Kind, Tag};
use std::{
    collections::{HashMap, HashSet},
    ops::Deref,
};

#[derive(Debug)]
pub struct EventFilter {
    ids: HashSet<[u8; 32]>,
    authors: HashSet<[u8; 32]>,
    tags: HashMap<String, HashSet<TagValue>>,
    kinds: HashSet<Kind>,
    since: Option<DateTime<Utc>>,
    until: Option<DateTime<Utc>>,
}

impl From<Filter> for EventFilter {
    fn from(query: Filter) -> Self {
        let authors = query
            .authors
            .into_iter()
            .map(|id| *id)
            .collect::<HashSet<_>>();

        let tags = query
            .tags
            .into_iter()
            .map(|(tag, values)| (tag, values))
            .collect::<HashMap<String, HashSet<TagValue>>>();

        let kinds = query.kinds.into_iter().collect::<HashSet<Kind>>();

        let ids = query
            .ids
            .into_iter()
            .map(|id| (*id))
            .collect::<HashSet<_>>();

        EventFilter {
            ids,
            authors,
            kinds,
            tags,
            since: query.since,
            until: query.until,
        }
    }
}

impl EventFilter {
    /// Returns true if the filter is empty, meaning that it will match any event.
    pub fn is_empty(&self) -> bool {
        self.ids.is_empty()
            && self.authors.is_empty()
            && self.tags.is_empty()
            && self.kinds.is_empty()
            && self.since.is_none()
            && self.until.is_none()
    }

    /// Returns a new filter that matches any event.
    pub fn into_option(self) -> Option<Self> {
        if self.is_empty() {
            None
        } else {
            Some(self)
        }
    }

    /// Receives a list of event-IDs, and returns a list of events that match
    /// the given filter.
    ///
    /// The list of event-IDs should come from an index from any namespace, and
    /// the filter will load the events from the `Events` namespace and will
    /// filter them by the given parameters.
    #[inline]
    pub fn check_event(&self, event: &Event) -> bool {
        if self.is_empty() {
            return true;
        }

        if !self.ids.is_empty() && !self.ids.contains(&event.id.0) {
            return false;
        }

        if !self.tags.is_empty() {
            let event_tags = event
                .tags()
                .iter()
                .filter_map(|tag| {
                    tag.get_indexable_value()
                        .map(|f| (tag.get_identifier().to_owned(), f))
                })
                .fold(HashMap::new(), |mut acc, (key, value)| {
                    acc.entry(key).or_insert_with(HashSet::new).insert(value);
                    acc
                });

            for (tag, possible_values) in self.tags.iter() {
                let tag_values = if let Some(tag_values) = event_tags.get(tag) {
                    tag_values
                } else {
                    // event has no tag with the given name
                    return false;
                };

                let mut found_tag = false;
                for tag_value in tag_values {
                    if possible_values.contains(tag_value) {
                        found_tag = true;
                        break;
                    }
                }

                if !found_tag {
                    return false;
                }
            }
        }

        if !self.authors.is_empty() && !self.authors.contains(event.author().deref()) {
            return false;
        }

        if !self.kinds.is_empty() && !self.kinds.contains(&event.kind()) {
            return false;
        }

        if let Some(since) = self.since {
            if event.created_at() < since {
                return false;
            }
        }
        if let Some(until) = self.until {
            if event.created_at() > until {
                return false;
            }
        }

        true
    }
}
