/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * documentdb_gateway_core/src/service/tls.rs
 *
 *-------------------------------------------------------------------------
 */

//! TLS certificate management and SSL/TLS configuration for the `DocumentDB` gateway.
//!
//! This module provides comprehensive certificate management capabilities including
//! automatic certificate generation, file-based certificate loading, certificate
//! bundle management, and automatic certificate reloading for zero-downtime updates.
//!
//! # Features
//!
//! - **Certificate Store Management**: Handles both file-based and auto-generated certificates
//! - **Certificate Bundles**: Manages server certificates, private keys, and CA chains
//! - **TLS Provider**: Automatic certificate reloading with background monitoring
//! - **SSL Acceptor Configuration**: Integration with OpenSSL for secure connections
//! - **Zero-Downtime Updates**: Hot reloading of certificates without service interruption
//!
//! # Certificate Types
//!
//! The module supports two main certificate input types:
//! - `PemFile`: Load certificates from existing PEM files on disk
//! - `PemAutoGenerated`: Automatically generate self-signed certificates for development
//!
//! # Architecture
//!
//! ```text
//! ┌───────────────────────┐    ┌───────────────────┐     ┌─────────────────┐
//! │ CertificateStorePaths │───▶│ CertificateBundle │───▶│   TlsProvider   │
//! └───────────────────────┘    └───────────────────┘     └─────────────────┘
//!              │                       │                       │
//!              ▼                       ▼                       ▼
//!         File Management        Certificate Loading      Automatic Reloading
//! ```

use std::{fmt::Debug, fs, path::Path, sync::Arc};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

use arc_swap::ArcSwap;
use openssl::{
    hash::MessageDigest,
    pkey::{Id, PKey, PKeyRef, Private},
    ssl::{SslAcceptor, SslCipherRef},
    x509::X509,
};

use crate::{
    configuration::{CertInputType, CertificateOptions},
    error::{DocumentDBError, Result},
    service::docdb_openssl,
};

#[path = "tls_state_dir.rs"]
mod state_dir;

use state_dir::{resolve_tls_paths, ENV_TLS_STATE_DIR};

/// Certificate file change monitoring interval in seconds
const CERT_FILES_CHANGE_WATCH_INTERVAL: u64 = 60;

/// Internal certificate store that manages certificate file paths.
///
/// The main difference between `CertificateStorePaths` and `CertificateOptions` is that
/// `CertificateStorePaths` will generate the keys if `cert_type` is
/// `CertInputType::PemAutoGenerated`.
pub struct CertificateStorePaths {
    certificate: String,
    private_key: String,
    ca: Option<String>,
}

impl CertificateStorePaths {
    /// Creates a new certificate store from the provided options.
    ///
    /// For `PemFile` type, validates that required file paths are provided.
    /// For `PemAutoGenerated` type, generates certificates if they don't exist.
    ///
    /// # Arguments
    ///
    /// * `certificate_options` - Configuration specifying certificate type and paths
    ///
    /// # Returns
    ///
    /// Returns a configured `CertificateStore` ready for use.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * Required file paths are missing for `PemFile` type
    /// * Certificate generation fails for auto-generation type
    /// * File system operations fail
    pub fn new(certificate_options: &CertificateOptions) -> Result<Self> {
        match certificate_options.cert_type {
            CertInputType::PemFile => {
                let certificate_path = certificate_options.file_path.as_ref().ok_or_else(|| {
                    DocumentDBError::internal_error("Certificate file path is required.".to_owned())
                })?;

                let private_key_path =
                    certificate_options.key_file_path.as_ref().ok_or_else(|| {
                        DocumentDBError::internal_error(
                            "Private key file path is required.".to_owned(),
                        )
                    })?;

                Ok(Self {
                    certificate: certificate_path.clone(),
                    private_key: private_key_path.clone(),
                    ca: certificate_options.ca_path.clone(),
                })
            }
            CertInputType::PemAutoGenerated => {
                let (cert_path, key_path) = resolve_tls_paths()?;

                // Reuse existing material when present. This is essential for
                // restart/upgrade scenarios (the gateway must come back up
                // with the same certificate so client trust pinning still
                // works) and matches the e2e-test reuse pattern.
                let cert_exists = Path::new(&cert_path).exists();
                let key_exists = Path::new(&key_path).exists();
                if cert_exists && key_exists {
                    tracing::info!(
                        "TLS auto-gen: reusing existing certificate at {cert_path} (set {ENV_TLS_STATE_DIR} to override)"
                    );
                } else {
                    tracing::info!(
                        "TLS auto-gen: generating self-signed certificate at {cert_path} (set {ENV_TLS_STATE_DIR} to override)"
                    );
                    docdb_openssl::generate_auth_keys(&key_path, &cert_path)?;
                    // openssl(1) honors the process umask when writing the
                    // key file, which on most systems leaves it world- or
                    // group-readable. Explicitly tighten to owner-only RW
                    // so the private key is not exposed even if the
                    // parent directory permissions were loosened by an
                    // admin or skipped by ensure_tls_state_dir (e.g.
                    // pre-existing dir we can't chmod).
                    #[cfg(unix)]
                    if let Err(e) =
                        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))
                    {
                        tracing::warn!(
                            "Failed to tighten permissions on generated private key {key_path}: {e}. \
                             Verify the file is not world- or group-readable."
                        );
                    }
                }

                Ok(Self {
                    certificate: cert_path,
                    private_key: key_path,
                    ca: None,
                })
            }
        }
    }
}

