//! OAuth 2.0 token management with `DPoP` authentication.
//!
//! Implements RFC 9449 `DPoP`-bound access tokens for secure API
//! authentication.

// WASM is single-threaded, so futures don't need to be Send
#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))]

#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};

use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;

use crate::crypto::DPoPKeyPair;
use crate::error::LastIDError;
use crate::shared::SharedLock;

/// Configuration for the token manager.
#[derive(Debug, Clone)]
pub struct TokenManagerConfig {
    /// OAuth client ID
    pub client_id: String,
    /// Token refresh buffer in seconds (refresh token before expiry)
    pub refresh_buffer_seconds: u64,
}

impl Default for TokenManagerConfig {
    fn default() -> Self {
        Self {
            client_id: String::new(),
            refresh_buffer_seconds: 30, // Refresh 30 seconds before expiry
        }
    }
}

/// Internal access token with expiry tracking (SEC-007: wrapped in
/// `SecretBox`).
///
/// The token value is stored as `SecretString` to prevent accidental exposure
/// in logs, debug output, or error messages. The underlying memory is zeroized
/// when the token is dropped.
#[derive(Clone)]
struct AccessToken {
    /// The access token string (protected by `SecretString`)
    token: SecretString,
    /// Unix timestamp when token expires
    expires_at: u64,
}

impl std::fmt::Debug for AccessToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AccessToken")
            .field("token", &"[REDACTED]")
            .field("expires_at", &self.expires_at)
            .finish()
    }
}

impl AccessToken {
    /// Create a new access token with the given value and expiration.
    fn new(token: String, expires_at: u64) -> Self {
        Self {
            token: token.into(),
            expires_at,
        }
    }

    /// Get the token value (exposed for use in API calls).
    fn expose_token(&self) -> &str {
        self.token.expose_secret()
    }

    /// Check if the token is expired or about to expire.
    fn is_expired(&self, buffer_seconds: u64) -> bool {
        let now = current_time_secs();
        now + buffer_seconds >= self.expires_at
    }
}

impl Zeroize for AccessToken {
    fn zeroize(&mut self) {
        // SecretString handles zeroization automatically
        // We just need to reset the metadata
        self.expires_at = 0;
    }
}

// Required for SecretBox
impl secrecy::CloneableSecret for AccessToken {}

/// OAuth token response from the IDP.
#[derive(Debug, Deserialize)]
struct TokenResponse {
    access_token: String,
    #[serde(default)]
    #[allow(dead_code)]
    token_type: String,
    expires_in: u64,
}

/// OAuth error response from the IDP.
#[derive(Debug, Deserialize)]
struct TokenErrorResponse {
    error: String,
    #[serde(default)]
    error_description: Option<String>,
}

/// Token manager for OAuth 2.0 authentication with `DPoP`.
///
/// Handles:
/// - `DPoP` keypair generation and management
/// - OAuth token acquisition via client credentials grant
/// - Token caching and automatic refresh
/// - `DPoP` nonce handling for replay protection
///
/// # Example
///
/// ```rust,no_run
/// use lastid_sdk::auth::{
///     TokenManager, TokenManagerConfig, TokenManagerTrait,
/// };
///
/// # async fn example() -> Result<(), lastid_sdk::LastIDError> {
/// let config = TokenManagerConfig {
///     client_id: "my-app".to_string(),
///     refresh_buffer_seconds: 30,
/// };
///
/// let manager = TokenManager::new("https://human.lastid.co", config)?;
/// let token = manager.get_token().await?;
/// println!("Bearer token: {}", token);
/// # Ok(())
/// # }
/// ```
pub struct TokenManager {
    endpoint: String,
    client_id: String,
    dpop_keypair: DPoPKeyPair,
    access_token: SharedLock<Option<AccessToken>>,
    dpop_nonce: SharedLock<Option<String>>,
    refresh_buffer_seconds: u64,
}

