use std::{sync::Arc, time::Duration};

use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD};
use chrono::Utc;
use dashmap::DashMap;
use tokio::sync::{Mutex, RwLock};
use zeroize::Zeroizing;

use super::{
    cache::{CACHE_TTL_PERCENTAGE, DEFAULT_MAX_CACHE_ENTRIES, SecretCache, VaultResponse},
    validation::{validate_vault_addr, validate_vault_secret_name},
};
use crate::secrets_manager::{SecretsBackend, SecretsError};

/// Fraction of the token TTL after which the token should be proactively renewed.
/// At 80% of TTL elapsed, renewal is triggered before the token expires.
// Reason: referenced in the renew_token() doc comment; the actual scheduling logic
// uses this constant to avoid a magic literal at the call site.
#[allow(dead_code)] // Reason: field kept for API completeness; may be used in future features
const TOKEN_RENEWAL_THRESHOLD: f64 = 0.8;

const VAULT_API_VERSION: &str = "v1";

/// Vault HTTP request timeout.
const VAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);

/// Maximum allowed size for any Vault HTTP response body.
///
/// Responses larger than 1 `MiB` are rejected before JSON parsing to prevent
/// memory exhaustion from oversized payloads (malicious or misconfigured Vault).
const MAX_VAULT_RESPONSE_BYTES: usize = 1024 * 1024; // 1 MiB

/// Read a Vault HTTP response, enforcing `MAX_VAULT_RESPONSE_BYTES`.
///
/// Reads the response body as raw bytes, rejects it if it exceeds the limit,
/// then deserializes to `T` with `serde_json`. This prevents memory exhaustion
/// from oversized payloads returned by a malicious or misconfigured Vault server.
async fn read_vault_response<T>(response: reqwest::Response) -> Result<T, SecretsError>
where
    T: serde::de::DeserializeOwned,
{
    let bytes = response.bytes().await.map_err(|e| {
        SecretsError::ConnectionError(format!("Failed to read Vault response body: {e}"))
    })?;
    if bytes.len() > MAX_VAULT_RESPONSE_BYTES {
        return Err(SecretsError::BackendError(format!(
            "Vault response too large: {} bytes (max: {MAX_VAULT_RESPONSE_BYTES})",
            bytes.len()
        )));
    }
    serde_json::from_slice(&bytes)
        .map_err(|e| SecretsError::BackendError(format!("Vault response parse error: {e}")))
}

/// Build a shared `reqwest::Client` for Vault HTTP calls.
///
/// The client is created once per `VaultBackend` instance and reused across
/// all requests to avoid per-call TLS handshake overhead.
fn build_http_client(tls_verify: bool) -> Result<reqwest::Client, SecretsError> {
    reqwest::Client::builder()
        .danger_accept_invalid_certs(!tls_verify)
        .timeout(VAULT_REQUEST_TIMEOUT)
        .build()
        .map_err(|e| SecretsError::ConnectionError(format!("HTTP client error: {e}")))
}

