//! JWT Ed25519 signing and verification.
//!
//! Guaranteed properties:
//! - Algorithm: EdDSA (Ed25519) only — not HS256/RS256.
//! - `kid` required in the header: verification rejects tokens with an absent or mismatched `kid`.
//! - Strict audience (`aud` exact match) and `exp` validated, leeway = 0.
//! - Scope-based TTL: `TokenScope::Human` → `ttl_human_secs` (default 3600 s),
//!   `TokenScope::Service` → `ttl_service_secs` (default 86400 s).
//! - `jti` unique ULID per token (used for revocation via `RevocationStore`).
//!
//! ## Public API
//!
//! ```rust,no_run
//! use ed25519_dalek::SigningKey;
//! use gradatum_auth::jwt::{JwtService, TokenScope};
//!
//! let mut rng = rand::rngs::OsRng;
//! let signing = SigningKey::generate(&mut rng);
//! let svc = JwtService::new(signing, "kid-2026".into(), "gradatum".into(), 3600, 86400);
//! let token = svc.sign("user-1", &["read".into()], TokenScope::Human, "main").unwrap();
//! let claims = svc.verify(&token).unwrap();
//! assert_eq!(claims.sub, "user-1");
//! assert_eq!(claims.tenant_id, "main");
//! ```

use std::time::{SystemTime, UNIX_EPOCH};

use ed25519_dalek::{SigningKey, VerifyingKey};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use pkcs8::EncodePrivateKey;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;

/// JWT error variants.
#[derive(Debug, thiserror::Error)]
pub enum JwtError {
    /// Token audience does not match the audience expected by this service.
    #[error("audience invalide")]
    InvalidAudience,

    /// Header `kid` does not match the `kid` configured in this `JwtService`.
    #[error("kid invalide ou absent")]
    InvalidKid,

    /// Token is expired (`exp < now`, leeway = 0).
    #[error("token expiré")]
    Expired,

    /// Token is malformed, has an invalid signature, or uses an incorrect algorithm.
    #[error("token malformé : {0}")]
    Malformed(#[from] jsonwebtoken::errors::Error),

    /// System clock read error.
    #[error("erreur horloge système : {0}")]
    Time(#[from] std::time::SystemTimeError),
}

/// Scope used to select the TTL of a token.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenScope {
    /// Human user or studio session — short TTL (default 3600 s = 1 h).
    Human,
    /// Machine service (static MCP stub) — long TTL (default 86400 s = 24 h).
    Service,
}

/// JWT claims included in the token payload.
///
/// Standard JWT fields: `sub`, `aud`, `iat`, `exp`, `jti`.
/// Custom fields: `scopes` (permission list) and `tenant_id` (multi-tenancy invariant).
///
/// `tenant_id` is required in all tokens issued by this service.
/// Use `"main"` for the root tenant.
/// Fine-grained multi-tenancy is planned for a future version.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Claims {
    /// Subject — identifier of the authenticated entity.
    pub sub: String,
    /// Audience — target service name (must match the `audience` of the `JwtService` exactly).
    pub aud: String,
    /// Issued-at (UNIX timestamp, seconds).
    pub iat: u64,
    /// Expiry (UNIX timestamp, seconds). `exp - iat` equals the TTL for the given scope.
    pub exp: u64,
    /// Unique JWT ID (ULID) — used for revocation via `RevocationStore`.
    pub jti: String,
    /// Permissions granted to this token.
    pub scopes: Vec<String>,
    /// Target tenant. Use `"main"` for the root tenant.
    pub tenant_id: String,
}

/// JWT Ed25519 service: signs and verifies tokens for a given audience and `kid`.
///
/// Instantiate once at startup and share behind an `Arc<JwtService>` in `AppState`.
pub struct JwtService {
    signing: SigningKey,
    verifying: VerifyingKey,
    kid: String,
    audience: String,
    ttl_human_secs: u64,
    ttl_service_secs: u64,
}

impl JwtService {
    /// Creates a new `JwtService`.
    ///
    /// # Arguments
    /// - `signing`: Ed25519 private key (generated by `ed25519_dalek::SigningKey::generate`)
    /// - `kid`: key identifier (required in the JWT header)
    /// - `audience`: exact audience of issued tokens (e.g. `"gradatum"`)
    /// - `ttl_human_secs`: TTL for `Human` tokens (recommended default: 3600)
    /// - `ttl_service_secs`: TTL for `Service` tokens (recommended default: 86400)
    pub fn new(
        signing: SigningKey,
        kid: String,
        audience: String,
        ttl_human_secs: u64,
        ttl_service_secs: u64,
    ) -> Self {
        let verifying = signing.verifying_key();
        Self {
            signing,
            verifying,
            kid,
            audience,
            ttl_human_secs,
            ttl_service_secs,
        }
    }

