//! Connection options shared by [`crate::client::GatewayClient`] and
//! [`crate::galaxy::GalaxyClient`]. Build with [`ClientOptions::new`] and a
//! chain of `with_*` setters; the `Debug` impl redacts the API key.

use std::fmt;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;

use tonic::transport::{Certificate, ClientTlsConfig};

use crate::auth::ApiKey;
use crate::error::Error;

const DEFAULT_MAX_GRPC_MESSAGE_BYTES: usize = 16 * 1024 * 1024;

/// Configuration for connecting to a gateway endpoint.
///
/// Defaults are 10s connect timeout, 30s call timeout, no streaming timeout,
/// and plaintext (h2c) transport. Set [`ClientOptions::with_plaintext`] to
/// `false` and supply [`ClientOptions::with_ca_file`] / a server-name override
/// for TLS deployments.
#[derive(Clone)]
pub struct ClientOptions {
    endpoint: String,
    api_key: Option<ApiKey>,
    plaintext: bool,
    ca_file: Option<PathBuf>,
    require_certificate_validation: bool,
    server_name_override: Option<String>,
    connect_timeout: Duration,
    call_timeout: Duration,
    stream_timeout: Option<Duration>,
    max_grpc_message_bytes: usize,
}

impl ClientOptions {
    /// Build options for the supplied gateway endpoint URL (for example,
    /// `http://127.0.0.1:5000`). Other settings take their defaults.
    pub fn new(endpoint: impl Into<String>) -> Self {
        Self {
            endpoint: endpoint.into(),
            api_key: None,
            plaintext: true,
            ca_file: None,
            require_certificate_validation: false,
            server_name_override: None,
            connect_timeout: Duration::from_secs(10),
            call_timeout: Duration::from_secs(30),
            stream_timeout: None,
            max_grpc_message_bytes: DEFAULT_MAX_GRPC_MESSAGE_BYTES,
        }
    }

    /// Attach an API key. The key flows through [`crate::auth::AuthInterceptor`]
    /// as the Bearer token on every request.
    pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
        self.api_key = Some(api_key);
        self
    }

    /// Toggle h2c (plaintext) vs TLS. `true` (the default) skips the TLS
    /// handshake and is suitable for loopback development.
    pub fn with_plaintext(mut self, plaintext: bool) -> Self {
        self.plaintext = plaintext;
        self
    }

    /// Trust roots PEM bundle for TLS connections. Ignored when
    /// `plaintext` is `true`.
    pub fn with_ca_file(mut self, ca_file: impl Into<PathBuf>) -> Self {
        self.ca_file = Some(ca_file.into());
        self
    }

    /// Require TLS certificate verification even without a pinned CA. Default
    /// false. Setting a CA file always verifies against that CA.
    ///
    /// Note for Rust: tonic 0.13's `ClientTlsConfig` exposes no hook for a
    /// custom rustls verifier, so the Rust client cannot accept an *arbitrary*
    /// self-signed certificate the way the other clients do. With the default
    /// (false) and no pinned CA, [`crate::client::GatewayClient::connect`]
    /// rejects the TLS connection and asks for a CA file. There are two
    /// supported TLS paths:
    ///
    /// - Pin the gateway certificate with [`ClientOptions::with_ca_file`] (the
    ///   lenient pin-only path; works for a self-signed gateway cert).
    /// - Set this `true` to verify against the operating system's trust roots
    ///   (`tls-native-roots`). This only succeeds for a certificate that chains
    ///   to a root the host already trusts, so it is for gateways fronted by a
    ///   publicly- or enterprise-CA-issued certificate, not a bare self-signed
    ///   one.
    pub fn with_require_certificate_validation(mut self, require: bool) -> Self {
        self.require_certificate_validation = require;
        self
    }

    /// Override the SNI/server name used during the TLS handshake. Useful
    /// when the dial-target host name does not match the certificate.
    pub fn with_server_name_override(mut self, server_name_override: impl Into<String>) -> Self {
        self.server_name_override = Some(server_name_override.into());
        self
    }

    /// Maximum time the transport waits for the initial TCP/TLS connection.
    pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
        self.connect_timeout = connect_timeout;
        self
    }

    /// Per-call deadline applied to every unary RPC. Streaming RPCs use
    /// [`ClientOptions::with_stream_timeout`] instead.
    pub fn with_call_timeout(mut self, call_timeout: Duration) -> Self {
        self.call_timeout = call_timeout;
        self
    }

    /// Optional deadline applied to streaming RPCs (for example,
    /// `StreamEvents`). Without a stream timeout the stream lives until the
    /// caller drops it or the server closes it.
    pub fn with_stream_timeout(mut self, stream_timeout: Duration) -> Self {
        self.stream_timeout = Some(stream_timeout);
        self
    }

    /// Maximum encoded/decoded gRPC message size in bytes (default 16 MiB).
    pub fn with_max_grpc_message_bytes(mut self, max_grpc_message_bytes: usize) -> Self {
        self.max_grpc_message_bytes = max_grpc_message_bytes;
        self
    }

    /// Configured endpoint URL.
    pub fn endpoint(&self) -> &str {
        &self.endpoint
    }

    /// Configured API key, if any.
    pub fn api_key(&self) -> Option<&ApiKey> {
        self.api_key.as_ref()
    }

    /// Whether the transport runs in plaintext (h2c) mode.
    pub fn plaintext(&self) -> bool {
        self.plaintext
    }

    /// Optional CA bundle path used to validate the server certificate.
    pub fn ca_file(&self) -> Option<&PathBuf> {
        self.ca_file.as_ref()
    }

    /// Whether TLS certificate verification is required even without a pinned
    /// CA. See [`ClientOptions::with_require_certificate_validation`].
    pub fn require_certificate_validation(&self) -> bool {
        self.require_certificate_validation
    }

    /// Optional SNI / server-name override for TLS handshakes.
    pub fn server_name_override(&self) -> Option<&str> {
        self.server_name_override.as_deref()
    }

    /// Connect timeout used during transport setup.
    pub fn connect_timeout(&self) -> Duration {
        self.connect_timeout
    }

    /// Per-call timeout for unary RPCs.
    pub fn call_timeout(&self) -> Duration {
        self.call_timeout
    }

    /// Optional per-call timeout for streaming RPCs.
    pub fn stream_timeout(&self) -> Option<Duration> {
        self.stream_timeout
    }

    /// Configured maximum encoded/decoded gRPC message size in bytes.
    pub fn max_grpc_message_bytes(&self) -> usize {
        self.max_grpc_message_bytes
    }
}