/// Secrets backend for `HashiCorp` Vault
///
/// Provides dynamic secrets, credential rotation, and lease management
/// via the `HashiCorp` Vault HTTP API.
///
/// # Example
/// ```no_run
/// // Requires: live HashiCorp Vault server.
/// # async fn example() -> Result<(), fraiseql_secrets::secrets_manager::SecretsError> {
/// use fraiseql_secrets::secrets_manager::VaultBackend;
/// use fraiseql_secrets::secrets_manager::SecretsBackend;
/// let vault = VaultBackend::new(
///     "https://vault.example.com:8200",
///     "s.xxxxxxxxxxxxxxxx"
/// )?;
/// let secret = vault.get_secret("database/creds/fraiseql").await?;
/// let (secret, expiry) = vault.get_secret_with_expiry("database/creds/fraiseql").await?;
/// # Ok(())
/// # }
/// ```
///
/// # Features
/// - Dynamic database credentials from configured roles
/// - Automatic lease tracking and renewal
/// - Generic secret retrieval from KV2 engine
/// - Encryption via Transit engine
/// - Audit logging for all operations
/// - Connection pooling and retry logic
///
/// # Token Renewal
///
/// When the Vault token is obtained via `AppRole` login (`with_approle`), the token carries
/// a TTL. To avoid using an expired token, callers should check `token_needs_renewal()` and
/// call `renew_token()` proactively at `TOKEN_RENEWAL_THRESHOLD` (80%) of TTL elapsed.
///
/// A background task should be spawned to call `renew_token()` periodically; for example:
/// ```rust,ignore
/// let vault = Arc::new(VaultBackend::with_approle(addr, role_id, secret_id).await?);
/// let vault_clone = Arc::clone(&vault);
/// tokio::spawn(async move {
///     loop {
///         tokio::time::sleep(Duration::from_secs(60)).await;
///         if vault_clone.token_needs_renewal() {
///             if let Err(e) = vault_clone.renew_token().await { /* log */ }
///         }
///     }
/// });
/// ```
///
/// # Configuration
/// ```toml
/// [secrets.vault]
/// addr = "https://vault.example.com:8200"
/// token = "s.xxxxxxxxxxxxxxxx"  # From environment or secrets manager
/// namespace = "fraiseql/prod"    # Optional, for Enterprise
/// tls_verify = true              # Verify TLS certificates
/// ```
pub struct VaultBackend {
    addr:              String,
    token:             Zeroizing<String>,
    namespace:         Option<String>,
    tls_verify:        bool,
    /// Shared HTTP client — built once to reuse TLS sessions across requests.
    client:            reqwest::Client,
    cache:             Arc<RwLock<SecretCache>>,
    /// Per-secret rotation locks.
    ///
    /// Held during the invalidate→fetch sequence in `rotate_secret` and during
    /// cache-miss fetches in `get_secret_with_expiry`, preventing the classic
    /// thundering-herd / stale-cache race: a concurrent reader cannot load the
    /// pre-rotation value into cache while rotation is in progress.
    rotation_locks:    Arc<DashMap<String, Arc<Mutex<()>>>>,
    /// When the current token was obtained (for renewal tracking).
    /// `None` when using a static long-lived token.
    token_obtained_at: Option<chrono::DateTime<Utc>>,
    /// Token TTL as reported by Vault at login time (seconds).
    /// `None` when using a static long-lived token.
    token_ttl_secs:    Option<i64>,
}

impl std::fmt::Debug for VaultBackend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // SECURITY: token is omitted from debug output to prevent accidental
        // exposure in logs, tracing spans, or error messages.
        f.debug_struct("VaultBackend")
            .field("addr", &self.addr)
            .field("token", &"[REDACTED]")
            .field("namespace", &self.namespace)
            .field("tls_verify", &self.tls_verify)
            .finish_non_exhaustive()
    }
}

impl Clone for VaultBackend {
    fn clone(&self) -> Self {
        VaultBackend {
            addr:              self.addr.clone(),
            token:             Zeroizing::new((*self.token).clone()),
            namespace:         self.namespace.clone(),
            tls_verify:        self.tls_verify,
            client:            self.client.clone(),
            cache:             Arc::clone(&self.cache),
            rotation_locks:    Arc::clone(&self.rotation_locks),
            token_obtained_at: self.token_obtained_at,
            token_ttl_secs:    self.token_ttl_secs,
        }
    }
}