    /// Builds a `JwtService` from the raw bytes of an Ed25519 seed (32 bytes).
    ///
    /// Used at production startup to load a key persisted by [`FileSecretsProvider`]
    /// (or any other [`SecretsProvider`]). The bytes must be the raw seed of an Ed25519
    /// `SigningKey` (ed25519-dalek format: 32 bytes, not PEM, not PKCS8).
    ///
    /// # Errors
    ///
    /// Returns `Err(&'static str)` if `bytes.len() != 32`.
    ///
    /// # Security
    ///
    /// Do not log the bytes passed to this function. The caller must obtain them
    /// via `secrecy::ExposeSecret::expose_secret` and pass them directly.
    ///
    /// [`FileSecretsProvider`]: gradatum_core::secrets::FileSecretsProvider
    /// [`SecretsProvider`]: gradatum_core::secrets::SecretsProvider
    pub fn from_signing_bytes(
        bytes: &[u8],
        kid: String,
        audience: String,
        ttl_human_secs: u64,
        ttl_service_secs: u64,
    ) -> Result<Self, &'static str> {
        if bytes.len() != 32 {
            return Err("clé Ed25519 doit être 32 bytes (seed raw)");
        }
        // SAFETY : slice de 32 bytes garanti par le check ci-dessus.
        let seed: [u8; 32] = bytes
            .try_into()
            .expect("slice de 32 bytes → array[32] ne peut pas échouer");
        let signing = SigningKey::from_bytes(&seed);
        Ok(Self::new(
            signing,
            kid,
            audience,
            ttl_human_secs,
            ttl_service_secs,
        ))
    }

    /// Generates a new Ed25519 key via `OsRng` and returns the raw seed wrapped in zeroize-on-drop.
    ///
    /// Used at first boot when no persisted key exists.
    /// The returned bytes must be persisted atomically (chmod 600) via
    /// [`FileSecretsProvider::write_atomic`].
    ///
    /// The seed is wrapped in [`Zeroizing`]: it is zeroed in memory as soon as the
    /// `Zeroizing<[u8; 32]>` is dropped — no exposure window.
    ///
    /// [`FileSecretsProvider::write_atomic`]: gradatum_core::secrets::FileSecretsProvider::write_atomic
    pub fn generate_signing_bytes() -> (Zeroizing<[u8; 32]>, SigningKey) {
        let mut rng = rand::rngs::OsRng;
        let signing = SigningKey::generate(&mut rng);
        // to_bytes() retourne la seed raw 32 bytes ; on la wrape dans Zeroizing
        // pour garantir l'effacement mémoire dès que l'appelant n'en a plus besoin.
        let seed = Zeroizing::new(signing.to_bytes());
        (seed, signing)
    }

    /// Creates a `JwtService` with an ephemeral Ed25519 key (dev/test only).
    ///
    /// The key is generated via `OsRng` on each call and is not persisted.
    /// All tokens issued by this instance are invalid after a restart.
    ///
    /// **WARN:** never use in production — use [`JwtService::new`]
    /// with a key loaded from `cfg.auth.jwt_private_key_path`.
    pub fn new_ephemeral() -> Self {
        let mut rng = rand::rngs::OsRng;
        let signing = SigningKey::generate(&mut rng);
        tracing::warn!(
            "JwtService initialisé avec une clé éphémère — \
            UNIQUEMENT acceptable en dev/test. \
            Configurer jwt_private_key_path en production."
        );
        Self::new(
            signing,
            "ephemeral-dev".to_string(),
            "gradatum".to_string(),
            3600,
            86400,
        )
    }

    /// Signs a JWT Ed25519 token.
    ///
    /// The TTL is determined by `scope`:
    /// - `TokenScope::Human` → `ttl_human_secs`
    /// - `TokenScope::Service` → `ttl_service_secs`
    ///
    /// The `kid` header is always set.
    /// The `jti` is a unique ULID generated on each call.
    ///
    /// # Arguments
    /// - `sub`: subject (owner/agent identifier)
    /// - `scopes`: permissions granted
    /// - `scope`: TTL scope (`Human` = 1 h, `Service` = 24 h)
    /// - `tenant_id`: target tenant — use `"main"` for the root tenant
    ///
    /// # Errors
    /// - `JwtError::Time` if the system clock is before `UNIX_EPOCH` (impossible in production)
    /// - `JwtError::Malformed` if the `jsonwebtoken` encoding fails (should not occur)
    pub fn sign(
        &self,
        sub: &str,
        scopes: &[String],
        scope: TokenScope,
        tenant_id: &str,
    ) -> Result<String, JwtError> {
        let iat = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        let ttl = match scope {
            TokenScope::Human => self.ttl_human_secs,
            TokenScope::Service => self.ttl_service_secs,
        };

        let claims = Claims {
            sub: sub.to_string(),
            aud: self.audience.clone(),
            iat,
            exp: iat + ttl,
            jti: ulid::Ulid::new().to_string(),
            scopes: scopes.to_vec(),
            tenant_id: tenant_id.to_string(),
        };

        let mut header = Header::new(Algorithm::EdDSA);
        header.kid = Some(self.kid.clone());

        // to_pkcs8_der() sur une SigningKey Ed25519 valide ne peut pas échouer :
        // c'est une sérialisation déterministe de la seed privée en PKCS8 v1 DER.
        // ring::Ed25519KeyPair::from_pkcs8_maybe_unchecked attend exactement ce format (48 bytes).
        let priv_der = self
            .signing
            .to_pkcs8_der()
            .expect("ed25519 SigningKey→PKCS8 DER ne peut pas échouer sur une clé valide");
        let encoding_key = EncodingKey::from_ed_der(priv_der.as_bytes());

        Ok(encode(&header, &claims, &encoding_key)?)
    }

    /// Returns the `kid` configured in this service.
    ///
    /// Used by the server middleware to build `TrustContext::BearerToken`.
    pub fn kid(&self) -> &str {
        &self.kid
    }

    /// Returns the TTL (in seconds) for `TokenScope::Service` tokens.
    ///
    /// Used by `/auth/exchange` to include `ttl_secs` in the response.
    pub fn ttl_service_secs(&self) -> u64 {
        self.ttl_service_secs
    }

    /// Returns the TTL (in seconds) for `TokenScope::Human` tokens.
    ///
    /// Used by `/auth/exchange` when the client requests a human-scoped token
    /// (studio interactive session, default 3600 s = 1 h).
    pub fn ttl_human_secs(&self) -> u64 {
        self.ttl_human_secs
    }

    /// Verifies and decodes a JWT token.
    ///
    /// # Checks performed
    /// 1. `kid` header == `self.kid` (otherwise `JwtError::InvalidKid`)
    /// 2. Algorithm == EdDSA
    /// 3. Valid Ed25519 signature
    /// 4. `aud` == `self.audience` (otherwise `JwtError::InvalidAudience`)
    /// 5. `exp` not exceeded, leeway = 0 (otherwise `JwtError::Expired`)
    ///
    /// # Errors
    /// - `JwtError::InvalidKid` if the header `kid` is absent or mismatched
    /// - `JwtError::InvalidAudience` if the audience does not match
    /// - `JwtError::Expired` if the token is expired
    /// - `JwtError::Malformed` for any other validation error
    pub fn verify(&self, token: &str) -> Result<Claims, JwtError> {
        // Vérification du kid AVANT décodage (fast-fail, pas de crypto inutile).
        let header = jsonwebtoken::decode_header(token)?;
        match header.kid.as_deref() {
            Some(k) if k == self.kid => {}
            _ => return Err(JwtError::InvalidKid),
        }

        // DecodingKey::from_ed_der attend les 32 bytes bruts de la clé publique Ed25519
        // (format ring::UnparsedPublicKey), PAS le SPKI DER (44 bytes).
        // VerifyingKey::as_bytes() retourne exactement les 32 bytes de la clé publique.
        let decoding_key = DecodingKey::from_ed_der(self.verifying.as_bytes());

        let mut validation = Validation::new(Algorithm::EdDSA);
        validation.set_audience(std::slice::from_ref(&self.audience));
        validation.validate_exp = true;
        // jsonwebtoken v9 défaut validate_nbf=false accepterait silencieusement un token avec nbf futur.
        // Activation explicite : rejette tout token dont le claim `nbf` est dans le futur.
        validation.validate_nbf = true;
        validation.leeway = 0;

        let data = decode::<Claims>(token, &decoding_key, &validation).map_err(|e| {
            use jsonwebtoken::errors::ErrorKind;
            match e.kind() {
                ErrorKind::ExpiredSignature => JwtError::Expired,
                ErrorKind::InvalidAudience => JwtError::InvalidAudience,
                _ => JwtError::Malformed(e),
            }
        })?;

        Ok(data.claims)
    }
}
