//! Configuration structs loaded once at startup from environment variables.
//!
//! Each struct provides a `from_env()` constructor that reads `std::env::var`
//! values. Required variables cause an `Error::ConfigError` on failure;
//! optional ones default to an empty string.

use secrecy::SecretString;

use super::Error;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Read a required environment variable or return `Error::ConfigError`.
fn required_env(name: &str) -> Result<String, Error> {
    std::env::var(name).map_err(|_| Error::ConfigError(format!("{name} is required but not set")))
}

/// Read an optional environment variable, defaulting to an empty string.
fn optional_env(name: &str) -> String {
    std::env::var(name).unwrap_or_default()
}

// ---------------------------------------------------------------------------
// KeycloakConfig
// ---------------------------------------------------------------------------

/// Keycloak OpenID Connect settings for the public (frontend) client.
///
/// Also carries the admin service-account credentials used for
/// server-to-server calls (e.g. user management APIs).
#[derive(Debug)]
pub struct KeycloakConfig {
    /// Base URL of the Keycloak instance (e.g. `http://localhost:8080`).
    pub url: String,
    /// Keycloak realm name.
    pub realm: String,
    /// Public client ID used by the dashboard frontend.
    pub client_id: String,
    /// OAuth redirect URI registered in Keycloak.
    pub redirect_uri: String,
    /// Root URL of this application (used for post-logout redirect).
    pub app_url: String,
    /// Confidential client ID for admin/server-to-server calls.
    pub admin_client_id: String,
    /// Confidential client secret (wrapped for debug safety).
    pub admin_client_secret: SecretString,
}

impl KeycloakConfig {
    /// Load Keycloak configuration from environment variables.
    ///
    /// # Errors
    ///
    /// Returns `Error::ConfigError` if a required variable is missing.
    pub fn from_env() -> Result<Self, Error> {
        Ok(Self {
            url: required_env("KEYCLOAK_URL")?,
            realm: required_env("KEYCLOAK_REALM")?,
            client_id: required_env("KEYCLOAK_CLIENT_ID")?,
            redirect_uri: required_env("REDIRECT_URI")?,
            app_url: required_env("APP_URL")?,
            admin_client_id: optional_env("KEYCLOAK_ADMIN_CLIENT_ID"),
            admin_client_secret: SecretString::from(optional_env("KEYCLOAK_ADMIN_CLIENT_SECRET")),
        })
    }

    /// OpenID Connect authorization endpoint URL.
    pub fn auth_endpoint(&self) -> String {
        format!(
            "{}/realms/{}/protocol/openid-connect/auth",
            self.url, self.realm
        )
    }

    /// OpenID Connect token endpoint URL.
    pub fn token_endpoint(&self) -> String {
        format!(
            "{}/realms/{}/protocol/openid-connect/token",
            self.url, self.realm
        )
    }

    /// OpenID Connect userinfo endpoint URL.
    pub fn userinfo_endpoint(&self) -> String {
        format!(
            "{}/realms/{}/protocol/openid-connect/userinfo",
            self.url, self.realm
        )
    }

    /// OpenID Connect end-session (logout) endpoint URL.
    pub fn logout_endpoint(&self) -> String {
        format!(
            "{}/realms/{}/protocol/openid-connect/logout",
            self.url, self.realm
        )
    }
}

// ---------------------------------------------------------------------------
// SmtpConfig
// ---------------------------------------------------------------------------

/// SMTP mail settings for transactional emails (invites, alerts, etc.).
#[derive(Debug)]
pub struct SmtpConfig {
    /// SMTP server hostname.
    pub host: String,
    /// SMTP server port (as string for flexibility, e.g. "587").
    pub port: String,
    /// SMTP username.
    pub username: String,
    /// SMTP password (wrapped for debug safety).
    pub password: SecretString,
    /// Sender address shown in the `From:` header.
    pub from_address: String,
}

impl SmtpConfig {
    /// Load SMTP configuration from environment variables.
    ///
    /// All fields are optional; defaults to empty strings when absent.
    ///
    /// # Errors
    ///
    /// Currently infallible but returns `Result` for consistency.
    pub fn from_env() -> Result<Self, Error> {
        Ok(Self {
            host: optional_env("SMTP_HOST"),
            port: optional_env("SMTP_PORT"),
            username: optional_env("SMTP_USERNAME"),
            password: SecretString::from(optional_env("SMTP_PASSWORD")),
            from_address: optional_env("SMTP_FROM_ADDRESS"),
        })
    }
}

// ---------------------------------------------------------------------------
// ServiceUrls
// ---------------------------------------------------------------------------