/// Where the TLS handshake gets its trust anchors for a given set of options.
/// Computed by [`tls_trust_decision`] and applied by [`build_tls_config`];
/// split out so the trust posture is unit-testable without a live handshake.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TlsTrustDecision {
    /// Plaintext transport — no TLS, no trust anchors.
    None,
    /// Validate against the CA pinned with [`ClientOptions::with_ca_file`].
    PinnedCa,
    /// Validate against the operating system's trust roots
    /// (`require_certificate_validation == true`, no pinned CA).
    SystemRoots,
    /// Reject up front: TLS requested with neither a pinned CA nor strict
    /// verification (the Rust pin-only lenient default).
    RejectNoCa,
}

/// Decide the TLS trust posture from `options` without touching the filesystem
/// or the network.
pub(crate) fn tls_trust_decision(options: &ClientOptions) -> TlsTrustDecision {
    if options.plaintext() {
        TlsTrustDecision::None
    } else if options.ca_file().is_some() {
        TlsTrustDecision::PinnedCa
    } else if options.require_certificate_validation() {
        TlsTrustDecision::SystemRoots
    } else {
        TlsTrustDecision::RejectNoCa
    }
}

/// Build the [`ClientTlsConfig`] for a non-plaintext connection described by
/// `options`, applying the lenient-default guard that is the **Rust
/// pin-only exception**.
///
/// Returns `Ok(None)` when `options.plaintext()` is `true` (no TLS needed).
/// Returns `Ok(Some(tls))` when a valid TLS config can be assembled — either
/// pinned to the CA from [`ClientOptions::with_ca_file`], or, when
/// `require_certificate_validation` is set with no pinned CA, verifying against
/// the operating system's trust roots (`tls-native-roots`).
/// Returns `Err(Error::InvalidEndpoint)` when TLS is requested but no pinned
/// CA was provided and `require_certificate_validation` is `false`.
///
/// # Why the no-CA guard exists
///
/// `tonic` 0.13's `ClientTlsConfig` builds its rustls verifier inside a
/// crate-private connector and exposes no hook for a custom
/// `ServerCertVerifier`. The Rust client therefore cannot accept an *arbitrary*
/// self-signed certificate the way the other language clients do. Rather than
/// silently falling back to a verifier with no trust anchors (which rejects
/// every certificate with a confusing handshake error), the lenient default
/// rejects the configuration early with an actionable error. The strict opt-in
/// instead loads the system trust roots so a certificate chaining to an
/// already-trusted root validates.
pub(crate) fn build_tls_config(options: &ClientOptions) -> Result<Option<ClientTlsConfig>, Error> {
    let decision = tls_trust_decision(options);
    if decision == TlsTrustDecision::None {
        return Ok(None);
    }

    let mut tls = ClientTlsConfig::new();
    if let Some(server_name) = options.server_name_override() {
        tls = tls.domain_name(server_name.to_owned());
    }
    match decision {
        TlsTrustDecision::PinnedCa => {
            let ca_file = options.ca_file().expect("PinnedCa implies a CA file");
            let certificate = fs::read(ca_file).map_err(|source| Error::InvalidEndpoint {
                endpoint: options.endpoint().to_owned(),
                detail: format!("failed to read CA file {}: {source}", ca_file.display()),
            })?;
            tls = tls.ca_certificate(Certificate::from_pem(certificate));
        }
        TlsTrustDecision::SystemRoots => {
            // Strict opt-in with no pinned CA: verify against the OS trust
            // store. Without this the bare `ClientTlsConfig` carries zero
            // trust anchors and rejects every certificate, so the documented
            // "verify against the system trust roots" behaviour would be
            // unreachable. Only a certificate chaining to an already-trusted
            // root validates — a bare self-signed gateway cert still needs
            // `with_ca_file`.
            tls = tls.with_native_roots();
        }
        TlsTrustDecision::RejectNoCa => {
            // Lenient-default fallback (Rust pin-only exception): the Rust
            // client cannot accept an arbitrary self-signed cert. Pin the
            // gateway's CA, or opt into strict verification against the
            // system trust roots.
            //
            // Note: a server-name override affects SNI (the hostname sent in
            // the TLS ClientHello) but does NOT pin trust.
            return Err(Error::InvalidEndpoint {
                endpoint: options.endpoint().to_owned(),
                detail: "TLS requested without a pinned CA. The Rust client cannot accept an \
                         arbitrary self-signed certificate (tonic 0.13 exposes no custom \
                         rustls verifier). Pin the gateway certificate with \
                         ClientOptions::with_ca_file, or call \
                         ClientOptions::with_require_certificate_validation(true) to verify \
                         against the system trust roots. Note: a server-name override \
                         affects SNI but does not pin trust."
                    .to_owned(),
            });
        }
        TlsTrustDecision::None => unreachable!("handled above"),
    }
    Ok(Some(tls))
}

