//! Shared client config types (endpoint/auth/retry) for UltraCloud SDKs.
//!
//! Lightweight, type-only helpers to keep SDK crates consistent without
//! forcing a unified SDK dependency tree.

use backoff::ExponentialBackoffBuilder;
use std::time::Duration;
use thiserror::Error;
use tonic::codegen::InterceptedService;
use tonic::service::Interceptor;
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};

/// Errors produced by client-common helpers.
#[derive(Debug, Error)]
pub enum ClientError {
    #[error("invalid endpoint: {0}")]
    InvalidEndpoint(String),
    #[error("TLS configuration error: {0}")]
    TlsConfig(String),
    #[error("transport error: {0}")]
    Transport(#[from] tonic::transport::Error),
    #[error("auth error: {0}")]
    Auth(String),
}

/// Endpoint configuration (URI + timeout + optional TLS domain/CA).
#[derive(Debug, Clone)]
pub struct EndpointConfig {
    pub uri: String,
    pub timeout: Duration,
    pub tls: Option<TlsConfig>,
}

impl EndpointConfig {
    pub fn new(uri: impl Into<String>) -> Self {
        Self {
            uri: uri.into(),
            timeout: Duration::from_millis(5_000),
            tls: None,
        }
    }

    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn with_tls(mut self, tls: TlsConfig) -> Self {
        self.tls = Some(tls);
        self
    }

    /// Build a tonic Endpoint with timeout/TLS applied.
    pub fn build_endpoint(&self) -> Result<Endpoint, ClientError> {
        let mut ep = Endpoint::from_shared(self.uri.clone())
            .map_err(|e| ClientError::InvalidEndpoint(e.to_string()))?
            .timeout(self.timeout);

        if let Some(tls) = &self.tls {
            let mut cfg = ClientTlsConfig::new();
            if let Some(domain) = &tls.domain {
                cfg = cfg.domain_name(domain.clone());
            }
            if let Some(ca_cert) = &tls.ca_cert_pem {
                cfg = cfg.ca_certificate(tonic::transport::Certificate::from_pem(ca_cert.clone()));
            }
            if let (Some(cert), Some(key)) = (&tls.client_cert_pem, &tls.client_key_pem) {
                cfg = cfg.identity(tonic::transport::Identity::from_pem(
                    cert.clone(),
                    key.clone(),
                ));
            }
            ep = ep
                .tls_config(cfg)
                .map_err(|e| ClientError::TlsConfig(e.to_string()))?;
        }

        Ok(ep)
    }
}

/// TLS configuration inputs.
#[derive(Debug, Clone, Default)]
pub struct TlsConfig {
    pub domain: Option<String>,
    pub ca_cert_pem: Option<String>,
    pub client_cert_pem: Option<String>,
    pub client_key_pem: Option<String>,
}

/// Auth configuration for interceptors.
#[derive(Debug, Clone)]
pub enum AuthConfig {
    None,
    Bearer { token: String },
    AccessKey { id: String, secret: String },
}

impl AuthConfig {
    pub fn bearer(token: impl Into<String>) -> Self {
        Self::Bearer {
            token: token.into(),
        }
    }
}

/// Retry/backoff configuration.
#[derive(Debug, Clone)]
pub struct RetryConfig {
    pub max_retries: u32,
    pub base_delay: Duration,
    pub max_delay: Duration,
    pub jitter: bool,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: 3,
            base_delay: Duration::from_millis(100),
            max_delay: Duration::from_secs(2),
            jitter: true,
        }
    }
}

impl RetryConfig {
    pub fn backoff(&self) -> backoff::ExponentialBackoff {
        let mut builder = ExponentialBackoffBuilder::new();
        builder
            .with_initial_interval(self.base_delay)
            .with_max_interval(self.max_delay)
            .with_max_elapsed_time(None);
        if !self.jitter {
            builder.with_randomization_factor(0.0);
        }
        builder.build()
    }
}

/// Build a channel with optional auth interceptor.
pub async fn build_channel(
    endpoint: &EndpointConfig,
    auth: &AuthConfig,
) -> Result<Channel, ClientError> {
    let ep = endpoint.build_endpoint()?;
    let channel = ep.connect().await?;

    match auth {
        AuthConfig::None => Ok(channel),
        AuthConfig::Bearer { .. } | AuthConfig::AccessKey { .. } => Ok(channel),
    }
}

/// Interceptor that injects auth headers; used by clients that need request metadata.
#[derive(Clone)]
pub struct AuthInterceptor(AuthConfig);

impl Interceptor for AuthInterceptor {
    fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
        match &self.0 {
            AuthConfig::None => {}
            AuthConfig::Bearer { token } => {
                req.metadata_mut().insert(
                    "authorization",
                    format!("Bearer {}", token).parse().unwrap(),
                );
            }
            AuthConfig::AccessKey { id, secret } => {
                req.metadata_mut().insert("x-api-key", id.parse().unwrap());
                req.metadata_mut()
                    .insert("x-api-secret", secret.parse().unwrap());
            }
        }
        Ok(req)
    }
}

/// Create an auth interceptor from AuthConfig (for tonic clients that need it).
pub fn auth_interceptor(auth: &AuthConfig) -> Option<AuthInterceptor> {
    match auth {
        AuthConfig::None => None,
        _ => Some(AuthInterceptor(auth.clone())),
    }
}

/// Helper to wrap a tonic client with an interceptor when auth is provided.
pub fn with_auth(
    channel: Channel,
    auth: &AuthConfig,
) -> InterceptedService<Channel, AuthInterceptor> {
    let interceptor = auth_interceptor(auth).unwrap_or(AuthInterceptor(AuthConfig::None));
    InterceptedService::new(channel, interceptor)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn retry_config_builds_backoff() {
        let cfg = RetryConfig::default();
        let backoff = cfg.backoff();
        assert!(backoff.initial_interval == cfg.base_delay);
    }

    #[tokio::test]
    async fn endpoint_builds_without_tls() {
        let ep = EndpointConfig::new("http://localhost:50051");
        let built = ep.build_endpoint().unwrap();
        assert!(built.uri().to_string().contains("localhost"));
    }
}