/// URLs and credentials for external services (LiteLLM, SearXNG, S3, etc.).
#[derive(Debug)]
pub struct ServiceUrls {
    /// LiteLLM proxy base URL.
    pub litellm_url: String,
    /// Default LiteLLM model to use.
    pub litellm_model: String,
    /// LiteLLM API key for authenticated requests.
    pub litellm_api_key: String,
    /// SearXNG meta-search engine base URL.
    pub searxng_url: String,
    /// LangChain service URL.
    pub langchain_url: String,
    /// LangGraph service URL.
    pub langgraph_url: String,
    /// LangFlow visual workflow builder URL.
    pub langflow_url: String,
    /// Langfuse observability URL.
    pub langfuse_url: String,
    /// Vector database URL.
    pub vectordb_url: String,
    /// S3-compatible object storage URL.
    pub s3_url: String,
    /// S3 access key.
    pub s3_access_key: String,
    /// S3 secret key (wrapped for debug safety).
    pub s3_secret_key: SecretString,
    /// Compliance scanner URL (external tool opened in a new tab).
    pub compliance_scanner_url: String,
}

impl ServiceUrls {
    /// Load service URLs from environment variables.
    ///
    /// All fields are optional with sensible defaults where applicable.
    ///
    /// # Errors
    ///
    /// Currently infallible but returns `Result` for consistency.
    pub fn from_env() -> Result<Self, Error> {
        Ok(Self {
            litellm_url: std::env::var("LITELLM_URL")
                .unwrap_or_else(|_| "http://localhost:4000".into()),
            litellm_model: std::env::var("LITELLM_MODEL").unwrap_or_else(|_| "qwen3-32b".into()),
            litellm_api_key: optional_env("LITELLM_API_KEY"),
            searxng_url: std::env::var("SEARXNG_URL")
                .unwrap_or_else(|_| "http://localhost:8888".into()),
            langchain_url: optional_env("LANGCHAIN_URL"),
            langgraph_url: optional_env("LANGGRAPH_URL"),
            langflow_url: optional_env("LANGFLOW_URL"),
            langfuse_url: optional_env("LANGFUSE_URL"),
            vectordb_url: optional_env("VECTORDB_URL"),
            s3_url: optional_env("S3_URL"),
            s3_access_key: optional_env("S3_ACCESS_KEY"),
            s3_secret_key: SecretString::from(optional_env("S3_SECRET_KEY")),
            compliance_scanner_url: optional_env("COMPLIANCE_SCANNER_URL"),
        })
    }
}

// ---------------------------------------------------------------------------
// StripeConfig
// ---------------------------------------------------------------------------

/// Stripe billing configuration.
#[derive(Debug)]
pub struct StripeConfig {
    /// Stripe secret API key (wrapped for debug safety).
    pub secret_key: SecretString,
    /// Stripe webhook signing secret (wrapped for debug safety).
    pub webhook_secret: SecretString,
    /// Stripe publishable key (safe to expose to the frontend).
    pub publishable_key: String,
}

impl StripeConfig {
    /// Load Stripe configuration from environment variables.
    ///
    /// # Errors
    ///
    /// Currently infallible but returns `Result` for consistency.
    pub fn from_env() -> Result<Self, Error> {
        Ok(Self {
            secret_key: SecretString::from(optional_env("STRIPE_SECRET_KEY")),
            webhook_secret: SecretString::from(optional_env("STRIPE_WEBHOOK_SECRET")),
            publishable_key: optional_env("STRIPE_PUBLISHABLE_KEY"),
        })
    }
}

// ---------------------------------------------------------------------------
// LlmProvidersConfig
// ---------------------------------------------------------------------------

/// Comma-separated list of enabled LLM provider identifiers.
///
/// For example: `LLM_PROVIDERS=litellm,openai,anthropic`
#[derive(Debug)]
pub struct LlmProvidersConfig {
    /// Parsed provider names.
    pub providers: Vec<String>,
}

