//! Configuration validation library for LMRC Stack projects
//!
//! This library provides types and validation for LMRC Stack project configurations.
//! It defines the schema for project configuration files and validates them.

use serde::{Deserialize, Serialize};
use thiserror::Error;
use validator::Validate;

pub mod providers;

/// Main configuration structure for LMRC Stack projects
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct LmrcConfig {
    /// Project metadata
    #[validate(nested)]
    pub project: ProjectConfig,

    /// Infrastructure provider selection
    #[validate(nested)]
    pub providers: ProviderConfig,

    /// Application configuration
    #[validate(nested)]
    pub apps: AppsConfig,

    /// Infrastructure-specific configurations
    #[validate(nested)]
    pub infrastructure: InfrastructureConfig,
}

/// Project metadata
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct ProjectConfig {
    /// Project name (alphanumeric with hyphens/underscores)
    #[validate(length(min = 1, max = 100))]
    pub name: String,

    /// Project description
    #[validate(length(min = 1, max = 500))]
    pub description: String,
}

/// Provider selection configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct ProviderConfig {
    /// Server provider (currently only "hetzner")
    pub server: String,

    /// Kubernetes provider (currently "k3s" or "kubernetes")
    pub kubernetes: String,

    /// Database provider (currently only "postgres")
    pub database: String,

    /// DNS provider (currently only "cloudflare")
    pub dns: String,

    /// Git provider (currently only "gitlab")
    pub git: String,
}

/// Applications configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct AppsConfig {
    /// List of application names in the workspace
    #[validate(length(min = 1))]
    pub applications: Vec<ApplicationEntry>,
}

/// Single application entry
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct ApplicationEntry {
    /// Application name
    #[validate(length(min = 1, max = 50))]
    pub name: String,

    /// Application type (e.g., "api", "web", "worker")
    #[validate(length(min = 1, max = 20))]
    pub app_type: String,

    /// Docker configuration for this application
    #[serde(default)]
    #[validate(nested)]
    pub docker: Option<DockerConfig>,

    /// Kubernetes deployment configuration
    #[serde(default)]
    #[validate(nested)]
    pub deployment: Option<DeploymentConfig>,
}

/// Docker build configuration for an application
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct DockerConfig {
    /// Dockerfile path relative to app directory
    #[serde(default = "default_dockerfile")]
    pub dockerfile: String,

    /// Build context path relative to workspace root
    #[serde(default = "default_context")]
    pub context: String,

    /// Image tags (e.g., ["latest", "v1.0.0"])
    #[serde(default = "default_tags")]
    pub tags: Vec<String>,
}

fn default_dockerfile() -> String {
    "Dockerfile".to_string()
}

fn default_context() -> String {
    ".".to_string()
}

fn default_tags() -> Vec<String> {
    vec!["latest".to_string()]
}

impl Default for DockerConfig {
    fn default() -> Self {
        Self {
            dockerfile: default_dockerfile(),
            context: default_context(),
            tags: default_tags(),
        }
    }
}

/// Kubernetes deployment configuration for an application
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct DeploymentConfig {
    /// Number of replicas
    #[serde(default = "default_replicas")]
    #[validate(range(min = 1, max = 100))]
    pub replicas: u32,

    /// Container port
    #[serde(default = "default_port")]
    #[validate(range(min = 1, max = 65535))]
    pub port: u16,

    /// CPU request (e.g., "100m", "1")
    #[serde(default)]
    pub cpu_request: Option<String>,

    /// Memory request (e.g., "128Mi", "1Gi")
    #[serde(default)]
    pub memory_request: Option<String>,

    /// CPU limit (e.g., "1", "2")
    #[serde(default)]
    pub cpu_limit: Option<String>,

    /// Memory limit (e.g., "512Mi", "2Gi")
    #[serde(default)]
    pub memory_limit: Option<String>,

    /// Environment variables
    #[serde(default)]
    pub env: Vec<EnvVar>,
}

fn default_replicas() -> u32 {
    1
}

