#![forbid(unsafe_code)]

//! JWT (JSON Web Token) authentication module for the lifecycle API server.
//!
//! This module provides:
//! - JWT token creation with configurable claims
//! - JWT token validation and verification
//! - Claims management (subject, expiration, custom claims)
//! - Error handling for JWT operations
//!
//! # Token Structure
//!
//! JWT tokens contain the following standard claims:
//! - `sub` (subject) - User ID or identifier
//! - `exp` (expiration) - Token expiration time (Unix timestamp)
//! - `iat` (issued at) - Token creation time (Unix timestamp)
//! - `aud` (audience) - Intended audience
//! - Custom claims as needed
//!
//! # Example
//!
//! ```ignore
//! let secret = "your-secret-key-min-32-chars-long!";
//! let provider = JwtProvider::new(secret, 3600)?; // 1 hour expiry
//!
//! // Create a token
//! let token = provider.create_token("user-123")?;
//!
//! // Verify and decode a token
//! let claims = provider.verify_token(&token)?;
//! assert_eq!(claims.sub, "user-123");
//! ```

use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;

/// JWT errors
#[allow(dead_code)]
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum JwtError {
    /// Token format is invalid
    #[error("Invalid token format")]
    InvalidFormat,

    /// Token has expired
    #[error("Token expired")]
    TokenExpired,

    /// Token signature is invalid
    #[error("Invalid signature")]
    InvalidSignature,

    /// Token claims are invalid
    #[error("Invalid claims")]
    InvalidClaims,

    /// Secret key is too short (must be at least 32 characters)
    #[error("Secret key must be at least 32 characters long")]
    SecretKeyTooShort,

    /// Generic JWT error with message
    #[error("JWT error: {0}")]
    JwtError(String),
}

/// JWT claims structure
/// Contains standard JWT claims plus custom fields
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Claims {
    /// Subject (user ID)
    pub sub: String,

    /// Expiration time (Unix timestamp)
    pub exp: i64,

    /// Issued at time (Unix timestamp)
    pub iat: i64,

    /// Audience (e.g., "syntaxis-api")
    #[serde(default)]
    pub aud: String,

    /// User roles (optional)
    #[serde(default)]
    pub roles: Vec<String>,

    /// Custom claims for lifecycle API
    #[serde(default)]
    pub tenant_id: Option<String>,

    /// API version scoped to
    #[serde(default)]
    pub api_version: Option<String>,
}

impl Claims {
    /// Create new claims with subject and expiration duration
    pub fn new(subject: impl Into<String>, expires_in: Duration) -> Self {
        let now = Utc::now();
        let exp = now + expires_in;

        Self {
            sub: subject.into(),
            exp: exp.timestamp(),
            iat: now.timestamp(),
            aud: "syntaxis-api".to_string(),
            roles: Vec::new(),
            tenant_id: None,
            api_version: None,
        }
    }

    /// Add a role to the claims
    #[allow(dead_code)]
    pub fn with_role(mut self, role: impl Into<String>) -> Self {
        self.roles.push(role.into());
        self
    }

    /// Add multiple roles
    #[allow(dead_code)]
    pub fn with_roles(mut self, roles: impl IntoIterator<Item = String>) -> Self {
        self.roles.extend(roles);
        self
    }

    /// Set tenant ID
    #[allow(dead_code)]
    pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
        self.tenant_id = Some(tenant_id.into());
        self
    }

    /// Set API version
    #[allow(dead_code)]
    pub fn with_api_version(mut self, version: impl Into<String>) -> Self {
        self.api_version = Some(version.into());
        self
    }

    /// Check if token is expired
    #[allow(dead_code)]
    pub fn is_expired(&self) -> bool {
        let now = Utc::now().timestamp();
        now > self.exp
    }

    /// Get time until expiration in seconds
    #[allow(dead_code)]
    pub fn seconds_until_expiry(&self) -> i64 {
        self.exp - Utc::now().timestamp()
    }
}

/// JWT provider for token creation and validation
#[allow(dead_code)]
#[derive(Clone)]
pub struct JwtProvider {
    /// Encoding key (for creating tokens)
    encoding_key: Arc<EncodingKey>,

    /// Decoding key (for verifying tokens)
    decoding_key: Arc<DecodingKey>,

    /// Token expiration time in seconds
    #[allow(dead_code)]
    expiration_seconds: i64,

    /// Algorithm (fixed to HS256)
    #[allow(dead_code)]
    algorithm: String,
}

impl std::fmt::Debug for JwtProvider {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("JwtProvider")
            .field("expiration_seconds", &self.expiration_seconds)
            .field("algorithm", &self.algorithm)
            .finish()
    }
}