#[async_trait::async_trait]
impl SecretsBackend for VaultBackend {
    fn name(&self) -> &'static str {
        "vault"
    }

    async fn health_check(&self) -> Result<(), SecretsError> {
        // GET /v1/sys/health is a lightweight unauthenticated endpoint
        let url = format!("{}/{VAULT_API_VERSION}/sys/health", self.addr);
        let resp = self
            .client
            .get(&url)
            .send()
            .await
            .map_err(|e| SecretsError::ConnectionError(format!("Vault unreachable: {e}")))?;
        if resp.status().is_success() {
            Ok(())
        } else {
            Err(SecretsError::ConnectionError(format!("Vault unhealthy: {}", resp.status())))
        }
    }

    async fn get_secret(&self, name: &str) -> Result<String, SecretsError> {
        validate_vault_secret_name(name)?;

        let (secret, _) = self.get_secret_with_expiry(name).await?;
        Ok(secret)
    }

    async fn get_secret_with_expiry(
        &self,
        name: &str,
    ) -> Result<(String, chrono::DateTime<Utc>), SecretsError> {
        validate_vault_secret_name(name)?;

        // Check cache first (outside the rotation lock for hot-path performance).
        let cache = self.cache.read().await;
        if let Some((cached_value, cached_expiry)) = cache.get_with_expiry(name).await {
            return Ok((cached_value, cached_expiry));
        }
        drop(cache);

        // S47: acquire the per-secret rotation lock before fetching from Vault.
        // This prevents a concurrent rotate_secret from placing a stale value into
        // the cache immediately after the rotation's own cache write.
        self.with_secret_lock(name, || async {
            // Re-check the cache under the lock — rotation may have populated it
            // while we were waiting.
            let cache = self.cache.read().await;
            if let Some((cached_value, cached_expiry)) = cache.get_with_expiry(name).await {
                return Ok((cached_value, cached_expiry));
            }
            drop(cache);

            // Fetch from Vault
            let response = self.fetch_secret(name).await?;

            // Calculate expiry: now + lease_duration
            let expiry = Utc::now() + chrono::Duration::seconds(response.lease_duration);
            // Scale lease_duration by CACHE_TTL_PERCENTAGE (0.8) using integer arithmetic.
            #[allow(clippy::cast_possible_truncation)]
            // Reason: CACHE_TTL_PERCENTAGE * 100.0 is 80.0, fits in i64
            let cache_ttl_secs =
                response.lease_duration.saturating_mul((CACHE_TTL_PERCENTAGE * 100.0) as i64) / 100;
            let cache_expiry = Utc::now() + chrono::Duration::seconds(cache_ttl_secs);

            // Extract secret from response data
            let secret_str = Self::extract_secret_from_response(&response, name)?;

            // Store in cache
            let cache = self.cache.read().await;
            cache.set(name.to_string(), secret_str.clone(), cache_expiry).await;

            Ok((secret_str, expiry))
        })
        .await
    }

    async fn rotate_secret(&self, name: &str) -> Result<String, SecretsError> {
        validate_vault_secret_name(name)?;

        // S47: hold the per-secret rotation lock across the invalidate→fetch sequence.
        // Any concurrent get_secret_with_expiry calls will wait until rotation completes,
        // then pick up the new value from the cache on their re-check (double-checked locking).
        self.with_secret_lock(name, || async {
            // Invalidate the cache entry so the subsequent fetch gets fresh credentials.
            let cache = self.cache.read().await;
            cache.invalidate(name).await;
            drop(cache);

            let (new_secret, _) = self.get_secret_with_expiry(name).await?;
            Ok(new_secret)
        })
        .await
    }
}

impl VaultBackend {
    /// Create new `VaultBackend` with server address and authentication token.
    ///
    /// # Errors
    ///
    /// Returns `SecretsError` if `addr` targets a private/loopback address (SSRF
    /// protection) or if the HTTP client cannot be built.
    pub fn new<S: Into<String>>(addr: S, token: S) -> Result<Self, SecretsError> {
        let addr_str: String = addr.into();
        // SECURITY: Reject Vault addresses that target private/loopback ranges (SSRF).
        validate_vault_addr(&addr_str)?;
        let client = build_http_client(true)?;
        Ok(VaultBackend {
            addr: addr_str,
            token: Zeroizing::new(token.into()),
            namespace: None,
            tls_verify: true,
            client,
            cache: Arc::new(RwLock::new(SecretCache::new(DEFAULT_MAX_CACHE_ENTRIES))),
            rotation_locks: Arc::new(DashMap::new()),
            // Static token — no TTL tracking
            token_obtained_at: None,
            token_ttl_secs: None,
        })
    }

    /// Set Vault namespace (Enterprise feature).
    #[must_use]
    pub fn with_namespace<S: Into<String>>(mut self, namespace: S) -> Self {
        self.namespace = Some(namespace.into());
        self
    }

    /// Set TLS certificate verification.
    ///
    /// # Panics
    ///
    /// Panics if the HTTP client cannot be rebuilt (should not happen in practice).
    #[must_use = "builder method returns modified builder"]
    pub fn with_tls_verify(mut self, verify: bool) -> Self {
        self.tls_verify = verify;
        // Rebuild the shared client with the updated TLS setting.
        self.client = build_http_client(verify).expect("Failed to rebuild Vault HTTP client");
        self
    }