impl LlmProvidersConfig {
    /// Load the provider list from `LLM_PROVIDERS`.
    ///
    /// # Errors
    ///
    /// Currently infallible but returns `Result` for consistency.
    pub fn from_env() -> Result<Self, Error> {
        let raw = optional_env("LLM_PROVIDERS");
        let providers: Vec<String> = raw
            .split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        Ok(Self { providers })
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used)]

    use super::*;
    use pretty_assertions::assert_eq;
    use serial_test::serial;

    // -----------------------------------------------------------------------
    // KeycloakConfig endpoint methods (no env vars needed)
    // -----------------------------------------------------------------------

    fn sample_keycloak() -> KeycloakConfig {
        KeycloakConfig {
            url: "https://auth.example.com".into(),
            realm: "myrealm".into(),
            client_id: "dashboard".into(),
            redirect_uri: "https://app.example.com/callback".into(),
            app_url: "https://app.example.com".into(),
            admin_client_id: String::new(),
            admin_client_secret: SecretString::from(String::new()),
        }
    }

    #[test]
    fn keycloak_auth_endpoint() {
        let kc = sample_keycloak();
        assert_eq!(
            kc.auth_endpoint(),
            "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth"
        );
    }

    #[test]
    fn keycloak_token_endpoint() {
        let kc = sample_keycloak();
        assert_eq!(
            kc.token_endpoint(),
            "https://auth.example.com/realms/myrealm/protocol/openid-connect/token"
        );
    }

    #[test]
    fn keycloak_userinfo_endpoint() {
        let kc = sample_keycloak();
        assert_eq!(
            kc.userinfo_endpoint(),
            "https://auth.example.com/realms/myrealm/protocol/openid-connect/userinfo"
        );
    }

    #[test]
    fn keycloak_logout_endpoint() {
        let kc = sample_keycloak();
        assert_eq!(
            kc.logout_endpoint(),
            "https://auth.example.com/realms/myrealm/protocol/openid-connect/logout"
        );
    }

    // -----------------------------------------------------------------------
    // LlmProvidersConfig::from_env()
    // -----------------------------------------------------------------------

    #[test]
    #[serial]
    fn llm_providers_empty_string() {
        std::env::set_var("LLM_PROVIDERS", "");
        let cfg = LlmProvidersConfig::from_env().unwrap();
        assert!(cfg.providers.is_empty());
        std::env::remove_var("LLM_PROVIDERS");
    }

    #[test]
    #[serial]
    fn llm_providers_single() {
        std::env::set_var("LLM_PROVIDERS", "litellm");
        let cfg = LlmProvidersConfig::from_env().unwrap();
        assert_eq!(cfg.providers, vec!["litellm"]);
        std::env::remove_var("LLM_PROVIDERS");
    }

    #[test]
    #[serial]
    fn llm_providers_multiple() {
        std::env::set_var("LLM_PROVIDERS", "litellm,openai,anthropic");
        let cfg = LlmProvidersConfig::from_env().unwrap();
        assert_eq!(cfg.providers, vec!["litellm", "openai", "anthropic"]);
        std::env::remove_var("LLM_PROVIDERS");
    }

    #[test]
    #[serial]
    fn llm_providers_trims_whitespace() {
        std::env::set_var("LLM_PROVIDERS", " litellm , openai ");
        let cfg = LlmProvidersConfig::from_env().unwrap();
        assert_eq!(cfg.providers, vec!["litellm", "openai"]);
        std::env::remove_var("LLM_PROVIDERS");
    }

    #[test]
    #[serial]
    fn llm_providers_filters_empty_entries() {
        std::env::set_var("LLM_PROVIDERS", "litellm,,openai,");
        let cfg = LlmProvidersConfig::from_env().unwrap();
        assert_eq!(cfg.providers, vec!["litellm", "openai"]);
        std::env::remove_var("LLM_PROVIDERS");
    }

    // -----------------------------------------------------------------------
    // ServiceUrls::from_env() defaults
    // -----------------------------------------------------------------------

    #[test]
    #[serial]
    fn service_urls_default_litellm_url() {
        std::env::remove_var("LITELLM_URL");
        let svc = ServiceUrls::from_env().unwrap();
        assert_eq!(svc.litellm_url, "http://localhost:4000");
    }

    #[test]
    #[serial]
    fn service_urls_default_litellm_model() {
        std::env::remove_var("LITELLM_MODEL");
        let svc = ServiceUrls::from_env().unwrap();
        assert_eq!(svc.litellm_model, "qwen3-32b");
    }

    #[test]
    #[serial]
    fn service_urls_default_searxng_url() {
        std::env::remove_var("SEARXNG_URL");
        let svc = ServiceUrls::from_env().unwrap();
        assert_eq!(svc.searxng_url, "http://localhost:8888");
    }

    #[test]
    #[serial]
    fn service_urls_custom_litellm_url() {
        std::env::set_var("LITELLM_URL", "http://litellm-host:4000");
        let svc = ServiceUrls::from_env().unwrap();
        assert_eq!(svc.litellm_url, "http://litellm-host:4000");
        std::env::remove_var("LITELLM_URL");
    }

    #[test]
    #[serial]
    fn required_env_missing_returns_config_error() {
        std::env::remove_var("__TEST_REQUIRED_MISSING__");
        let result = required_env("__TEST_REQUIRED_MISSING__");
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(err_msg.contains("__TEST_REQUIRED_MISSING__"));
    }
}