/// A bundle containing SSL/TLS certificate, private key, and CA chain.
///
/// This struct holds all the cryptographic materials needed for SSL/TLS operations,
/// including the server certificate, its private key, and any intermediate CA certificates.
#[derive(Debug)]
pub struct CertificateBundle {
    certificate: X509,
    private_key: PKey<Private>,
    ca_chain: Vec<X509>,
}

impl CertificateBundle {
    /// Creates a certificate bundle by loading certificates from the certificate store.
    ///
    /// This async function reads certificate files from disk and parses them into
    /// the appropriate OpenSSL structures.
    ///
    /// # Arguments
    ///
    /// * `certificate_store` - The certificate store containing file paths
    ///
    /// # Returns
    ///
    /// Returns a `CertificateBundle` on success.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * Certificate or private key files cannot be read
    /// * Certificate parsing fails (invalid PEM format)
    /// * CA chain file is specified but cannot be read or parsed
    pub async fn from_cert_store(certificate_store: &CertificateStorePaths) -> Result<Self> {
        let cert_bytes = tokio::fs::read(&certificate_store.certificate).await?;
        let certificate = X509::from_pem(&cert_bytes)?;

        let pkey_bytes = tokio::fs::read(&certificate_store.private_key).await?;
        let pkey = PKey::private_key_from_pem(&pkey_bytes)?;

        let ca_chain = if let Some(ca_path) = &certificate_store.ca {
            let ca_chain_pem = tokio::fs::read(ca_path).await?;
            X509::stack_from_pem(&ca_chain_pem)?
        } else {
            Vec::new()
        };

        Ok(Self {
            certificate,
            private_key: pkey,
            ca_chain,
        })
    }

    /// Returns a clone of the server certificate.
    ///
    /// Note: The OpenSSL crate doesn't provide borrowing for X509 certificates,
    /// so this method returns a clone.
    pub fn certificate(&self) -> X509 {
        self.certificate.clone()
    }

    /// Returns a reference to the private key.
    pub fn private_key(&self) -> &PKeyRef<Private> {
        self.private_key.as_ref()
    }

    /// Returns a slice of the CA certificate chain.
    pub fn ca_chain(&self) -> &[X509] {
        &self.ca_chain
    }
}

/// A provider that manages SSL/TLS certificates with automatic reloading.
///
/// This struct handles certificate loading, monitoring for file changes, and
/// automatic reloading when certificate files are updated on disk. It runs
/// a background task that periodically checks for certificate file modifications
/// every 60 seconds.
///
/// The provider uses `ArcSwap` to allow atomic updates of the SSL acceptor
/// without blocking ongoing connections.
#[derive(Clone)]
pub struct TlsProvider {
    tls_acceptor: Arc<ArcSwap<SslAcceptor>>,
    ciphersuite_mapping: Option<fn(Option<&str>) -> i32>,
    certificate_bundle: Arc<ArcSwap<CertificateBundle>>,
}

impl Debug for TlsProvider {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TlsProvider")
            // Don't print the actual acceptor for security reasons
            .field("tls_acceptor", &"<SslAcceptor>")
            .field("ciphersuite_mapping", &self.ciphersuite_mapping)
            .field("certificate_bundle", &self.certificate_bundle)
            .finish()
    }
}