impl TokenManager {
    /// Create a new token manager.
    ///
    /// Generates a new `DPoP` keypair for proof-of-possession authentication.
    ///
    /// # Arguments
    ///
    /// * `endpoint` - Base URL of the IDP (e.g., `https://human.lastid.co`)
    /// * `config` - Token manager configuration
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::Crypto` if `DPoP` keypair generation fails.
    pub fn new(
        endpoint: impl Into<String>,
        config: TokenManagerConfig,
    ) -> Result<Self, LastIDError> {
        Self::with_optional_keypair(endpoint, config, None)
    }

    /// Create a new token manager with an optional pre-generated `DPoP`
    /// keypair.
    ///
    /// This is useful for serverless deployments where the keypair needs to
    /// persist across invocations. If `keypair` is `None`, a new keypair is
    /// generated.
    ///
    /// # Arguments
    ///
    /// * `endpoint` - Base URL of the IDP (e.g., `https://human.lastid.co`)
    /// * `config` - Token manager configuration
    /// * `keypair` - Optional pre-generated `DPoP` keypair
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::Crypto` if `DPoP` keypair generation fails (when
    /// `keypair` is `None`).
    pub fn with_optional_keypair(
        endpoint: impl Into<String>,
        config: TokenManagerConfig,
        keypair: Option<DPoPKeyPair>,
    ) -> Result<Self, LastIDError> {
        let dpop_keypair = match keypair {
            Some(kp) => kp,
            None => DPoPKeyPair::generate()?,
        };

        Ok(Self {
            endpoint: endpoint.into(),
            client_id: config.client_id,
            dpop_keypair,
            access_token: SharedLock::new(None),
            dpop_nonce: SharedLock::new(None),
            refresh_buffer_seconds: config.refresh_buffer_seconds,
        })
    }

    /// Acquire a new access token from the IDP.
    async fn acquire_token(&self) -> Result<String, LastIDError> {
        // Include client_id in query string for CORS preflight validation.
        // Browser preflight (OPTIONS) requests have no body, so the IDP needs
        // client_id in the URL to validate the origin against allowedOrigins.
        // The client_id is also sent in the POST body for the actual request.
        let token_url = format!(
            "{}/oauth/token?client_id={}",
            self.endpoint,
            urlencoding::encode(&self.client_id)
        );

        // Try to acquire token, handling DPoP nonce if needed
        match self.do_token_request(&token_url, None).await {
            Ok(token) => Ok(token),
            Err(LastIDError::Http(ref e)) if Self::is_dpop_nonce_error(e) => {
                // Server requires a DPoP nonce - it should have been stored from the
                // response header during do_token_request. Retry with the stored nonce.
                if let Some(nonce) = self.get_dpop_nonce() {
                    self.do_token_request(&token_url, Some(&nonce)).await
                } else {
                    Err(LastIDError::crypto(
                        "Server requires DPoP nonce but none provided in response headers",
                    ))
                }
            }
            Err(e) => Err(e),
        }
    }

    /// Perform the actual token request.
    async fn do_token_request(
        &self,
        url: &str,
        nonce: Option<&str>,
    ) -> Result<String, LastIDError> {
        // Create DPoP proof for token endpoint with client_id as subject
        let dpop_proof =
            self.dpop_keypair
                .create_dpop_proof("POST", url, None, Some(&self.client_id))?;

        // Build request body (client credentials grant)
        // SDK clients don't send scope - IDP only accepts vp:request vp:verify for SDK
        // clients
        let body = TokenRequest {
            grant_type: "client_credentials".to_string(),
            client_id: self.client_id.clone(),
            scope: None,
        };

        // Send request
        let client = reqwest::Client::new();
        let mut request = client
            .post(url)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .header("DPoP", &dpop_proof);

        // Add DPoP nonce if provided
        if let Some(n) = nonce {
            request = request.header("DPoP-Nonce", n);
        }

        let response = request
            .form(&body)
            .send()
            .await
            .map_err(|e| crate::error::HttpError::network(e.to_string()))?;

        // Check for DPoP nonce in response headers (server may provide a new one)
        if let Some(nonce_header) = response.headers().get("dpop-nonce")
            && let Ok(nonce_str) = nonce_header.to_str()
        {
            self.set_dpop_nonce(Some(nonce_str.to_string()));
        }

        if response.status().is_success() {
            let token_response: TokenResponse = response
                .json()
                .await
                .map_err(|e| crate::error::HttpError::network(e.to_string()))?;

            // Cache the token (SEC-007: protected with SecretString)
            let expires_at = current_time_secs() + token_response.expires_in;
            let token_value = token_response.access_token;
            let access_token = AccessToken::new(token_value.clone(), expires_at);
            self.set_cached_token(access_token);

            Ok(token_value)
        } else {
            let status = response.status().as_u16();

            // Try to parse error response
            let error_text = response.text().await.unwrap_or_default();

            // Check for use_dpop_nonce error
            if error_text.contains("use_dpop_nonce") {
                return Err(LastIDError::Http(crate::error::HttpError::Status {
                    status,
                    message: error_text,
                }));
            }

            // Parse structured error if possible
            if let Ok(error_response) = serde_json::from_str::<TokenErrorResponse>(&error_text) {
                let message = error_response
                    .error_description
                    .unwrap_or(error_response.error);
                return Err(LastIDError::Http(crate::error::HttpError::status(
                    status, message,
                )));
            }

            Err(LastIDError::Http(crate::error::HttpError::status(
                status, error_text,
            )))
        }
    }

    /// Check if the error indicates a `DPoP` nonce is required.
    fn is_dpop_nonce_error(e: &crate::error::HttpError) -> bool {
        match e {
            crate::error::HttpError::Status { message, .. } => message.contains("use_dpop_nonce"),
            _ => false,
        }
    }

    // Token cache and DPoP nonce accessors using SharedLock

    #[cfg(not(target_arch = "wasm32"))]
    fn get_cached_token(&self) -> Option<AccessToken> {
        // parking_lot::RwLock::read() returns the guard directly (no poisoning)
        self.access_token.read().clone()
    }

    #[cfg(target_arch = "wasm32")]
    fn get_cached_token(&self) -> Option<AccessToken> {
        self.access_token.borrow().clone()
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn set_cached_token(&self, token: AccessToken) {
        // parking_lot::RwLock::write() returns the guard directly (no poisoning)
        *self.access_token.write() = Some(token);
    }

    #[cfg(target_arch = "wasm32")]
    fn set_cached_token(&self, token: AccessToken) {
        *self.access_token.borrow_mut() = Some(token);
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn get_dpop_nonce(&self) -> Option<String> {
        // parking_lot::RwLock::read() returns the guard directly (no poisoning)
        self.dpop_nonce.read().clone()
    }

    #[cfg(target_arch = "wasm32")]
    fn get_dpop_nonce(&self) -> Option<String> {
        self.dpop_nonce.borrow().clone()
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn set_dpop_nonce(&self, nonce: Option<String>) {
        // parking_lot::RwLock::write() returns the guard directly (no poisoning)
        *self.dpop_nonce.write() = nonce;
    }

    #[cfg(target_arch = "wasm32")]
    fn set_dpop_nonce(&self, nonce: Option<String>) {
        *self.dpop_nonce.borrow_mut() = nonce;
    }
}

// Implement TokenManagerTrait for TokenManager

use async_trait::async_trait;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;

use super::TokenManagerTrait;

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl TokenManagerTrait for TokenManager {
    async fn get_token(&self) -> Result<String, LastIDError> {
        // Check if existing token is still valid
        if let Some(token) = self.get_cached_token()
            && !token.is_expired(self.refresh_buffer_seconds)
        {
            // SEC-007: Expose the token from SecretString for use in API calls
            return Ok(token.expose_token().to_string());
        }

        // Acquire new token
        self.acquire_token().await
    }

    fn create_dpop_proof(
        &self,
        method: &str,
        url: &str,
        access_token: Option<&str>,
    ) -> Result<String, LastIDError> {
        self.dpop_keypair
            .create_dpop_proof(method, url, access_token, Some(&self.client_id))
    }

    fn public_jwk(&self) -> serde_json::Value {
        self.dpop_keypair.public_jwk()
    }

    fn thumbprint(&self) -> &str {
        self.dpop_keypair.thumbprint()
    }
}

impl TokenManager {
    /// Verify that an access token is bound to this manager's `DPoP` key
    /// (SEC-004).
    ///
    /// Checks that the token's `cnf.jkt` claim (confirmation JWK thumbprint)
    /// matches the thumbprint of the `DPoP` keypair used by this manager.
    ///
    /// # Arguments
    ///
    /// * `access_token` - The JWT access token to verify
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the token is properly bound to this `DPoP` key
    /// * `Err(LastIDError::TokenBindingMismatch)` if the thumbprints don't
    ///   match
    /// * `Err(LastIDError::InvalidCredential)` if the token format is invalid
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::auth::{TokenManager, TokenManagerConfig};
    ///
    /// # fn example() -> Result<(), lastid_sdk::LastIDError> {
    /// let config = TokenManagerConfig::default();
    /// let manager = TokenManager::new("https://idp.example.com", config)?;
    ///
    /// // Verify that the token is bound to our DPoP key
    /// manager.verify_token_binding("eyJ...")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn verify_token_binding(&self, access_token: &str) -> Result<(), LastIDError> {
        // Extract the cnf.jkt claim from the access token
        let jkt = Self::extract_jkt_claim(access_token)?;

        let expected = self.dpop_keypair.thumbprint();

        if jkt != expected {
            return Err(LastIDError::token_binding_mismatch(expected, jkt));
        }

        Ok(())
    }

    /// Extract the JWK thumbprint from the token's `cnf.jkt` claim.
    ///
    /// The token is expected to be a JWT with a `cnf` (confirmation) claim
    /// containing a `jkt` (JWK thumbprint) field as per RFC 9449.
    fn extract_jkt_claim(token: &str) -> Result<String, LastIDError> {
        // JWT format: header.payload.signature
        let parts: Vec<&str> = token.split('.').collect();
        if parts.len() != 3 {
            return Err(LastIDError::invalid_credential(
                "Invalid access token format: expected JWT with 3 parts",
            ));
        }

        // Decode the payload (second part)
        let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).map_err(|e| {
            LastIDError::invalid_credential(format!("Failed to decode token payload: {e}"))
        })?;

        // Parse as JSON
        let payload: serde_json::Value = serde_json::from_slice(&payload_bytes).map_err(|e| {
            LastIDError::invalid_credential(format!("Failed to parse token payload: {e}"))
        })?;

        // Extract cnf.jkt
        let jkt = payload
            .get("cnf")
            .and_then(|cnf| cnf.get("jkt"))
            .and_then(|jkt| jkt.as_str())
            .ok_or_else(|| {
                LastIDError::invalid_credential(
                    "Token missing cnf.jkt claim required for DPoP binding verification",
                )
            })?;

        Ok(jkt.to_string())
    }
}

/// Token request body for client credentials grant.
#[derive(Debug, Serialize)]
struct TokenRequest {
    grant_type: String,
    client_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    scope: Option<String>,
}

/// Get current time in seconds since Unix epoch.
#[cfg(not(target_arch = "wasm32"))]
fn current_time_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

#[cfg(target_arch = "wasm32")]
fn current_time_secs() -> u64 {
    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
    {
        (js_sys::Date::now() / 1000.0) as u64
    }
}

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

    #[test]
    fn test_token_manager_config_default() {
        let config = TokenManagerConfig::default();
        assert!(config.client_id.is_empty());
        assert_eq!(config.refresh_buffer_seconds, 30);
    }

    #[test]
    fn test_token_expired() {
        let token = AccessToken::new("test".to_string(), current_time_secs() + 10);

        // Not expired with 5 second buffer
        assert!(!token.is_expired(5));

        // Expired with 15 second buffer (10 + 15 > current + 10)
        assert!(token.is_expired(15));
    }

    #[test]
    fn test_token_manager_creation() {
        let config = TokenManagerConfig {
            client_id: "test-client".to_string(),
            refresh_buffer_seconds: 60,
        };

        let manager = TokenManager::new("https://example.com", config);
        assert!(manager.is_ok());

        let manager = manager.unwrap();
        assert!(!manager.thumbprint().is_empty());
    }

    // Token binding verification tests (SEC-004)

    fn create_test_jwt(payload: &serde_json::Value) -> String {
        use base64::Engine;
        use base64::engine::general_purpose::URL_SAFE_NO_PAD;

        let header = serde_json::json!({"alg": "ES256", "typ": "at+jwt"});
        let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
        let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(payload).unwrap());

        format!("{}.{}.fake_signature", header_b64, payload_b64)
    }

    #[test]
    fn test_extract_jkt_claim_success() {
        let payload = serde_json::json!({
            "iss": "https://idp.example.com",
            "sub": "client123",
            "cnf": {
                "jkt": "test-thumbprint-value"
            }
        });
        let token = create_test_jwt(&payload);

        let result = TokenManager::extract_jkt_claim(&token);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "test-thumbprint-value");
    }

    #[test]
    fn test_extract_jkt_claim_missing_cnf() {
        let payload = serde_json::json!({
            "iss": "https://idp.example.com",
            "sub": "client123"
            // No cnf claim
        });
        let token = create_test_jwt(&payload);

        let result = TokenManager::extract_jkt_claim(&token);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("cnf.jkt"));
    }

    #[test]
    fn test_extract_jkt_claim_missing_jkt() {
        let payload = serde_json::json!({
            "iss": "https://idp.example.com",
            "sub": "client123",
            "cnf": {
                // No jkt field
                "other": "value"
            }
        });
        let token = create_test_jwt(&payload);

        let result = TokenManager::extract_jkt_claim(&token);
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_jkt_claim_invalid_token_format() {
        // Not a JWT (no dots)
        let result = TokenManager::extract_jkt_claim("invalid-token");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("3 parts"));
    }

    #[test]
    fn test_verify_token_binding_mismatch() {
        let config = TokenManagerConfig {
            client_id: "test-client".to_string(),
            refresh_buffer_seconds: 60,
        };
        let manager = TokenManager::new("https://example.com", config).unwrap();

        // Create token with wrong thumbprint
        let payload = serde_json::json!({
            "iss": "https://idp.example.com",
            "sub": "client123",
            "cnf": {
                "jkt": "wrong-thumbprint"
            }
        });
        let token = create_test_jwt(&payload);

        let result = manager.verify_token_binding(&token);
        assert!(result.is_err());

        // Check it's a TokenBindingMismatch error
        match result.unwrap_err() {
            crate::LastIDError::TokenBindingMismatch { expected, actual } => {
                assert_eq!(expected, manager.thumbprint());
                assert_eq!(actual, "wrong-thumbprint");
            }
            e => panic!("Expected TokenBindingMismatch error, got: {e:?}"),
        }
    }

    #[test]
    fn test_verify_token_binding_success() {
        let config = TokenManagerConfig {
            client_id: "test-client".to_string(),
            refresh_buffer_seconds: 60,
        };
        let manager = TokenManager::new("https://example.com", config).unwrap();

        // Create token with correct thumbprint
        let payload = serde_json::json!({
            "iss": "https://idp.example.com",
            "sub": "client123",
            "cnf": {
                "jkt": manager.thumbprint()
            }
        });
        let token = create_test_jwt(&payload);

        let result = manager.verify_token_binding(&token);
        assert!(result.is_ok());
    }

    #[test]
    fn test_access_token_debug_redacted() {
        // SEC-007: Verify that AccessToken debug output doesn't leak the token
        let token = AccessToken::new(
            "super-secret-token-value".to_string(),
            current_time_secs() + 3600,
        );

        let debug_output = format!("{:?}", token);
        assert!(!debug_output.contains("super-secret-token-value"));
        assert!(debug_output.contains("[REDACTED]"));
    }

    #[test]
    fn test_access_token_expose_secret() {
        // SEC-007: Verify that expose_token() returns the correct value
        let token = AccessToken::new("my-access-token".to_string(), current_time_secs() + 3600);

        assert_eq!(token.expose_token(), "my-access-token");
    }
}