    /// Create a `VaultBackend` by authenticating via `AppRole`.
    ///
    /// Posts to `/v1/auth/approle/login` with the given `role_id` and `secret_id`,
    /// then uses the returned client token for subsequent requests.
    ///
    /// # Errors
    ///
    /// Returns `SecretsError::ConnectionError` if login fails.
    pub async fn with_approle(
        addr: &str,
        role_id: &str,
        secret_id: &str,
    ) -> Result<Self, SecretsError> {
        let client = reqwest::Client::builder()
            .timeout(VAULT_REQUEST_TIMEOUT)
            .build()
            .map_err(|e| SecretsError::ConnectionError(format!("HTTP client error: {e}")))?;

        let login_url =
            format!("{}/{}/auth/approle/login", addr.trim_end_matches('/'), VAULT_API_VERSION);
        let body = serde_json::json!({
            "role_id": role_id,
            "secret_id": secret_id,
        });

        let response: serde_json::Value = {
            let resp =
                client.post(&login_url).json(&body).send().await.map_err(|e| {
                    SecretsError::ConnectionError(format!("AppRole login failed: {e}"))
                })?;
            read_vault_response(resp).await?
        };

        let token = response["auth"]["client_token"]
            .as_str()
            .ok_or_else(|| {
                SecretsError::ConnectionError("No client_token in AppRole response".into())
            })?
            .to_string();

        let token_ttl_secs = response["auth"]["lease_duration"].as_i64();
        let token_obtained_at = Some(Utc::now());

        let mut backend = Self::new(addr, token.as_str())?;
        backend.token_obtained_at = token_obtained_at;
        backend.token_ttl_secs = token_ttl_secs;
        Ok(backend)
    }

    /// Get Vault server address.
    #[must_use]
    pub fn addr(&self) -> &str {
        &self.addr
    }

    /// Get authentication token.
    ///
    /// Available only in test builds. Production code must not extract the raw
    /// token — it is stored in `Zeroizing<String>` precisely to limit its lifetime
    /// and exposure.
    #[cfg(test)]
    #[must_use]
    pub(super) fn token(&self) -> &str {
        &self.token
    }

    /// Get configured namespace.
    #[must_use]
    pub fn namespace(&self) -> Option<&str> {
        self.namespace.as_deref()
    }

    /// Check if TLS verification is enabled.
    #[must_use]
    pub const fn tls_verify(&self) -> bool {
        self.tls_verify
    }

    /// Authenticate via `AppRole`, bypassing SSRF validation for tests.
    ///
    /// Identical to `with_approle` except that `validate_vault_addr` is skipped,
    /// allowing the backend to target a `wiremock::MockServer` on loopback.
    /// Do NOT use in production code.
    #[cfg(test)]
    pub(super) async fn with_approle_for_test(
        addr: &str,
        role_id: &str,
        secret_id: &str,
    ) -> Result<Self, SecretsError> {
        let client = reqwest::Client::builder()
            .timeout(VAULT_REQUEST_TIMEOUT)
            .build()
            .map_err(|e| SecretsError::ConnectionError(format!("HTTP client error: {e}")))?;

        let login_url =
            format!("{}/{}/auth/approle/login", addr.trim_end_matches('/'), VAULT_API_VERSION);
        let body = serde_json::json!({
            "role_id": role_id,
            "secret_id": secret_id,
        });

        let response: serde_json::Value = {
            let resp =
                client.post(&login_url).json(&body).send().await.map_err(|e| {
                    SecretsError::ConnectionError(format!("AppRole login failed: {e}"))
                })?;
            read_vault_response(resp).await?
        };

        let token = response["auth"]["client_token"]
            .as_str()
            .ok_or_else(|| {
                SecretsError::ConnectionError("No client_token in AppRole response".into())
            })?
            .to_string();

        let token_ttl_secs = response["auth"]["lease_duration"].as_i64();
        let token_obtained_at = Some(chrono::Utc::now());

        let client_new = build_http_client(false)?;
        Ok(VaultBackend {
            addr: addr.to_string(),
            token: zeroize::Zeroizing::new(token),
            namespace: None,
            tls_verify: false,
            client: client_new,
            cache: std::sync::Arc::new(tokio::sync::RwLock::new(super::cache::SecretCache::new(
                super::cache::DEFAULT_MAX_CACHE_ENTRIES,
            ))),
            rotation_locks: Arc::new(DashMap::new()),
            token_obtained_at,
            token_ttl_secs,
        })
    }

