use std::sync::Arc;
use uuid::Uuid;

use crate::application::dtos::settings_dto::{
    OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
};
use crate::application::services::auth_application_service::AuthApplicationService;
use crate::common::config::OidcConfig;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::repositories::settings_repository::SettingsRepository;
use crate::infrastructure::repositories::pg::SettingsPgRepository;
use crate::infrastructure::services::oidc_service::OidcService;

/// Admin settings service — manages platform configuration in the database.
///
/// Configuration priority: **env vars > DB settings > defaults**.
/// Supports hot-reloading OIDC configuration without server restart.
pub struct AdminSettingsService {
    settings_repo: Arc<SettingsPgRepository>,
    env_oidc_config: OidcConfig,
    auth_app_service: Arc<AuthApplicationService>,
    server_base_url: String,
}

impl AdminSettingsService {
    pub fn new(
        settings_repo: Arc<SettingsPgRepository>,
        env_oidc_config: OidcConfig,
        auth_app_service: Arc<AuthApplicationService>,
        server_base_url: String,
    ) -> Self {
        Self {
            settings_repo,
            env_oidc_config,
            auth_app_service,
            server_base_url,
        }
    }

    /// Auto-generated OIDC callback URL
    fn callback_url(&self) -> String {
        let base = self.server_base_url.trim_end_matches('/');
        format!("{}/api/auth/oidc/callback", base)
    }

    /// Detect which OIDC fields are overridden by environment variables
    fn get_env_overrides(&self) -> Vec<String> {
        let mut out = Vec::new();
        let vars = [
            ("OXICLOUD_OIDC_ENABLED", "enabled"),
            ("OXICLOUD_OIDC_ISSUER_URL", "issuer_url"),
            ("OXICLOUD_OIDC_CLIENT_ID", "client_id"),
            ("OXICLOUD_OIDC_CLIENT_SECRET", "client_secret"),
            ("OXICLOUD_OIDC_SCOPES", "scopes"),
            ("OXICLOUD_OIDC_AUTO_PROVISION", "auto_provision"),
            ("OXICLOUD_OIDC_ADMIN_GROUPS", "admin_groups"),
            (
                "OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN",
                "disable_password_login",
            ),
            ("OXICLOUD_OIDC_PROVIDER_NAME", "provider_name"),
        ];
        for (env_key, field_name) in &vars {
            if std::env::var(env_key).is_ok() {
                out.push(field_name.to_string());
            }
        }
        out
    }

    /// Apply environment variable overrides on top of a config
    fn apply_env_overrides(&self, config: &mut OidcConfig) {
        let e = &self.env_oidc_config;
        if std::env::var("OXICLOUD_OIDC_ENABLED").is_ok() {
            config.enabled = e.enabled;
        }
        if std::env::var("OXICLOUD_OIDC_ISSUER_URL").is_ok() {
            config.issuer_url = e.issuer_url.clone();
        }
        if std::env::var("OXICLOUD_OIDC_CLIENT_ID").is_ok() {
            config.client_id = e.client_id.clone();
        }
        if std::env::var("OXICLOUD_OIDC_CLIENT_SECRET").is_ok() {
            config.client_secret = e.client_secret.clone();
        }
        if std::env::var("OXICLOUD_OIDC_SCOPES").is_ok() {
            config.scopes = e.scopes.clone();
        }
        if std::env::var("OXICLOUD_OIDC_REDIRECT_URI").is_ok() {
            config.redirect_uri = e.redirect_uri.clone();
        }
        if std::env::var("OXICLOUD_OIDC_FRONTEND_URL").is_ok() {
            config.frontend_url = e.frontend_url.clone();
        }
        if std::env::var("OXICLOUD_OIDC_AUTO_PROVISION").is_ok() {
            config.auto_provision = e.auto_provision;
        }
        if std::env::var("OXICLOUD_OIDC_ADMIN_GROUPS").is_ok() {
            config.admin_groups = e.admin_groups.clone();
        }
        if std::env::var("OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN").is_ok() {
            config.disable_password_login = e.disable_password_login;
        }
        if std::env::var("OXICLOUD_OIDC_PROVIDER_NAME").is_ok() {
            config.provider_name = e.provider_name.clone();
        }
    }