impl TlsProvider {
    /// Creates a new TLS provider with automatic certificate reloading.
    ///
    /// This function sets up the initial certificate bundle and starts a background
    /// task that monitors certificate files for changes and reloads them automatically.
    /// The background task runs every 60 seconds to check for certificate updates.
    ///
    /// # Arguments
    ///
    /// * `certificate_options` - Configuration specifying certificate type and paths
    /// * `acceptor_builder` - Optional custom function for creating SSL acceptor builders
    ///
    /// # Returns
    ///
    /// Returns a `TlsProvider` that manages SSL/TLS acceptors and automatic reloading.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * Initial certificate loading fails
    /// * Certificate store creation fails
    /// * File modification time cannot be determined
    /// * TLS acceptor creation fails
    pub async fn new(
        certificate_options: &CertificateOptions,
        acceptor_builder: Option<docdb_openssl::AcceptorBuilderFn>,
        ciphersuite_mapping: Option<fn(Option<&str>) -> i32>,
    ) -> Result<Self> {
        let cert_store = CertificateStorePaths::new(certificate_options)?;
        let cert_bundle = CertificateBundle::from_cert_store(&cert_store).await?;

        let tls_builder = docdb_openssl::create_tls_acceptor(&cert_bundle, acceptor_builder)?;
        let tls_acceptor_arc = Arc::new(ArcSwap::from_pointee(tls_builder.build()));

        let mut last_cert_modified = Self::get_modified_time(&cert_store.certificate).await?;
        let mut last_key_modified = Self::get_modified_time(&cert_store.private_key).await?;
        let tls_acceptor_arc_clone = Arc::clone(&tls_acceptor_arc);

        let certificate_bundle = Arc::new(ArcSwap::from_pointee(
            CertificateBundle::from_cert_store(&cert_store).await?,
        ));

        let certificate_bundle_clone = Arc::clone(&certificate_bundle);

        tokio::spawn(async move {
            let mut certs_changed_watch = tokio::time::interval(tokio::time::Duration::from_secs(
                CERT_FILES_CHANGE_WATCH_INTERVAL,
            ));

            loop {
                certs_changed_watch.tick().await;
                if let (Ok(cert_m), Ok(key_m)) = (
                    Self::get_modified_time(&cert_store.certificate).await,
                    Self::get_modified_time(&cert_store.private_key).await,
                ) {
                    if !(cert_m > last_cert_modified || key_m > last_key_modified) {
                        continue;
                    }

                    tracing::info!("Reloading TLS certificates since they have been modified.");

                    match CertificateBundle::from_cert_store(&cert_store).await {
                        Ok(new_bundle) => {
                            match docdb_openssl::create_tls_acceptor(&new_bundle, acceptor_builder)
                            {
                                Ok(new_tls_acceptor) => {
                                    tls_acceptor_arc_clone
                                        .store(Arc::new(new_tls_acceptor.build()));
                                    last_cert_modified = cert_m;
                                    last_key_modified = key_m;
                                    certificate_bundle_clone.store(Arc::new(new_bundle));

                                    tracing::info!("TLS certificates reloaded.");
                                }
                                Err(e) => tracing::error!("Failed to create TLS acceptor: {e:?}."),
                            }
                        }
                        Err(e) => tracing::error!("Failed to reload TLS certificates: {e:?}."),
                    }
                }
            }
        });

        Ok(Self {
            tls_acceptor: tls_acceptor_arc,
            ciphersuite_mapping,
            certificate_bundle,
        })
    }

    /// Returns the current TLS acceptor.
    #[must_use]
    pub fn tls_acceptor(&self) -> Arc<SslAcceptor> {
        Arc::clone(&self.tls_acceptor.load())
    }

    /// Maps an SSL ciphersuite to a numeric identifier using the configured cipher mapping function.
    ///
    /// This method converts an SSL cipher reference to a numeric code for telemetry,
    /// logging, or other identification purposes. If no cipher mapping function was
    /// provided during construction, returns 0 as a default value.
    ///
    /// # Arguments
    ///
    /// * `ciphersuite` - An optional reference to an SSL cipher from the current connection.
    ///   This is typically obtained from `SslRef::current_cipher()` on an active SSL connection.
    ///
    /// # Returns
    ///
    /// Returns an `i32` representing the numeric identifier for the cipher:
    /// * If a cipher mapping function is configured and a cipher is provided, returns the mapped value
    /// * If no cipher mapping function is configured, returns 0
    /// * If the cipher is `None`, the mapping function receives `None` and may return a default value
    #[must_use]
    pub fn ciphersuite_to_i32(&self, ciphersuite: Option<&SslCipherRef>) -> i32 {
        self.ciphersuite_mapping
            .map_or(0, |mapping| mapping(ciphersuite.map(SslCipherRef::name)))
    }

    pub fn is_valid_certificate(&self) -> bool {
        let bundle = self.certificate_bundle.load();
        let Ok(pubkey) = bundle.certificate.public_key() else {
            tracing::error!("Detected invalid certificate. Failed to obtain public key.");
            return false;
        };

        let is_valid = bundle.private_key.id() == Id::RSA
            && pubkey.id() == Id::RSA
            && pubkey.public_eq(&bundle.private_key);

        if !is_valid {
            match Self::sha1_thumbprint(&bundle.certificate) {
                Ok(thumbprint) => {
                    tracing::error!("Detected invalid certificate. Thumbprint: {thumbprint:?}");
                }
                Err(e) => tracing::error!(
                    "Detected invalid certificate. Failed to generate certificate. Thumbprint: {e:?}"
                ),
            }
        }

        is_valid
    }

    fn sha1_thumbprint(cert: &X509) -> Result<String> {
        let der = cert.to_der()?;
        let digest = openssl::hash::hash(MessageDigest::sha1(), &der)?;
        Ok(hex::encode_upper(digest))
    }

    async fn get_modified_time(path: &str) -> Result<std::time::SystemTime> {
        let metadata = tokio::fs::metadata(path).await?;
        Ok(metadata.modified()?)
    }
}

#[cfg(test)]
#[path = "tls_tests.rs"]
mod tests;