    /// Create a `VaultBackend` pointing at a loopback address for unit/integration tests.
    ///
    /// **This constructor bypasses SSRF validation** — it exists solely so that tests
    /// can point the backend at a `wiremock::MockServer` bound to `127.0.0.1`.
    /// Do NOT use in production code.
    #[cfg(test)]
    pub(super) fn new_for_test(addr: impl Into<String>, token: impl Into<String>) -> Self {
        let client = build_http_client(false).expect("Failed to build test Vault HTTP client");
        VaultBackend {
            addr: addr.into(),
            token: Zeroizing::new(token.into()),
            namespace: None,
            tls_verify: false,
            client,
            cache: Arc::new(RwLock::new(SecretCache::new(DEFAULT_MAX_CACHE_ENTRIES))),
            rotation_locks: Arc::new(DashMap::new()),
            token_obtained_at: None,
            token_ttl_secs: None,
        }
    }

    /// Number of per-secret locks currently tracked.
    ///
    /// Each entry is created on first access and persists for the lifetime of the
    /// backend. Used in tests to verify the field is present and populated.
    #[must_use]
    pub fn rotation_locks_len(&self) -> usize {
        self.rotation_locks.len()
    }

    /// Acquire (or create) the per-secret rotation lock, then run `f` while holding it.
    ///
    /// This prevents the thundering-herd race between `rotate_secret` (which
    /// invalidates the cache and re-fetches) and concurrent `get_secret_with_expiry`
    /// callers (which may also get a cache miss and race to Vault during rotation).
    ///
    /// Lock ordering: always acquire `rotation_locks` entry before `cache`.
    async fn with_secret_lock<F, Fut, T>(&self, name: &str, f: F) -> T
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = T>,
    {
        let lock = {
            self.rotation_locks
                .entry(name.to_string())
                .or_insert_with(|| Arc::new(Mutex::new(())))
                .clone()
        };
        let _guard = lock.lock().await;
        f().await
    }

    /// Returns `true` if the Vault auth token should be renewed.
    ///
    /// Returns `false` for static tokens (created via `new()`). For `AppRole` tokens
    /// (created via `with_approle()`), returns `true` when more than
    /// `TOKEN_RENEWAL_THRESHOLD` (80%) of the token's TTL has elapsed.
    ///
    /// Callers should invoke `renew_token()` when this returns `true` to avoid
    /// authentication failures from expired tokens.
    #[must_use]
    pub fn token_needs_renewal(&self) -> bool {
        let (Some(obtained_at), Some(ttl_secs)) = (self.token_obtained_at, self.token_ttl_secs)
        else {
            // Static long-lived token — no renewal needed
            return false;
        };

        let elapsed_secs = (Utc::now() - obtained_at).num_seconds();
        // Use integer arithmetic to avoid f64 precision loss for large TTL values.
        #[allow(clippy::cast_possible_truncation)]
        // Reason: TOKEN_RENEWAL_THRESHOLD * 100.0 is 80.0, fits in i64
        let renewal_threshold_secs =
            ttl_secs.saturating_mul((TOKEN_RENEWAL_THRESHOLD * 100.0) as i64) / 100;
        elapsed_secs >= renewal_threshold_secs
    }

    /// Renew the Vault auth token via `POST /v1/auth/token/renew-self`.
    ///
    /// On success, resets the `token_obtained_at` clock and updates `token_ttl_secs`
    /// from the response so that `token_needs_renewal()` reflects the renewed lease.
    ///
    /// # Errors
    ///
    /// Returns `SecretsError::ConnectionError` if the renewal request fails or if
    /// the token is not renewable (e.g. orphaned or periodic tokens).
    pub async fn renew_token(&mut self) -> Result<(), SecretsError> {
        let url = format!(
            "{}/{}/auth/token/renew-self",
            self.addr.trim_end_matches('/'),
            VAULT_API_VERSION
        );
        let response: serde_json::Value = {
            let resp = self
                .client
                .post(&url)
                .header("X-Vault-Token", &*self.token)
                .header("X-Vault-Namespace", self.namespace.as_deref().unwrap_or(""))
                .send()
                .await
                .map_err(|e| {
                    SecretsError::ConnectionError(format!("Token renewal request failed: {e}"))
                })?;
            if !resp.status().is_success() {
                return Err(SecretsError::ConnectionError(format!(
                    "Token renewal failed with status {}",
                    resp.status()
                )));
            }
            read_vault_response(resp).await.map_err(|e| {
                SecretsError::ConnectionError(format!("Token renewal response error: {e}"))
            })?
        };

        // Vault returns the renewed token info under `auth`
        let new_token = response["auth"]["client_token"].as_str().ok_or_else(|| {
            SecretsError::ConnectionError(
                "No client_token in renewal response — token may not be renewable".into(),
            )
        })?;

        self.token = Zeroizing::new(new_token.to_string());
        self.token_obtained_at = Some(Utc::now());
        if let Some(ttl) = response["auth"]["lease_duration"].as_i64() {
            self.token_ttl_secs = Some(ttl);
        }

        Ok(())
    }