    /// Load the effective OIDC config: DB settings + env var overrides + defaults.
    pub async fn load_effective_oidc_config(&self) -> Result<OidcConfig, DomainError> {
        let db = self.settings_repo.get_by_category("oidc").await?;
        let d = OidcConfig::default();

        let mut config = OidcConfig {
            enabled: db
                .get("oidc.enabled")
                .and_then(|v| v.parse().ok())
                .unwrap_or(d.enabled),
            issuer_url: db.get("oidc.issuer_url").cloned().unwrap_or(d.issuer_url),
            client_id: db.get("oidc.client_id").cloned().unwrap_or(d.client_id),
            client_secret: db
                .get("oidc.client_secret")
                .cloned()
                .unwrap_or(d.client_secret),
            redirect_uri: self.callback_url(),
            scopes: db.get("oidc.scopes").cloned().unwrap_or(d.scopes),
            frontend_url: self.server_base_url.clone(),
            auto_provision: db
                .get("oidc.auto_provision")
                .and_then(|v| v.parse().ok())
                .unwrap_or(d.auto_provision),
            admin_groups: db
                .get("oidc.admin_groups")
                .cloned()
                .unwrap_or(d.admin_groups),
            disable_password_login: db
                .get("oidc.disable_password_login")
                .and_then(|v| v.parse().ok())
                .unwrap_or(d.disable_password_login),
            provider_name: db
                .get("oidc.provider_name")
                .cloned()
                .unwrap_or(d.provider_name),
        };

        // Env vars override DB
        self.apply_env_overrides(&mut config);
        Ok(config)
    }

    /// Get OIDC settings for display in admin UI (secrets masked).
    pub async fn get_oidc_settings(&self) -> Result<OidcSettingsDto, DomainError> {
        let db = self.settings_repo.get_by_category("oidc").await?;

        let has_secret = db
            .get("oidc.client_secret")
            .map(|s| !s.is_empty())
            .unwrap_or(false)
            || std::env::var("OXICLOUD_OIDC_CLIENT_SECRET")
                .map(|s| !s.is_empty())
                .unwrap_or(false);

        // Load effective config with env var overrides applied
        let effective = self.load_effective_oidc_config().await?;

        Ok(OidcSettingsDto {
            enabled: effective.enabled,
            issuer_url: effective.issuer_url,
            client_id: effective.client_id,
            client_secret_set: has_secret,
            scopes: effective.scopes,
            auto_provision: effective.auto_provision,
            admin_groups: effective.admin_groups,
            disable_password_login: effective.disable_password_login,
            provider_name: effective.provider_name,
            callback_url: effective.redirect_uri.clone(),
            env_overrides: self.get_env_overrides(),
        })
    }

    /// Save OIDC settings to DB and hot-reload the OIDC service.
    pub async fn save_oidc_settings(
        &self,
        dto: SaveOidcSettingsDto,
        updated_by: Uuid,
    ) -> Result<(), DomainError> {
        let cat = "oidc";
        let by = Some(updated_by);

        self.settings_repo
            .set("oidc.enabled", &dto.enabled.to_string(), cat, false, by)
            .await?;
        self.settings_repo
            .set("oidc.issuer_url", &dto.issuer_url, cat, false, by)
            .await?;
        self.settings_repo
            .set("oidc.client_id", &dto.client_id, cat, false, by)
            .await?;

        if let Some(ref secret) = dto.client_secret
            && !secret.is_empty()
        {
            self.settings_repo
                .set("oidc.client_secret", secret, cat, true, by)
                .await?;
        }
        if let Some(ref v) = dto.scopes {
            self.settings_repo
                .set("oidc.scopes", v, cat, false, by)
                .await?;
        }
        if let Some(v) = dto.auto_provision {
            self.settings_repo
                .set("oidc.auto_provision", &v.to_string(), cat, false, by)
                .await?;
        }
        if let Some(ref v) = dto.admin_groups {
            self.settings_repo
                .set("oidc.admin_groups", v, cat, false, by)
                .await?;
        }
        if let Some(v) = dto.disable_password_login {
            self.settings_repo
                .set(
                    "oidc.disable_password_login",
                    &v.to_string(),
                    cat,
                    false,
                    by,
                )
                .await?;
        }
        if let Some(ref v) = dto.provider_name {
            self.settings_repo
                .set("oidc.provider_name", v, cat, false, by)
                .await?;
        }

        // Hot-reload OIDC service
        let eff = self.load_effective_oidc_config().await?;
        if eff.enabled
            && !eff.issuer_url.is_empty()
            && !eff.client_id.is_empty()
            && !eff.client_secret.is_empty()
        {
            let svc = Arc::new(OidcService::new(eff.clone()));
            self.auth_app_service.reload_oidc(svc, eff);
            tracing::info!("OIDC service hot-reloaded with new configuration");
        } else if !eff.enabled {
            self.auth_app_service.disable_oidc();
            tracing::info!("OIDC service disabled via admin panel");
        }

        Ok(())
    }