fn default_port() -> u16 {
    8080
}

impl Default for DeploymentConfig {
    fn default() -> Self {
        Self {
            replicas: default_replicas(),
            port: default_port(),
            cpu_request: None,
            memory_request: None,
            cpu_limit: None,
            memory_limit: None,
            env: Vec::new(),
        }
    }
}

/// Environment variable configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct EnvVar {
    /// Variable name
    #[validate(length(min = 1))]
    pub name: String,

    /// Variable value (for non-sensitive values)
    #[serde(default)]
    pub value: Option<String>,

    /// Reference to a secret (alternative to value)
    #[serde(default)]
    pub secret_ref: Option<SecretRef>,
}

/// Reference to a Kubernetes secret
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct SecretRef {
    /// Secret name
    #[validate(length(min = 1))]
    pub name: String,

    /// Key within the secret
    #[validate(length(min = 1))]
    pub key: String,
}

/// Infrastructure configuration for all providers
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct InfrastructureConfig {
    /// Hetzner-specific configuration
    #[validate(nested)]
    pub hetzner: Option<HetznerConfig>,

    /// K3s-specific configuration
    #[validate(nested)]
    pub k3s: Option<K3sConfig>,

    /// PostgreSQL-specific configuration
    #[validate(nested)]
    pub postgres: Option<PostgresConfig>,

    /// Cloudflare-specific configuration
    #[validate(nested)]
    pub cloudflare: Option<CloudflareConfig>,

    /// GitLab-specific configuration
    #[validate(nested)]
    pub gitlab: Option<GitLabConfig>,
}

/// Hetzner Cloud configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct HetznerConfig {
    /// Server type (e.g., "cx11", "cpx21")
    pub server_type: String,

    /// Location (e.g., "nbg1", "fsn1")
    pub location: String,

    /// Number of servers
    #[validate(range(min = 1, max = 100))]
    pub count: u32,

    /// SSH key names or IDs already configured in Hetzner Cloud
    #[serde(default)]
    pub ssh_keys: Vec<String>,
}

/// K3s cluster configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct K3sConfig {
    /// K3s version
    pub version: Option<String>,

    /// Enable Traefik ingress controller
    pub enable_traefik: bool,
}

/// PostgreSQL configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct PostgresConfig {
    /// PostgreSQL version
    pub version: String,

    /// Database name
    #[validate(length(min = 1, max = 63))]
    pub database_name: String,
}

/// Cloudflare configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct CloudflareConfig {
    /// Primary domain
    #[validate(length(min = 1))]
    pub domain: String,

    /// Enable proxy
    pub proxied: bool,
}

/// GitLab configuration
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct GitLabConfig {
    /// GitLab instance URL
    #[validate(url)]
    pub url: String,

    /// Project namespace/group
    #[validate(length(min = 1))]
    pub namespace: String,
}

/// Configuration validation errors
#[derive(Debug, Error)]
pub enum ConfigError {
    /// Invalid TOML format
    #[error("Invalid TOML format: {0}")]
    InvalidToml(String),