impl Default for ClientOptions {
    fn default() -> Self {
        Self::new("http://127.0.0.1:5000")
    }
}

impl fmt::Debug for ClientOptions {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ClientOptions")
            .field("endpoint", &self.endpoint)
            .field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
            .field("plaintext", &self.plaintext)
            .field("ca_file", &self.ca_file)
            .field(
                "require_certificate_validation",
                &self.require_certificate_validation,
            )
            .field("server_name_override", &self.server_name_override)
            .field("connect_timeout", &self.connect_timeout)
            .field("call_timeout", &self.call_timeout)
            .field("stream_timeout", &self.stream_timeout)
            .field("max_grpc_message_bytes", &self.max_grpc_message_bytes)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::ClientOptions;
    use crate::auth::ApiKey;

    use super::{build_tls_config, tls_trust_decision, TlsTrustDecision};

    #[test]
    fn debug_redacts_api_key() {
        let options =
            ClientOptions::new("http://localhost:5000").with_api_key(ApiKey::new("mxgw_secret"));

        let debug = format!("{options:?}");

        assert!(debug.contains("<redacted>"));
        assert!(!debug.contains("mxgw_secret"));
    }

    #[test]
    fn plaintext_needs_no_tls() {
        let options = ClientOptions::new("http://127.0.0.1:5000").with_plaintext(true);
        assert_eq!(tls_trust_decision(&options), TlsTrustDecision::None);
        assert!(build_tls_config(&options).unwrap().is_none());
    }

    #[test]
    fn pinned_ca_uses_pinned_trust() {
        let options = ClientOptions::new("https://127.0.0.1:5000")
            .with_plaintext(false)
            .with_ca_file("/some/ca.pem");
        assert_eq!(tls_trust_decision(&options), TlsTrustDecision::PinnedCa);
    }

    #[test]
    fn strict_without_ca_uses_system_roots() {
        // Regression for Client.Rust-031: strict verification with no pinned CA
        // must verify against the system trust roots, not produce a config with
        // zero trust anchors. The trust decision proves roots are consulted; the
        // build then succeeds (no no-CA guard error) and emits a config.
        let options = ClientOptions::new("https://127.0.0.1:5000")
            .with_plaintext(false)
            .with_require_certificate_validation(true);

        assert_eq!(
            tls_trust_decision(&options),
            TlsTrustDecision::SystemRoots,
            "strict-no-CA must request the system trust roots"
        );
        assert!(
            build_tls_config(&options).unwrap().is_some(),
            "strict-no-CA must build a usable TLS config"
        );
    }

    #[test]
    fn lenient_without_ca_is_rejected() {
        let options = ClientOptions::new("https://127.0.0.1:5000").with_plaintext(false);
        assert_eq!(tls_trust_decision(&options), TlsTrustDecision::RejectNoCa);
        assert!(build_tls_config(&options).is_err());
    }
}
