//! Query traits for filtering configuration changes.

use crate::domain::error::Result;
use crate::domain::stream::change::{ChangeKind, ChangeSource, ConfigChange};

/// Query matcher for filtering changes.
pub trait QueryMatcher: Send + Sync {
    /// Returns true if the change matches the query.
    fn matches(&self, change: &ConfigChange) -> bool;
}

/// Query for filtering configuration changes.
///
/// Use queries to filter which changes are delivered to handlers.
///
/// # Example
///
/// ```rust
/// use cfgmatic_reactive::prelude::{ChangeKind, ConfigChange, FieldQuery, QueryMatcher, Result};
///
/// struct PathQuery {
///     prefix: String,
/// }
///
/// impl QueryMatcher for PathQuery {
///     fn matches(&self, change: &ConfigChange) -> bool {
///         change.source.as_file_path()
///             .map(|p| p.starts_with(&self.prefix))
///             .unwrap_or(false)
///     }
/// }
///
/// impl FieldQuery for PathQuery {
///     fn execute(&self, change: &ConfigChange) -> Result<bool> {
///         Ok(self.matches(change))
///     }
/// }
///
/// let change = ConfigChange::file_change("1", "/etc/myapp/config.toml", ChangeKind::Modified);
/// let query = PathQuery { prefix: "/etc".into() };
/// assert!(query.matches(&change));
/// ```
pub trait FieldQuery: Send + Sync {
    /// Executes the query against a change.
    ///
    /// # Errors
    ///
    /// Returns an error if query execution fails.
    fn execute(&self, change: &ConfigChange) -> Result<bool>;

    /// Returns the query name for debugging.
    fn name(&self) -> &str {
        std::any::type_name::<Self>()
    }
}

/// Query for matching change kinds.
#[derive(Debug, Clone)]
pub struct KindQuery {
    /// Allowed change kinds.
    pub kinds: Vec<ChangeKind>,
}

impl KindQuery {
    /// Creates a new kind query.
    #[must_use]
    pub const fn new(kinds: Vec<ChangeKind>) -> Self {
        Self { kinds }
    }

    /// Creates a query that matches any change kind.
    #[must_use]
    pub fn any() -> Self {
        Self {
            kinds: vec![
                ChangeKind::Created,
                ChangeKind::Modified,
                ChangeKind::Deleted,
            ],
        }
    }

    /// Creates a query that matches only modifications.
    #[must_use]
    pub fn modifications_only() -> Self {
        Self {
            kinds: vec![ChangeKind::Modified],
        }
    }

    /// Creates a query that matches only creations.
    #[must_use]
    pub fn creations_only() -> Self {
        Self {
            kinds: vec![ChangeKind::Created],
        }
    }

    /// Creates a query that matches only deletions.
    #[must_use]
    pub fn deletions_only() -> Self {
        Self {
            kinds: vec![ChangeKind::Deleted],
        }
    }
}

impl QueryMatcher for KindQuery {
    fn matches(&self, change: &ConfigChange) -> bool {
        self.kinds.contains(&change.kind)
    }
}

impl FieldQuery for KindQuery {
    fn execute(&self, change: &ConfigChange) -> Result<bool> {
        Ok(self.matches(change))
    }
}

/// Query for matching file paths using glob patterns.
#[derive(Debug, Clone)]
pub struct PathQuery {
    /// Glob pattern for matching paths.
    pub pattern: String,
}

impl PathQuery {
    /// Creates a new path query.
    #[must_use]
    pub fn new(pattern: impl Into<String>) -> Self {
        Self {
            pattern: pattern.into(),
        }
    }

    /// Creates a query for JSON files.
    #[must_use]
    pub fn json_files() -> Self {
        Self::new("**/*.json")
    }

    /// Creates a query for YAML files.
    #[must_use]
    pub fn yaml_files() -> Self {
        Self::new("**/*.{yaml,yml}")
    }

    /// Creates a query for TOML files.
    #[must_use]
    pub fn toml_files() -> Self {
        Self::new("**/*.toml")
    }

    /// Creates a query for a specific directory.
    #[must_use]
    pub fn directory(dir: impl Into<String>) -> Self {
        let dir = dir.into();
        Self::new(format!("{}/**/*", dir.trim_end_matches('/')))
    }
}

impl QueryMatcher for PathQuery {
    fn matches(&self, change: &ConfigChange) -> bool {
        match &change.source {
            ChangeSource::File { path } => path
                .to_str()
                .is_some_and(|p| glob_match::glob_match(&self.pattern, p)),
            _ => false,
        }
    }
}