    /// Token TTL as set at login time (seconds). `None` for static tokens.
    /// Exposed for test assertions only.
    #[cfg(test)]
    pub(super) const fn token_ttl_secs_for_test(&self) -> Option<i64> {
        self.token_ttl_secs
    }

    /// Extract secret data from Vault API response.
    ///
    /// Handles both KV2 format (nested data.data) and dynamic credentials (flat data).
    pub(super) fn extract_secret_from_response(
        response: &VaultResponse,
        path: &str,
    ) -> Result<String, SecretsError> {
        // For KV2 engine: response.data.data contains actual secret
        // For dynamic credentials: response.data contains username/password
        if let Some(data_obj) = response.data.get("data") {
            serde_json::to_string(data_obj).map_err(|e| {
                SecretsError::BackendError(format!(
                    "Failed to serialize KV2 secret from {}: {}",
                    path, e
                ))
            })
        } else {
            // Dynamic credentials or generic secret
            serde_json::to_string(&response.data).map_err(|e| {
                SecretsError::BackendError(format!(
                    "Failed to serialize secret from {}: {}",
                    path, e
                ))
            })
        }
    }

    /// Fetch secret from Vault HTTP API with retry on transient errors.
    ///
    /// Retries up to 3 times with exponential backoff (100ms, 200ms, 400ms)
    /// on 503 (Service Unavailable), 429 (Too Many Requests), or connection errors.
    /// Non-retryable errors (403, 404) fail immediately.
    async fn fetch_secret(&self, name: &str) -> Result<VaultResponse, SecretsError> {
        let url = self.build_vault_url(name);
        let delays = [
            std::time::Duration::from_millis(100),
            std::time::Duration::from_millis(200),
            std::time::Duration::from_millis(400),
        ];

        let mut last_error = None;
        for (attempt, delay) in delays.iter().enumerate() {
            match self
                .client
                .get(&url)
                .header("X-Vault-Token", &*self.token)
                .header("X-Vault-Namespace", self.namespace.as_deref().unwrap_or(""))
                .send()
                .await
            {
                Ok(response) => {
                    match response.status() {
                        reqwest::StatusCode::OK => {
                            return read_vault_response::<VaultResponse>(response).await;
                        },
                        // Retryable statuses
                        reqwest::StatusCode::SERVICE_UNAVAILABLE
                        | reqwest::StatusCode::TOO_MANY_REQUESTS => {
                            last_error = Some(SecretsError::BackendError(format!(
                                "Vault returned {} (attempt {}/3)",
                                response.status(),
                                attempt + 1
                            )));
                            tokio::time::sleep(*delay).await;
                        },
                        // Non-retryable statuses
                        reqwest::StatusCode::NOT_FOUND => {
                            return Err(SecretsError::NotFound(format!(
                                "Secret not found in Vault: {name}"
                            )));
                        },
                        reqwest::StatusCode::FORBIDDEN => {
                            return Err(SecretsError::BackendError(format!(
                                "Permission denied accessing Vault secret: {name}"
                            )));
                        },
                        status => {
                            return Err(SecretsError::BackendError(format!(
                                "Vault request failed with status {status} for {name}"
                            )));
                        },
                    }
                },
                Err(e) => {
                    // Connection errors are retryable
                    last_error = Some(SecretsError::ConnectionError(format!(
                        "Vault connection error (attempt {}/3): {e}",
                        attempt + 1
                    )));
                    tokio::time::sleep(*delay).await;
                },
            }
        }
        Err(last_error
            .unwrap_or_else(|| SecretsError::ConnectionError("Max retries exceeded".into())))
    }

