//! ConfigMap specification builder.

use std::collections::HashMap;

/// Specification for creating a Kubernetes ConfigMap.
///
/// ConfigMaps store configuration data as key-value pairs that can be consumed
/// by pods through environment variables, command-line arguments, or configuration files.
///
/// # Examples
///
/// ```no_run
/// use lmrc_kubernetes::ConfigMapSpec;
///
/// let configmap = ConfigMapSpec::new("app-config")
///     .with_data("database.url", "postgres://localhost:5432/mydb")
///     .with_data("log.level", "info")
///     .with_label("app", "my-application");
/// ```
#[derive(Debug, Clone)]
pub struct ConfigMapSpec {
    name: String,
    data: HashMap<String, String>,
    binary_data: HashMap<String, Vec<u8>>,
    labels: HashMap<String, String>,
    immutable: bool,
}

impl ConfigMapSpec {
    /// Creates a new ConfigMapSpec with the given name.
    ///
    /// # Arguments
    ///
    /// * `name` - The name of the ConfigMap
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let configmap = ConfigMapSpec::new("my-config");
    /// ```
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            data: HashMap::new(),
            binary_data: HashMap::new(),
            labels: HashMap::new(),
            immutable: false,
        }
    }

    /// Adds a text data entry to the ConfigMap.
    ///
    /// # Arguments
    ///
    /// * `key` - The key for the data entry
    /// * `value` - The string value
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_data("app.name", "my-application");
    /// ```
    pub fn with_data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.data.insert(key.into(), value.into());
        self
    }

    /// Adds multiple text data entries from a HashMap.
    ///
    /// # Arguments
    ///
    /// * `data` - HashMap of key-value pairs
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    /// use std::collections::HashMap;
    ///
    /// let mut data = HashMap::new();
    /// data.insert("key1".to_string(), "value1".to_string());
    /// data.insert("key2".to_string(), "value2".to_string());
    ///
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_data_map(data);
    /// ```
    pub fn with_data_map(mut self, data: HashMap<String, String>) -> Self {
        self.data.extend(data);
        self
    }

    /// Adds binary data to the ConfigMap.
    ///
    /// # Arguments
    ///
    /// * `key` - The key for the binary data entry
    /// * `value` - The binary data as bytes
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let binary_data = vec![0x89, 0x50, 0x4E, 0x47];
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_binary_data("image.png", binary_data);
    /// ```
    pub fn with_binary_data(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
        self.binary_data.insert(key.into(), value);
        self
    }

    /// Adds a label to the ConfigMap.
    ///
    /// # Arguments
    ///
    /// * `key` - The label key
    /// * `value` - The label value
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_label("environment", "production");
    /// ```
    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.labels.insert(key.into(), value.into());
        self
    }

    /// Makes the ConfigMap immutable.
    ///
    /// Once a ConfigMap is marked as immutable, its data cannot be changed.
    /// This provides protection against accidental updates and improves performance.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_data("version", "1.0.0")
    ///     .immutable(true);
    /// ```
    pub fn immutable(mut self, immutable: bool) -> Self {
        self.immutable = immutable;
        self
    }

    /// Loads data from a file content.
    ///
    /// # Arguments
    ///
    /// * `filename` - The key to use (typically the filename)
    /// * `content` - The file content
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let config_content = "database:\n  host: localhost\n  port: 5432";
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .from_file("database.yaml", config_content);
    /// ```
    pub fn from_file(mut self, filename: impl Into<String>, content: impl Into<String>) -> Self {
        self.data.insert(filename.into(), content.into());
        self
    }

    /// Validates the ConfigMap specification.
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if valid, or an error message if invalid.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmrc_kubernetes::ConfigMapSpec;
    ///
    /// let configmap = ConfigMapSpec::new("my-config")
    ///     .with_data("key", "value");
    ///
    /// assert!(configmap.validate().is_ok());
    /// ```
    pub fn validate(&self) -> Result<(), String> {
        if self.name.is_empty() {
            return Err("ConfigMap name cannot be empty".to_string());
        }

        if self.data.is_empty() && self.binary_data.is_empty() {
            return Err("ConfigMap must have at least one data or binary_data entry".to_string());
        }

        // Validate ConfigMap name follows DNS subdomain rules
        if !self
            .name
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
        {
            return Err(
                "ConfigMap name must contain only alphanumeric characters, '-', or '.'".to_string(),
            );
        }

        if self.name.starts_with('-') || self.name.ends_with('-') {
            return Err("ConfigMap name cannot start or end with '-'".to_string());
        }

        Ok(())
    }

    // Getters

    /// Returns the ConfigMap name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the data entries.
    pub fn data(&self) -> &HashMap<String, String> {
        &self.data
    }

    /// Returns the binary data entries.
    pub fn binary_data(&self) -> &HashMap<String, Vec<u8>> {
        &self.binary_data
    }

    /// Returns the labels.
    pub fn labels(&self) -> &HashMap<String, String> {
        &self.labels
    }

    /// Returns whether the ConfigMap is immutable.
    pub fn is_immutable(&self) -> bool {
        self.immutable
    }
}

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

    #[test]
    fn test_new_configmap() {
        let cm = ConfigMapSpec::new("test-config");
        assert_eq!(cm.name(), "test-config");
        assert!(cm.data().is_empty());
    }

    #[test]
    fn test_with_data() {
        let cm = ConfigMapSpec::new("test-config")
            .with_data("key1", "value1")
            .with_data("key2", "value2");

        assert_eq!(cm.data().len(), 2);
        assert_eq!(cm.data().get("key1").unwrap(), "value1");
    }

    #[test]
    fn test_with_binary_data() {
        let cm = ConfigMapSpec::new("test-config").with_binary_data("file.bin", vec![1, 2, 3, 4]);

        assert_eq!(cm.binary_data().len(), 1);
        assert_eq!(cm.binary_data().get("file.bin").unwrap(), &vec![1, 2, 3, 4]);
    }

    #[test]
    fn test_immutable() {
        let cm = ConfigMapSpec::new("test-config")
            .with_data("key", "value")
            .immutable(true);

        assert!(cm.is_immutable());
    }

    #[test]
    fn test_validation_empty_name() {
        let cm = ConfigMapSpec::new("");
        assert!(cm.validate().is_err());
    }

    #[test]
    fn test_validation_no_data() {
        let cm = ConfigMapSpec::new("test");
        assert!(cm.validate().is_err());
    }

    #[test]
    fn test_validation_valid() {
        let cm = ConfigMapSpec::new("test-config").with_data("key", "value");
        assert!(cm.validate().is_ok());
    }
}