    /// TOML deserialization error
    #[error("TOML deserialization error: {0}")]
    TomlDe(#[from] toml::de::Error),

    /// Validation failed
    #[error("Configuration validation failed: {0}")]
    ValidationFailed(#[from] validator::ValidationErrors),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Missing required provider configuration
    #[error("Missing required configuration for provider: {0}")]
    MissingProviderConfig(String),
}

impl LmrcConfig {
    /// Load configuration from a TOML file
    pub fn from_file(path: &std::path::Path) -> Result<Self, ConfigError> {
        let content = std::fs::read_to_string(path)?;
        Self::from_toml_str(&content)
    }

    /// Parse configuration from a TOML string
    pub fn from_toml_str(content: &str) -> Result<Self, ConfigError> {
        let config: Self = toml::from_str(content)?;
        config.validate()?;
        config.validate_provider_configs()?;
        Ok(config)
    }

    /// Validate that provider-specific configs are present when providers are selected
    fn validate_provider_configs(&self) -> Result<(), ConfigError> {
        // Validate server provider config
        if self.providers.server == "hetzner" && self.infrastructure.hetzner.is_none() {
            return Err(ConfigError::MissingProviderConfig("hetzner".to_string()));
        }

        // Validate kubernetes provider config
        if self.providers.kubernetes == "k3s" && self.infrastructure.k3s.is_none() {
            return Err(ConfigError::MissingProviderConfig("k3s".to_string()));
        }
        // Future: validate eks, gke, aks, k0s configs

        // Validate database provider config
        if self.providers.database == "postgres" && self.infrastructure.postgres.is_none() {
            return Err(ConfigError::MissingProviderConfig("postgres".to_string()));
        }

        // Validate DNS provider config
        if self.providers.dns == "cloudflare" && self.infrastructure.cloudflare.is_none() {
            return Err(ConfigError::MissingProviderConfig("cloudflare".to_string()));
        }

        // Validate Git provider config
        if self.providers.git == "gitlab" && self.infrastructure.gitlab.is_none() {
            return Err(ConfigError::MissingProviderConfig("gitlab".to_string()));
        }

        Ok(())
    }

    /// Generate a default/template configuration
    pub fn template() -> Self {
        Self {
            project: ProjectConfig {
                name: "my-project".to_string(),
                description: "My LMRC Stack project".to_string(),
            },
            providers: ProviderConfig {
                server: "hetzner".to_string(),
                kubernetes: "k3s".to_string(),
                database: "postgres".to_string(),
                dns: "cloudflare".to_string(),
                git: "gitlab".to_string(),
            },
            apps: AppsConfig {
                applications: vec![ApplicationEntry {
                    name: "api".to_string(),
                    app_type: "api".to_string(),
                    docker: Some(DockerConfig {
                        dockerfile: "apps/api/Dockerfile".to_string(),
                        context: "apps/api".to_string(),
                        tags: vec!["latest".to_string()],
                    }),
                    deployment: Some(DeploymentConfig {
                        replicas: 2,
                        port: 8080,
                        cpu_request: Some("100m".to_string()),
                        memory_request: Some("128Mi".to_string()),
                        cpu_limit: Some("1".to_string()),
                        memory_limit: Some("512Mi".to_string()),
                        env: vec![],
                    }),
                }],
            },
            infrastructure: InfrastructureConfig {
                hetzner: Some(HetznerConfig {
                    server_type: "cx21".to_string(),
                    location: "nbg1".to_string(),
                    count: 1,
                    ssh_keys: vec![],
                }),
                k3s: Some(K3sConfig {
                    version: None,
                    enable_traefik: true,
                }),
                postgres: Some(PostgresConfig {
                    version: "16".to_string(),
                    database_name: "myapp".to_string(),
                }),
                cloudflare: Some(CloudflareConfig {
                    domain: "example.com".to_string(),
                    proxied: true,
                }),
                gitlab: Some(GitLabConfig {
                    url: "https://gitlab.com".to_string(),
                    namespace: "mygroup".to_string(),
                }),
            },
        }
    }

    /// Convert config to TOML string
    pub fn to_toml_string(&self) -> Result<String, ConfigError> {
        toml::to_string_pretty(self).map_err(|e| ConfigError::InvalidToml(e.to_string()))
    }
}

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

    #[test]
    fn test_template_config_is_valid() {
        let config = LmrcConfig::template();
        assert!(config.validate().is_ok());
        assert!(config.validate_provider_configs().is_ok());
    }

    #[test]
    fn test_config_to_toml_and_back() {
        let config = LmrcConfig::template();
        let toml_str = config.to_toml_string().unwrap();
        let parsed = LmrcConfig::from_toml_str(&toml_str).unwrap();
        assert_eq!(config.project.name, parsed.project.name);
    }

    #[test]
    fn test_missing_provider_config() {
        let mut config = LmrcConfig::template();
        config.infrastructure.hetzner = None;
        assert!(config.validate_provider_configs().is_err());
    }
}
