use std::collections::HashMap;

use clap::Args;
use serde::{Deserialize, Serialize};

use crate::{config::IdpConfig as IdpConfigStruct, error::KmsError};

// Support for JWT token inspired by the doc at : https://cloud.google.com/api-gateway/docs/authenticating-users-jwt
// and following pages

#[derive(Debug, Default, Args, Deserialize, Serialize)]
#[serde(default)]
pub struct IdpAuthConfig {
    /// JWT authentication provider configuration.
    ///
    /// The expected argument is --jwt-auth-provider="`PROVIDER_CONFIG_1`" --jwt-auth-provider="`PROVIDER_CONFIG_2`" ...
    /// where each `PROVIDER_CONFIG_N` defines one identity provider configuration.
    ///
    /// Each provider configuration `PROVIDER_CONFIG_N` should be in the format: "`JWT_ISSUER_URI,JWKS_URI,JWT_AUDIENCE_1,JWT_AUDIENCE_2,...`"
    /// where:
    /// - `JWT_ISSUER_URI`: The issuer URI of the JWT token (required)
    /// - `JWKS_URI`: The JWKS (JSON Web Key Set) URI (optional, defaults to <JWT_ISSUER_URI>/.well-known/jwks.json)
    /// - `JWT_AUDIENCE_1..N`: One or more audience values for the JWT token (optional)
    ///
    /// Examples:
    /// --jwt-auth-provider="https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs, kacls-migration, another-audience"
    /// --jwt-auth-provider="https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys"
    /// --jwt-auth-provider="https://<your-tenant>.<region>.auth0.com/""
    //
    /// This argument can be repeated to configure multiple identity providers.
    #[clap(verbatim_doc_comment, long, env = "KMS_JWT_AUTH_PROVIDER", action = clap::ArgAction::Append)]
    pub jwt_auth_provider: Option<Vec<String>>,
}

impl IdpAuthConfig {
    /// Build a JWKS URI using `jwt_issuer_uri` and an optional `jwks_uri`.
    pub(crate) fn uri(jwt_issuer_uri: &str, jwks_uri: Option<&str>) -> String {
        jwks_uri.as_ref().map_or_else(
            || {
                format!(
                    "{}/.well-known/jwks.json",
                    jwt_issuer_uri.trim_end_matches('/')
                )
            },
            std::string::ToString::to_string,
        )
    }

    /// Parse this configuration into one identity provider configuration per JWT authentication provider.
    ///
    /// Each provider configuration string is parsed in the format: "`JWT_ISSUER_URI,JWKS_URI,JWT_AUDIENCE_1,JWT_AUDIENCE_2,...`"
    /// where `JWKS_URI` and `JWT_AUDIENCE_*` are optional and can be empty.
    ///
    /// Duplicate configurations (same JWT issuer URI, JWKS URI, and audience list) are automatically
    /// deduplicated, with the last one taking precedence. The entire audience list is compared
    /// for equality when determining duplicates.
    pub(crate) fn extract_idp_configs(self) -> Result<Option<Vec<IdpConfigStruct>>, KmsError> {
        self.jwt_auth_provider
            .map(|provider_configs| {
                // Key the map by the full triple to handle exact duplicates (last one wins)
                type IdpKey = (String, Option<String>, Option<Vec<String>>);
                let mut configs: HashMap<IdpKey, IdpConfigStruct> = HashMap::new();

                for provider_config in provider_configs {
                    let parts: Vec<&str> = provider_config.split(',').collect();

                    // Extract the issuer URI (first part, required)
                    let jwt_issuer_uri = match parts.first() {
                        Some(issuer) if !issuer.trim().is_empty() => issuer.trim().to_owned(),
                        _ => {
                            return Err(KmsError::InvalidRequest(
                                "JWT provider configuration must contain at least a non-empty \
                                 issuer URI"
                                    .to_owned(),
                            ));
                        }
                    };

                    // Extract JWKS URI (second part, optional)
                    let jwks_uri = parts
                        .get(1)
                        .filter(|s| !s.trim().is_empty())
                        .map(|s| s.trim().to_owned());

                    // Extract JWT audiences (third and subsequent parts, optional)
                    let jwt_audience = parts.get(2..).and_then(|slice| {
                        let audiences: Vec<String> = slice
                            .iter()
                            .map(|s| s.trim().to_owned())
                            .filter(|s| !s.is_empty())
                            .collect();
                        (!audiences.is_empty()).then_some(audiences)
                    });

                    let idp_config = IdpConfigStruct {
                        jwt_issuer_uri: jwt_issuer_uri.clone(),
                        jwks_uri: jwks_uri.clone(),
                        jwt_audience: jwt_audience.clone(),
                    };

                    // Insert using the full triple as key to handle exact duplicates (last one wins)
                    let key = (jwt_issuer_uri, jwks_uri, jwt_audience);
                    configs.insert(key, idp_config);
                }

                // Convert HashMap values back to Vec
                Ok(configs.into_values().collect())
            })
            .transpose()
    }
}

#[cfg(test)]
mod tests {
    use cosmian_logger::{info, log_init};

    use crate::config::IdpAuthConfig;

    #[test]
    #[allow(clippy::unwrap_used)]
    fn test_idp_extraction() {
        log_init(None);
        let idp_list = IdpAuthConfig {
            jwt_auth_provider: Some(vec![
                "https://issuer1.com,https://jwks1.com,key1,key2".to_owned(),
                "https://issuer2.com,,key3".to_owned(),
                "https://issuer1.com,https://jwks1.com,key1,key2".to_owned(), // Duplicate
                "https://issuer3.com,,".to_owned(),
            ]),
        };
        let extracted = idp_list.extract_idp_configs().unwrap().unwrap();
        assert_eq!(extracted.len(), 3); // One duplicate should be removed
        info!("Extracted IDP Configs: {:#?}", extracted);
    }
}