impl FieldQuery for PathQuery {
    fn execute(&self, change: &ConfigChange) -> Result<bool> {
        Ok(self.matches(change))
    }
}

/// Composite query that combines multiple queries with AND logic.
#[derive(Default)]
pub struct AndQuery {
    /// Sub-queries.
    pub queries: Vec<Box<dyn QueryMatcher>>,
}

impl std::fmt::Debug for AndQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AndQuery")
            .field("queries_count", &self.queries.len())
            .finish()
    }
}

impl AndQuery {
    /// Creates a new AND query.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds a query to the composition.
    #[must_use]
    pub fn with<Q: QueryMatcher + 'static>(mut self, query: Q) -> Self {
        self.queries.push(Box::new(query));
        self
    }

    /// Adds a boxed query to the composition.
    #[must_use]
    pub fn with_boxed(mut self, query: Box<dyn QueryMatcher>) -> Self {
        self.queries.push(query);
        self
    }
}

impl QueryMatcher for AndQuery {
    fn matches(&self, change: &ConfigChange) -> bool {
        self.queries.iter().all(|q| q.matches(change))
    }
}

/// Composite query that combines multiple queries with OR logic.
#[derive(Default)]
pub struct OrQuery {
    /// Sub-queries.
    pub queries: Vec<Box<dyn QueryMatcher>>,
}

impl std::fmt::Debug for OrQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("OrQuery")
            .field("queries_count", &self.queries.len())
            .finish()
    }
}

impl OrQuery {
    /// Creates a new OR query.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds a query to the composition.
    #[must_use]
    pub fn with<Q: QueryMatcher + 'static>(mut self, query: Q) -> Self {
        self.queries.push(Box::new(query));
        self
    }

    /// Adds a boxed query to the composition.
    #[must_use]
    pub fn with_boxed(mut self, query: Box<dyn QueryMatcher>) -> Self {
        self.queries.push(query);
        self
    }
}

impl QueryMatcher for OrQuery {
    fn matches(&self, change: &ConfigChange) -> bool {
        self.queries.iter().any(|q| q.matches(change))
    }
}

/// Query that inverts another query.
pub struct NotQuery {
    /// The query to invert.
    pub inner: Box<dyn QueryMatcher>,
}

impl std::fmt::Debug for NotQuery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NotQuery").finish()
    }
}

impl NotQuery {
    /// Creates a new NOT query.
    #[must_use]
    pub fn new<Q: QueryMatcher + 'static>(query: Q) -> Self {
        Self {
            inner: Box::new(query),
        }
    }
}