    /// Build Vault API URL for a secret path.
    fn build_vault_url(&self, path: &str) -> String {
        format!("{}/{}/{}", self.addr.trim_end_matches('/'), VAULT_API_VERSION, path)
    }

    /// Build HTTP request to Vault with standard headers.
    fn build_vault_request(&self, client: &reqwest::Client, url: &str) -> reqwest::RequestBuilder {
        client
            .post(url)
            .header("X-Vault-Token", (*self.token).clone())
            .header("X-Vault-Namespace", self.namespace.as_deref().unwrap_or(""))
    }

    /// Handle Transit engine response for encrypt/decrypt.
    async fn handle_transit_response(
        &self,
        response: reqwest::Response,
        data_field: &str,
        operation: &str,
    ) -> Result<String, SecretsError> {
        match response.status() {
            reqwest::StatusCode::OK => {
                let body: serde_json::Value = read_vault_response(response).await.map_err(|e| {
                    SecretsError::BackendError(format!("Failed to parse Transit response: {}", e))
                })?;

                body["data"][data_field].as_str().map(|s| s.to_string()).ok_or_else(|| {
                    SecretsError::EncryptionError(format!("Missing {} in response", data_field))
                })
            },
            reqwest::StatusCode::NOT_FOUND => {
                Err(SecretsError::NotFound("Transit key not found".to_string()))
            },
            status => Err(SecretsError::EncryptionError(format!(
                "Vault Transit {} failed with status {}",
                operation, status
            ))),
        }
    }

    /// Encrypt plaintext using Vault Transit engine.
    ///
    /// # Arguments
    /// * `key_name` - Name of the transit encryption key
    /// * `plaintext` - Data to encrypt
    ///
    /// # Errors
    ///
    /// Returns `SecretsError::ValidationError` if the key name is invalid.
    /// Returns `SecretsError::BackendError` if the Vault Transit request fails.
    pub async fn encrypt_field(
        &self,
        key_name: &str,
        plaintext: &str,
    ) -> Result<String, SecretsError> {
        validate_vault_secret_name(key_name)?;

        let url = format!(
            "{}/{}/transit/encrypt/{}",
            self.addr.trim_end_matches('/'),
            VAULT_API_VERSION,
            key_name
        );

        let request_body = serde_json::json!({
            "plaintext": STANDARD_NO_PAD.encode(plaintext)
        });

        let response = self
            .build_vault_request(&self.client, &url)
            .json(&request_body)
            .send()
            .await
            .map_err(|e| {
                SecretsError::BackendError(format!("Vault Transit encrypt request failed: {}", e))
            })?;

        self.handle_transit_response(response, "ciphertext", "encrypt").await
    }

    /// Decrypt ciphertext using Vault Transit engine.
    ///
    /// # Arguments
    /// * `key_name` - Name of the transit encryption key
    /// * `ciphertext` - Encrypted data (in Vault's format)
    ///
    /// # Errors
    ///
    /// Returns `SecretsError::ValidationError` if the key name is invalid.
    /// Returns `SecretsError::BackendError` if the Vault Transit request fails.
    pub async fn decrypt_field(
        &self,
        key_name: &str,
        ciphertext: &str,
    ) -> Result<String, SecretsError> {
        validate_vault_secret_name(key_name)?;

        let url = format!(
            "{}/{}/transit/decrypt/{}",
            self.addr.trim_end_matches('/'),
            VAULT_API_VERSION,
            key_name
        );

        let request_body = serde_json::json!({
            "ciphertext": ciphertext
        });

        let response = self
            .build_vault_request(&self.client, &url)
            .json(&request_body)
            .send()
            .await
            .map_err(|e| {
                SecretsError::BackendError(format!("Vault Transit decrypt request failed: {}", e))
            })?;

        // Get plaintext from response
        let plaintext_b64 = self.handle_transit_response(response, "plaintext", "decrypt").await?;

        // Decode base64 to get original plaintext
        STANDARD_NO_PAD
            .decode(&plaintext_b64)
            .ok()
            .and_then(|bytes| String::from_utf8(bytes).ok())
            .ok_or_else(|| SecretsError::EncryptionError("Failed to decode plaintext".to_string()))
    }
}
