// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! LDAP client for authentication operations via the Flex Gateway LDAP service.

use crate::error::LdapError;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use pdk_core::classy::client::{HttpClient, Service, Uri};
use pdk_core::log::debug;
use std::rc::Rc;
use std::str::FromStr;
use std::time::Duration;
use url::Url;

/// LDAP client that authenticates users via the Flex Gateway LDAP HTTP service.
///
/// Credentials and configuration are forwarded as HTTP headers to the LDAP
/// sidecar service; the library does **not** extract credentials from HTTP
/// requests — that responsibility belongs to the calling policy.
pub struct LdapClient {
    pub(crate) http_client: Rc<HttpClient>,
    pub(crate) server_url: String,
    pub(crate) server_user_dn: String,
    pub(crate) server_user_password: String,
    pub(crate) search_base: String,
    pub(crate) search_filter: String,
    pub(crate) search_in_subtree: bool,
}

const FLEX_LDAP_URL_HEADER: &str = "X-Flex-Authentication-Ldap-Url";
const FLEX_LDAP_BIND_DN_HEADER: &str = "X-Flex-Authentication-Ldap-Bind-Dn";
const FLEX_LDAP_BIND_PASS_HEADER: &str = "X-Flex-Authentication-Ldap-Bind-Pass";
const FLEX_LDAP_SEARCH_BASE_HEADER: &str = "X-Flex-Authentication-Ldap-Search-Base";
const FLEX_LDAP_SEARCH_FILTER_HEADER: &str = "X-Flex-Authentication-Ldap-Search-Filter";
const FLEX_LDAP_SEARCH_SUBTREE: &str = "X-Flex-Authentication-Ldap-Search-In-Subtree";

// Standard HTTP headers
const AUTHORIZATION_HEADER: &str = "authorization";
const AUTHORITY_HEADER_DEFAULT: &str = "ldap";

// Request constants
const BASIC_AUTHORIZATION_SCHEMA: &str = "Basic";
const LDAP_SERVICE_PATH: &str = "/services/authentication/ldap";
const LDAP_SERVICE: &str = "x-flex-services";

const PLATFORM_TIMEOUT_DURATION: Duration = Duration::from_secs(10);

impl LdapClient {
    /// Authenticate a user via the LDAP service using an encoded credential.
    ///
    /// # Arguments
    ///
    /// * `credential` - The base64-encoded credential value extracted by the policy.
    ///   For Basic authentication this is the base64-encoded `username:password`
    ///   string (without the `Basic ` prefix).
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Authentication successful (HTTP 200 from LDAP service)
    /// * `Err(LdapError::AuthenticationFailed)` - Invalid credentials (HTTP 400/401)
    /// * `Err(LdapError::ServerError)` - LDAP server error (HTTP 500/503)
    /// * `Err(LdapError::RequestFailed(_))` - Network or transport error
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // The policy extracts the credential — the library does not.
    /// let auth_value = extract_authorization_from_request()?; // e.g. "dXNlcjpwYXNz"
    ///
    /// match client.authenticate_encoded(&auth_value).await {
    ///     Ok(()) => { /* allow request */ }
    ///     Err(LdapError::AuthenticationFailed) => { /* reject with 401 */ }
    ///     Err(LdapError::ServerError) => { /* reject with 503 */ }
    ///     Err(e) => { /* handle other errors */ }
    /// }
    /// ```
    pub async fn authenticate_encoded(&self, credential: &str) -> Result<(), LdapError> {
        debug!("Calling ldap service.");
        // Build headers for the LDAP service request
        let mut headers = self.ldap_config_headers();
        let subtree = &self.search_in_subtree.to_string();
        headers.push((FLEX_LDAP_SEARCH_SUBTREE, subtree));

        let credentials = &self.format_basic_auth_header(credential);
        headers.push((AUTHORIZATION_HEADER, credentials));

        let authority_header = self.get_authority_header();

        let service = Service::new(
            LDAP_SERVICE,
            Uri::from_str(&format!("http://{authority_header}{LDAP_SERVICE_PATH}")).unwrap(),
        );

        debug!("Headers sent to ldap: {headers:?}");

        let response = self
            .http_client
            .request(&service)
            .path(LDAP_SERVICE_PATH)
            .headers(headers)
            .timeout(PLATFORM_TIMEOUT_DURATION)
            .get()
            .await
            .map_err(|e| LdapError::RequestFailed(format!("{e:?}")))?;

        let status = response.status_code();

        match status {
            200 => Ok(()),
            400 => Err(LdapError::AuthenticationFailed),
            401 => Err(LdapError::AuthenticationFailed),
            500 => Err(LdapError::ServerError),
            503 => Err(LdapError::ServerError),
            _ => Err(LdapError::AuthenticationFailed),
        }
    }

    /// Authenticate a user via the LDAP service using plaintext credentials.
    ///
    /// This is a convenience method that encodes the username and password
    /// and calls [`authenticate_encoded`](Self::authenticate_encoded).
    ///
    /// # Arguments
    ///
    /// * `username` - The username extracted by the policy
    /// * `password` - The password extracted by the policy
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Authentication successful (HTTP 200 from LDAP service)
    /// * `Err(LdapError::AuthenticationFailed)` - Invalid credentials (HTTP 400/401)
    /// * `Err(LdapError::ServerError)` - LDAP server error (HTTP 500/503)
    /// * `Err(LdapError::RequestFailed(_))` - Network or transport error
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // The policy extracts username and password — the library does not.
    /// let username = extract_username_from_request()?;
    /// let password = extract_password_from_request()?;
    ///
    /// match client.authenticate(&username, &password).await {
    ///     Ok(()) => { /* allow request */ }
    ///     Err(LdapError::AuthenticationFailed) => { /* reject with 401 */ }
    ///     Err(LdapError::ServerError) => { /* reject with 503 */ }
    ///     Err(e) => { /* handle other errors */ }
    /// }
    /// ```
    pub async fn authenticate(&self, username: &str, password: &str) -> Result<(), LdapError> {
        let encoded_credentials = URL_SAFE_NO_PAD.encode(format!("{username}:{password}"));
        self.authenticate_encoded(&encoded_credentials).await
    }

    fn ldap_config_headers(&self) -> Vec<(&str, &str)> {
        vec![
            (FLEX_LDAP_URL_HEADER, &self.server_url),
            (FLEX_LDAP_BIND_DN_HEADER, &self.server_user_dn),
            (FLEX_LDAP_BIND_PASS_HEADER, &self.server_user_password),
            (FLEX_LDAP_SEARCH_BASE_HEADER, &self.search_base),
            (FLEX_LDAP_SEARCH_FILTER_HEADER, &self.search_filter),
        ]
    }

    /// Format a credential value as a `Basic <credential>` authorization header value.
    fn format_basic_auth_header(&self, credential: &str) -> String {
        format!("{BASIC_AUTHORIZATION_SCHEMA} {credential}")
    }

    fn get_authority_header(&self) -> String {
        match Url::parse(&self.server_url) {
            Ok(val) => String::from(val.host_str().unwrap_or(AUTHORITY_HEADER_DEFAULT)),
            Err(_) => String::from(AUTHORITY_HEADER_DEFAULT),
        }
    }
}