impl JwtProvider {
    /// Create a new JWT provider with a secret key
    ///
    /// # Arguments
    /// - `secret`: Secret key for signing (must be at least 32 characters)
    /// - `expiration_seconds`: Token expiration time in seconds
    ///
    /// # Errors
    /// - Returns error if secret is less than 32 characters
    #[allow(dead_code)]
    pub fn new(secret: impl Into<String>, expiration_seconds: i64) -> Result<Self, JwtError> {
        let secret_str = secret.into();

        if secret_str.len() < 32 {
            return Err(JwtError::SecretKeyTooShort);
        }

        let encoding_key = EncodingKey::from_secret(secret_str.as_bytes());
        let decoding_key = DecodingKey::from_secret(secret_str.as_bytes());

        Ok(Self {
            encoding_key: Arc::new(encoding_key),
            decoding_key: Arc::new(decoding_key),
            expiration_seconds,
            algorithm: "HS256".to_string(),
        })
    }

    /// Create a JWT token for a subject with default claims
    ///
    /// # Arguments
    /// - `subject`: User ID or identifier
    ///
    /// # Returns
    /// - Signed JWT token string
    ///
    /// # Errors
    /// - Returns error if token creation fails
    #[allow(dead_code)]
    pub fn create_token(&self, subject: impl Into<String>) -> Result<String, JwtError> {
        let claims = Claims::new(subject, Duration::seconds(self.expiration_seconds));
        self.encode_token(&claims)
    }

    /// Create a JWT token with custom claims
    ///
    /// # Arguments
    /// - `claims`: Fully configured claims
    ///
    /// # Returns
    /// - Signed JWT token string
    ///
    /// # Errors
    /// - Returns error if token creation fails
    #[allow(dead_code)]
    pub fn encode_token(&self, claims: &Claims) -> Result<String, JwtError> {
        encode(&Header::default(), claims, &self.encoding_key)
            .map_err(|e| JwtError::JwtError(e.to_string()))
    }

    /// Verify and decode a JWT token
    ///
    /// # Arguments
    /// - `token`: JWT token string
    ///
    /// # Returns
    /// - Decoded claims if token is valid
    ///
    /// # Errors
    /// - Returns error if token is invalid, expired, or signature doesn't match
    #[allow(dead_code)]
    pub fn verify_token(&self, token: &str) -> Result<Claims, JwtError> {
        let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
        // Allow any audience
        validation.set_audience(&["syntaxis-api"]);

        let token_data: TokenData<Claims> = decode(token, &self.decoding_key, &validation)
            .map_err(|e| {
                let error_message = e.to_string().to_lowercase();
                if error_message.contains("invalidsignature")
                    || error_message.contains("invalid signature")
                {
                    JwtError::InvalidSignature
                } else if error_message.contains("invalid token") {
                    JwtError::InvalidFormat
                } else if error_message.contains("expiredsignature")
                    || error_message.contains("token expired")
                {
                    JwtError::TokenExpired
                } else if error_message.contains("invalid claims")
                    || error_message.contains("invalidaudience")
                    || error_message.contains("invalidsubject")
                {
                    JwtError::InvalidClaims
                } else {
                    JwtError::JwtError(e.to_string())
                }
            })?;

        Ok(token_data.claims)
    }

    /// Get the expiration time in seconds
    #[allow(dead_code)]
    pub fn expiration_seconds(&self) -> i64 {
        self.expiration_seconds
    }