    /// Test OIDC connection by fetching the discovery document.
    pub async fn test_oidc_connection(
        &self,
        dto: TestOidcConnectionDto,
    ) -> Result<OidcTestResultDto, DomainError> {
        let issuer = dto.issuer_url.trim_end_matches('/');
        let discovery_url = format!("{}/.well-known/openid-configuration", issuer);

        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(|e| {
                DomainError::new(
                    ErrorKind::InternalError,
                    "OIDC",
                    format!("HTTP client error: {}", e),
                )
            })?;

        let resp = match client.get(&discovery_url).send().await {
            Ok(r) => r,
            Err(e) => {
                return Ok(OidcTestResultDto {
                    success: false,
                    message: format!(
                        "Cannot reach the OIDC provider: {}. Check your Issuer URL.",
                        e
                    ),
                    issuer: None,
                    authorization_endpoint: None,
                    token_endpoint: None,
                    userinfo_endpoint: None,
                    provider_name_suggestion: None,
                });
            }
        };

        if !resp.status().is_success() {
            return Ok(OidcTestResultDto {
                success: false,
                message: format!(
                    "OIDC discovery returned HTTP {} — the Issuer URL may be incorrect.",
                    resp.status()
                ),
                issuer: None,
                authorization_endpoint: None,
                token_endpoint: None,
                userinfo_endpoint: None,
                provider_name_suggestion: None,
            });
        }

        #[derive(serde::Deserialize)]
        struct Discovery {
            issuer: Option<String>,
            authorization_endpoint: Option<String>,
            token_endpoint: Option<String>,
            userinfo_endpoint: Option<String>,
        }

        let disc: Discovery = match resp.json().await {
            Ok(d) => d,
            Err(e) => {
                return Ok(OidcTestResultDto {
                    success: false,
                    message: format!("Invalid discovery document: {}", e),
                    issuer: None,
                    authorization_endpoint: None,
                    token_endpoint: None,
                    userinfo_endpoint: None,
                    provider_name_suggestion: None,
                });
            }
        };

        // Suggest provider name from hostname
        let suggestion = issuer
            .trim_start_matches("https://")
            .trim_start_matches("http://")
            .split('/')
            .next()
            .and_then(|host| {
                let parts: Vec<&str> = host.split('.').collect();
                let name = if parts.len() >= 2 { parts[0] } else { host };
                let mut c = name.chars();
                c.next().map(|f| f.to_uppercase().to_string() + c.as_str())
            });

        Ok(OidcTestResultDto {
            success: true,
            message: "OIDC provider is reachable and returned a valid discovery document.".into(),
            issuer: disc.issuer,
            authorization_endpoint: disc.authorization_endpoint,
            token_endpoint: disc.token_endpoint,
            userinfo_endpoint: disc.userinfo_endpoint,
            provider_name_suggestion: suggestion,
        })
    }

    // ========================================================================
    // System Initialization
    // ========================================================================

    /// Check if the system has been initialized (first admin created).
    /// Returns `true` if the `system_initialized` flag is set to `"true"` in the DB.
    pub async fn is_system_initialized(&self) -> bool {
        match self.settings_repo.get("system_initialized").await {
            Ok(Some(val)) => val == "true",
            _ => false, // fail-closed: if DB error or key absent, system is NOT initialized
        }
    }

    /// Mark the system as initialized after the first admin is created.
    pub async fn mark_system_initialized(&self, admin_user_id: Uuid) -> Result<(), DomainError> {
        self.settings_repo
            .set(
                "system_initialized",
                "true",
                "system",
                false,
                Some(admin_user_id),
            )
            .await
    }

    /// Atomically try to claim system initialization.
    ///
    /// Returns `Ok(true)` if this call was the one that marked the system as
    /// initialized (the caller "won" the race), or `Ok(false)` if another
    /// request already did it.  This eliminates the race-condition window
    /// between `is_system_initialized()` and `mark_system_initialized()`.
    pub async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
        self.settings_repo
            .try_claim_initialization(admin_user_id)
            .await
    }

    // ========================================================================
    // Registration Control
    // ========================================================================

    /// Check if public self-registration is enabled.
    /// Priority: env var `OXICLOUD_DISABLE_REGISTRATION` > DB setting > default (true).
    pub async fn get_registration_enabled(&self) -> bool {
        // Env var override takes priority
        if let Ok(val) = std::env::var("OXICLOUD_DISABLE_REGISTRATION") {
            return !matches!(val.to_lowercase().as_str(), "true" | "1" | "yes");
        }
        // Check DB setting
        match self.settings_repo.get("registration_enabled").await {
            Ok(Some(val)) => val == "true",
            _ => true, // default: enabled
        }
    }

    /// Enable or disable public self-registration.
    pub async fn set_registration_enabled(
        &self,
        enabled: bool,
        updated_by: Uuid,
    ) -> Result<(), DomainError> {
        self.settings_repo
            .set(
                "registration_enabled",
                if enabled { "true" } else { "false" },
                "general",
                false,
                Some(updated_by),
            )
            .await
    }
}
