//! Stream types - configuration change events.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Type of configuration change.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChangeKind {
    /// File was created.
    Created,
    /// File was modified.
    Modified,
    /// File was deleted.
    Deleted,
}

impl ChangeKind {
    /// Returns true if this is a creation event.
    #[must_use]
    pub const fn is_create(&self) -> bool {
        matches!(self, Self::Created)
    }

    /// Returns true if this is a modification event.
    #[must_use]
    pub const fn is_modify(&self) -> bool {
        matches!(self, Self::Modified)
    }

    /// Returns true if this is a deletion event.
    #[must_use]
    pub const fn is_delete(&self) -> bool {
        matches!(self, Self::Deleted)
    }
}

/// Source of the configuration change.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChangeSource {
    /// File system change.
    File {
        /// Path to the changed file.
        path: PathBuf,
    },
    /// Environment variable change.
    Env {
        /// Variable name.
        name: String,
    },
    /// Manual trigger.
    Manual {
        /// Trigger reason.
        reason: String,
    },
    /// Programmatic change.
    Programmatic {
        /// Source identifier.
        source: String,
    },
}

impl ChangeSource {
    /// Creates a file change source.
    #[must_use]
    pub fn file(path: impl Into<PathBuf>) -> Self {
        Self::File { path: path.into() }
    }

    /// Creates an environment variable change source.
    #[must_use]
    pub fn env(name: impl Into<String>) -> Self {
        Self::Env { name: name.into() }
    }

    /// Creates a manual trigger source.
    #[must_use]
    pub fn manual(reason: impl Into<String>) -> Self {
        Self::Manual {
            reason: reason.into(),
        }
    }

    /// Creates a programmatic change source.
    #[must_use]
    pub fn programmatic(source: impl Into<String>) -> Self {
        Self::Programmatic {
            source: source.into(),
        }
    }

    /// Returns the file path if this is a file change.
    #[must_use]
    pub const fn as_file_path(&self) -> Option<&PathBuf> {
        match self {
            Self::File { path } => Some(path),
            _ => None,
        }
    }
}

/// A configuration change event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigChange {
    /// Unique identifier for this change.
    pub id: String,
    /// Kind of change.
    pub kind: ChangeKind,
    /// Source of the change.
    pub source: ChangeSource,
    /// Timestamp when the change occurred.
    pub timestamp: DateTime<Utc>,
    /// Old value (if available).
    pub old_value: Option<String>,
    /// New value (if available).
    pub new_value: Option<String>,
    /// Metadata associated with the change.
    pub metadata: serde_json::Value,
}

impl ConfigChange {
    /// Creates a new config change.
    #[must_use]
    pub fn new(id: impl Into<String>, kind: ChangeKind, source: ChangeSource) -> Self {
        Self {
            id: id.into(),
            kind,
            source,
            timestamp: Utc::now(),
            old_value: None,
            new_value: None,
            metadata: serde_json::Value::Null,
        }
    }

    /// Creates a file change event.
    #[must_use]
    pub fn file_change(id: impl Into<String>, path: impl Into<PathBuf>, kind: ChangeKind) -> Self {
        Self::new(id, kind, ChangeSource::file(path))
    }

    /// Sets the old value.
    #[must_use]
    pub fn with_old_value(mut self, value: impl Into<String>) -> Self {
        self.old_value = Some(value.into());
        self
    }

    /// Sets the new value.
    #[must_use]
    pub fn with_new_value(mut self, value: impl Into<String>) -> Self {
        self.new_value = Some(value.into());
        self
    }

    /// Sets the metadata.
    #[must_use]
    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
        self.metadata = metadata;
        self
    }

    /// Returns true if this is a file change.
    #[must_use]
    pub const fn is_file_change(&self) -> bool {
        matches!(self.source, ChangeSource::File { .. })
    }
}

/// Configuration change stream trait.
pub trait ConfigStream: Send + Sync {
    /// The item type yielded by the stream.
    type Item;

    /// Polls for the next change.
    ///
    /// # Errors
    ///
    /// Returns an error if polling fails.
    fn poll_next(&mut self) -> crate::domain::error::Result<Option<Self::Item>>;

    /// Returns true if the stream is closed.
    fn is_closed(&self) -> bool;
}

/// Async configuration change stream trait.
#[cfg(feature = "async")]
#[async_trait::async_trait]
pub trait AsyncConfigStream: Send + Sync {
    /// The item type yielded by the stream.
    type Item;

    /// Receives the next change asynchronously.
    ///
    /// # Errors
    ///
    /// Returns an error if receiving fails.
    async fn next(&mut self) -> crate::domain::error::Result<Option<Self::Item>>;

    /// Returns true if the stream is closed.
    fn is_closed(&self) -> bool;
}

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

    #[test]
    fn test_change_kind_predicates() {
        assert!(ChangeKind::Created.is_create());
        assert!(!ChangeKind::Created.is_modify());
        assert!(ChangeKind::Modified.is_modify());
        assert!(ChangeKind::Deleted.is_delete());
    }

    #[test]
    fn test_change_source_constructors() {
        let file = ChangeSource::file("/path/to/file");
        assert!(file.as_file_path().is_some());

        let env = ChangeSource::env("HOME");
        assert!(matches!(env, ChangeSource::Env { .. }));

        let manual = ChangeSource::manual("user request");
        assert!(matches!(manual, ChangeSource::Manual { .. }));
    }

    #[test]
    fn test_config_change_builder() {
        let change = ConfigChange::file_change("id-1", "/path/to/file", ChangeKind::Modified)
            .with_old_value("old")
            .with_new_value("new");

        assert_eq!(change.id, "id-1");
        assert_eq!(change.kind, ChangeKind::Modified);
        assert!(change.is_file_change());
        assert_eq!(change.old_value, Some("old".to_string()));
        assert_eq!(change.new_value, Some("new".to_string()));
    }
}