    /// Get the algorithm used
    #[allow(dead_code)]
    pub fn algorithm(&self) -> &str {
        &self.algorithm
    }
}

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

    const TEST_SECRET: &str = "super_secret_key_that_is_long_enough";

    #[test]
    fn test_jwt_provider_creation() {
        let provider = JwtProvider::new(TEST_SECRET, 3600);
        assert!(provider.is_ok());
        let provider = provider.unwrap();
        assert_eq!(provider.expiration_seconds(), 3600);
        assert_eq!(provider.algorithm(), "HS256");
    }

    #[test]
    fn test_jwt_provider_short_secret() {
        let provider = JwtProvider::new("short", 3600);
        assert!(provider.is_err());
        assert_eq!(provider.unwrap_err(), JwtError::SecretKeyTooShort);
    }

    #[test]
    fn test_claims_creation() {
        let claims = Claims::new("user-123", Duration::hours(1));
        assert_eq!(claims.sub, "user-123");
        assert_eq!(claims.aud, "syntaxis-api");
        assert!(claims.roles.is_empty());
        assert!(!claims.is_expired());
    }

    #[test]
    fn test_claims_with_roles() {
        let claims = Claims::new("user-123", Duration::hours(1))
            .with_role("admin")
            .with_role("user");

        assert_eq!(claims.roles, vec!["admin", "user"]);
    }

    #[test]
    fn test_claims_with_roles_vec() {
        let roles = vec!["admin".to_string(), "user".to_string()];
        let claims = Claims::new("user-123", Duration::hours(1)).with_roles(roles.clone());

        assert_eq!(claims.roles, roles);
    }

    #[test]
    fn test_claims_with_tenant_id() {
        let claims = Claims::new("user-123", Duration::hours(1)).with_tenant_id("tenant-456");

        assert_eq!(claims.tenant_id, Some("tenant-456".to_string()));
    }

    #[test]
    fn test_claims_with_api_version() {
        let claims = Claims::new("user-123", Duration::hours(1)).with_api_version("v1");

        assert_eq!(claims.api_version, Some("v1".to_string()));
    }

    #[test]
    fn test_token_creation_and_verification() {
        let provider = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let token = provider.create_token("user-123").unwrap();

        assert!(!token.is_empty());
        assert!(token.contains('.'));

        let claims = provider.verify_token(&token).unwrap();
        assert_eq!(claims.sub, "user-123");
    }

    #[test]
    fn test_token_with_custom_claims() {
        let provider = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let custom_claims = Claims::new("user-123", Duration::hours(1))
            .with_role("admin")
            .with_tenant_id("tenant-456")
            .with_api_version("v1");

        let token = provider.encode_token(&custom_claims).unwrap();
        let decoded = provider.verify_token(&token).unwrap();

        assert_eq!(decoded.sub, "user-123");
        assert_eq!(decoded.roles, vec!["admin"]);
        assert_eq!(decoded.tenant_id, Some("tenant-456".to_string()));
        assert_eq!(decoded.api_version, Some("v1".to_string()));
    }

    #[test]
    fn test_token_with_invalid_signature() {
        let provider1 = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let provider2 = JwtProvider::new("different_secret_key_long_enough", 3600).unwrap();

        let token = provider1.create_token("user-123").unwrap();
        let result = provider2.verify_token(&token);

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), JwtError::InvalidSignature);
    }

    #[test]
    fn test_claims_expiration() {
        // Create claims that expire immediately (negative duration means already expired)
        let claims = Claims::new("user-123", Duration::seconds(-10));

        assert!(claims.is_expired());
    }

    #[test]
    fn test_claims_not_expired() {
        let claims = Claims::new("user-123", Duration::hours(1));
        assert!(!claims.is_expired());

        let seconds_until_expiry = claims.seconds_until_expiry();
        assert!(seconds_until_expiry > 0);
        assert!(seconds_until_expiry <= 3600);
    }

    #[test]
    fn test_invalid_token_format() {
        let provider = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let result = provider.verify_token("not.a.valid.token");

        assert!(result.is_err());
    }

    #[test]
    fn test_malformed_token() {
        let provider = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let result = provider.verify_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");

        assert!(result.is_err());
    }

    #[test]
    fn test_multiple_providers_same_secret() {
        let provider1 = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let provider2 = JwtProvider::new(TEST_SECRET, 3600).unwrap();

        let token = provider1.create_token("user-123").unwrap();
        let claims = provider2.verify_token(&token).unwrap();

        assert_eq!(claims.sub, "user-123");
    }

    #[test]
    fn test_token_claims_audience() {
        let provider = JwtProvider::new(TEST_SECRET, 3600).unwrap();
        let token = provider.create_token("user-123").unwrap();
        let claims = provider.verify_token(&token).unwrap();

        assert_eq!(claims.aud, "syntaxis-api");
    }

    #[test]
    fn test_claims_timestamps() {
        let before = Utc::now().timestamp();
        let claims = Claims::new("user-123", Duration::hours(1));
        let after = Utc::now().timestamp();

        // iat should be between before and after
        assert!(claims.iat >= before && claims.iat <= after);

        // exp should be approximately 1 hour after iat
        assert!(claims.exp > claims.iat);
        assert!(claims.exp - claims.iat >= 3599 && claims.exp - claims.iat <= 3601);
    }

    #[test]
    fn test_error_display() {
        assert_eq!(JwtError::InvalidFormat.to_string(), "Invalid token format");
        assert_eq!(JwtError::TokenExpired.to_string(), "Token expired");
        assert_eq!(JwtError::InvalidSignature.to_string(), "Invalid signature");
        assert_eq!(JwtError::InvalidClaims.to_string(), "Invalid claims");
        assert_eq!(
            JwtError::SecretKeyTooShort.to_string(),
            "Secret key must be at least 32 characters long"
        );
    }
}
