//! JWT token generation and validation

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

/// JWT-related errors
#[derive(Debug, Error)]
pub enum JwtError {
    #[error("Failed to create token: {0}")]
    CreateError(String),

    #[error("Invalid token: {0}")]
    InvalidToken(String),

    #[error("Token expired")]
    Expired,

    #[error("Missing claim: {0}")]
    MissingClaim(String),
}

/// JWT configuration
#[derive(Debug, Clone)]
pub struct JwtConfig {
    /// Secret key for signing tokens
    pub secret: String,
    /// Token expiration in seconds
    pub expiration_seconds: i64,
    /// Token issuer
    pub issuer: Option<String>,
    /// Token audience
    pub audience: Option<String>,
}

impl JwtConfig {
    pub fn new(secret: impl Into<String>, expiration_seconds: i64) -> Self {
        Self {
            secret: secret.into(),
            expiration_seconds,
            issuer: None,
            audience: None,
        }
    }

    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
        self.issuer = Some(issuer.into());
        self
    }

    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
        self.audience = Some(audience.into());
        self
    }
}

/// Standard JWT claims
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
    /// Subject (user ID)
    pub sub: String,
    /// Issued at timestamp
    pub iat: i64,
    /// Expiration timestamp
    pub exp: i64,
    /// Issuer
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iss: Option<String>,
    /// Audience
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aud: Option<String>,
    /// User email
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    /// Custom claims
    #[serde(flatten)]
    pub custom: serde_json::Map<String, serde_json::Value>,
}

impl JwtClaims {
    /// Create new claims with subject and expiration
    pub fn new(subject: impl Into<String>, expires_in: Duration) -> Self {
        let now = Utc::now();
        Self {
            sub: subject.into(),
            iat: now.timestamp(),
            exp: (now + expires_in).timestamp(),
            iss: None,
            aud: None,
            email: None,
            custom: serde_json::Map::new(),
        }
    }

    pub fn with_email(mut self, email: impl Into<String>) -> Self {
        self.email = Some(email.into());
        self
    }

    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
        self.iss = Some(issuer.into());
        self
    }

    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
        self.aud = Some(audience.into());
        self
    }

    pub fn add_custom_claim(
        mut self,
        key: impl Into<String>,
        value: impl Into<serde_json::Value>,
    ) -> Self {
        self.custom.insert(key.into(), value.into());
        self
    }

    /// Check if token is expired
    pub fn is_expired(&self) -> bool {
        Utc::now().timestamp() > self.exp
    }
}

/// Create a JWT token
///
/// # Example
///
/// ```rust
/// use lmrc_http_common::auth::{JwtConfig, JwtClaims, create_token};
/// use chrono::Duration;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let config = JwtConfig::new("my_secret_key", 3600);
/// let claims = JwtClaims::new("user_123", Duration::hours(1))
///     .with_email("user@example.com");
///
/// let token = create_token(&claims, &config)?;
/// # Ok(())
/// # }
/// ```
pub fn create_token(claims: &JwtClaims, config: &JwtConfig) -> Result<String, JwtError> {
    encode(
        &Header::default(),
        claims,
        &EncodingKey::from_secret(config.secret.as_bytes()),
    )
    .map_err(|e| JwtError::CreateError(e.to_string()))
}

/// Verify and decode a JWT token
///
/// # Example
///
/// ```rust
/// use lmrc_http_common::auth::{JwtConfig, JwtClaims, create_token, verify_token};
/// use chrono::Duration;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let config = JwtConfig::new("my_secret_key", 3600);
/// let claims = JwtClaims::new("user_123", Duration::hours(1));
///
/// let token = create_token(&claims, &config)?;
/// let decoded = verify_token(&token, &config)?;
///
/// assert_eq!(decoded.sub, "user_123");
/// # Ok(())
/// # }
/// ```
pub fn verify_token(token: &str, config: &JwtConfig) -> Result<JwtClaims, JwtError> {
    let mut validation = Validation::default();

    if let Some(ref iss) = config.issuer {
        validation.set_issuer(&[iss]);
    }

    if let Some(ref aud) = config.audience {
        validation.set_audience(&[aud]);
    }

    let token_data = decode::<JwtClaims>(
        token,
        &DecodingKey::from_secret(config.secret.as_bytes()),
        &validation,
    )
    .map_err(|e| {
        if e.to_string().contains("ExpiredSignature") {
            JwtError::Expired
        } else {
            JwtError::InvalidToken(e.to_string())
        }
    })?;

    Ok(token_data.claims)
}

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

    #[test]
    fn test_create_and_verify_token() {
        let config = JwtConfig::new("test_secret", 3600);
        let claims = JwtClaims::new("user_123", Duration::hours(1))
            .with_email("test@example.com");

        let token = create_token(&claims, &config).unwrap();
        let decoded = verify_token(&token, &config).unwrap();

        assert_eq!(decoded.sub, "user_123");
        assert_eq!(decoded.email, Some("test@example.com".to_string()));
    }

    #[test]
    fn test_expired_token() {
        let config = JwtConfig::new("test_secret", 3600);
        let mut claims = JwtClaims::new("user_123", Duration::hours(1));
        // Force expiration
        claims.exp = Utc::now().timestamp() - 100;

        let token = create_token(&claims, &config).unwrap();
        let result = verify_token(&token, &config);

        assert!(matches!(result, Err(JwtError::Expired)));
    }

    #[test]
    fn test_invalid_secret() {
        let config = JwtConfig::new("test_secret", 3600);
        let claims = JwtClaims::new("user_123", Duration::hours(1));

        let token = create_token(&claims, &config).unwrap();

        let wrong_config = JwtConfig::new("wrong_secret", 3600);
        let result = verify_token(&token, &wrong_config);

        assert!(matches!(result, Err(JwtError::InvalidToken(_))));
    }
}