impl QueryMatcher for NotQuery {
    fn matches(&self, change: &ConfigChange) -> bool {
        !self.inner.matches(change)
    }
}

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

    #[test]
    fn test_kind_query_any_matches_all_change_kinds() {
        let query = KindQuery::any();

        assert!(query.matches(&ConfigChange::file_change(
            "created",
            "/path/config.toml",
            ChangeKind::Created,
        )));
        assert!(query.matches(&ConfigChange::file_change(
            "modified",
            "/path/config.toml",
            ChangeKind::Modified,
        )));
        assert!(query.matches(&ConfigChange::file_change(
            "deleted",
            "/path/config.toml",
            ChangeKind::Deleted,
        )));
    }

    #[test]
    fn test_kind_query_modifications() {
        let query = KindQuery::modifications_only();

        let modify = ConfigChange::file_change("1", "/path", ChangeKind::Modified);
        let create = ConfigChange::file_change("2", "/path", ChangeKind::Created);

        assert!(query.matches(&modify));
        assert!(!query.matches(&create));
    }

    #[test]
    fn test_kind_query_creations() {
        let query = KindQuery::creations_only();

        let create = ConfigChange::file_change("1", "/path", ChangeKind::Created);
        let modify = ConfigChange::file_change("2", "/path", ChangeKind::Modified);

        assert!(query.matches(&create));
        assert!(!query.matches(&modify));
    }

    #[test]
    fn test_kind_query_deletions_and_execute() {
        let query = KindQuery::deletions_only();
        let delete = ConfigChange::file_change("1", "/path", ChangeKind::Deleted);
        let create = ConfigChange::file_change("2", "/path", ChangeKind::Created);

        assert_eq!(query.name(), std::any::type_name::<KindQuery>());
        assert!(query.execute(&delete).unwrap());
        assert!(!query.execute(&create).unwrap());
    }

    #[test]
    fn test_path_query_matches_known_file_patterns() {
        let json = ConfigChange::file_change("1", "/etc/myapp/config.json", ChangeKind::Modified);
        let toml = ConfigChange::file_change("2", "/etc/myapp/config.toml", ChangeKind::Modified);
        let yaml = ConfigChange::file_change("3", "/etc/myapp/config.yaml", ChangeKind::Modified);

        assert!(PathQuery::json_files().matches(&json));
        assert!(PathQuery::toml_files().matches(&toml));
        assert!(PathQuery::yaml_files().matches(&yaml));
    }

    #[test]
    fn test_path_query_directory_trims_trailing_slash() {
        let query = PathQuery::directory("/etc/myapp/");
        let inside = ConfigChange::file_change("1", "/etc/myapp/config.toml", ChangeKind::Modified);
        let outside = ConfigChange::file_change("2", "/var/tmp/config.toml", ChangeKind::Modified);

        assert_eq!(query.pattern, "/etc/myapp/**/*");
        assert!(query.matches(&inside));
        assert!(!query.matches(&outside));
    }

    #[test]
    fn test_path_query_does_not_match_non_file_sources() {
        let query = PathQuery::json_files();
        let env_change =
            ConfigChange::new("env", ChangeKind::Modified, ChangeSource::env("APP_PORT"));

        assert!(!query.matches(&env_change));
        assert!(!query.execute(&env_change).unwrap());
    }

    #[test]
    fn test_and_query() {
        let query = AndQuery::new().with(KindQuery::modifications_only());

        let modify = ConfigChange::file_change("1", "/path", ChangeKind::Modified);
        let create = ConfigChange::file_change("2", "/path", ChangeKind::Created);

        assert!(query.matches(&modify));
        assert!(!query.matches(&create));
    }

    #[test]
    fn test_and_query_with_boxed_and_empty_behavior() {
        let empty = AndQuery::new();
        let query = AndQuery::new()
            .with_boxed(Box::new(KindQuery::modifications_only()))
            .with(PathQuery::toml_files());

        let matching =
            ConfigChange::file_change("1", "/etc/myapp/config.toml", ChangeKind::Modified);
        let non_matching =
            ConfigChange::file_change("2", "/etc/myapp/config.json", ChangeKind::Modified);

        assert!(empty.matches(&matching));
        assert!(query.matches(&matching));
        assert!(!query.matches(&non_matching));
        assert!(format!("{empty:?}").contains("queries_count"));
    }

    #[test]
    fn test_or_query() {
        let query = OrQuery::new()
            .with(KindQuery::creations_only())
            .with(KindQuery::deletions_only());

        let create = ConfigChange::file_change("1", "/path", ChangeKind::Created);
        let modify = ConfigChange::file_change("2", "/path", ChangeKind::Modified);
        let delete = ConfigChange::file_change("3", "/path", ChangeKind::Deleted);

        assert!(query.matches(&create));
        assert!(!query.matches(&modify));
        assert!(query.matches(&delete));
    }

    #[test]
    fn test_or_query_with_boxed_and_empty_behavior() {
        let empty = OrQuery::new();
        let query = OrQuery::new()
            .with_boxed(Box::new(KindQuery::deletions_only()))
            .with(PathQuery::toml_files());

        let toml_change =
            ConfigChange::file_change("1", "/etc/myapp/config.toml", ChangeKind::Modified);
        let env_change = ConfigChange::new("2", ChangeKind::Modified, ChangeSource::env("HOME"));

        assert!(!empty.matches(&toml_change));
        assert!(query.matches(&toml_change));
        assert!(!query.matches(&env_change));
        assert!(format!("{query:?}").contains("queries_count"));
    }

    #[test]
    fn test_not_query() {
        let query = NotQuery::new(KindQuery::deletions_only());

        let delete = ConfigChange::file_change("1", "/path", ChangeKind::Deleted);
        let modify = ConfigChange::file_change("2", "/path", ChangeKind::Modified);

        assert!(!query.matches(&delete));
        assert!(query.matches(&modify));
    }

    #[test]
    fn test_not_query_debug() {
        let query = NotQuery::new(PathQuery::json_files());
        let change = ConfigChange::file_change("1", "/tmp/config.toml", ChangeKind::Modified);

        assert!(query.matches(&change));
        assert_eq!(format!("{query:?}"), "NotQuery");
    }
}
