//! HS256 JWT validation and role-based access control for provisioning-daemon.

use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use provisioning_core::protocol::ToolCategory;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AuthError {
    #[error("missing or malformed Authorization header")]
    MissingToken,
    #[error("invalid token: {0}")]
    Invalid(String),
    #[error("insufficient role: need {required}, got {actual:?}")]
    Forbidden { required: &'static str, actual: Vec<String> },
    #[error("HS256 secret must be at least 32 bytes (got {0})")]
    WeakSecret(usize),
}

/// RFC 7518 §3.2 — HS256 HMAC key length must be >= hash output (32 bytes).
const MIN_HS256_SECRET_BYTES: usize = 32;

// ── Claims ────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
    /// Subject — user or service identity.
    pub sub: String,
    /// Expiry (Unix timestamp).
    pub exp: u64,
    /// Roles granted to this principal.
    #[serde(default)]
    pub roles: Vec<String>,
}

impl Claims {
    pub fn has_role(&self, role: &str) -> bool {
        self.roles.iter().any(|r| r == role)
    }
}

// ── JwtConfig ─────────────────────────────────────────────────────────────────

#[derive(Clone)]
pub struct JwtConfig {
    encoding_key: EncodingKey,
    decoding_key: DecodingKey,
    validation: Validation,
}

impl JwtConfig {
    /// Build from a shared HMAC-SHA256 secret. Rejects secrets shorter than 32 bytes
    /// per RFC 7518 §3.2 (HMAC key length must match hash output).
    pub fn from_secret(secret: &[u8]) -> Result<Self, AuthError> {
        if secret.len() < MIN_HS256_SECRET_BYTES {
            return Err(AuthError::WeakSecret(secret.len()));
        }
        let mut validation = Validation::new(Algorithm::HS256);
        validation.validate_exp = true;
        Ok(Self {
            encoding_key: EncodingKey::from_secret(secret),
            decoding_key: DecodingKey::from_secret(secret),
            validation,
        })
    }

    pub fn sign(&self, claims: &Claims) -> Result<String, AuthError> {
        encode(&Header::default(), claims, &self.encoding_key)
            .map_err(|e| AuthError::Invalid(e.to_string()))
    }

    pub fn validate(&self, token: &str) -> Result<Claims, AuthError> {
        decode::<Claims>(token, &self.decoding_key, &self.validation)
            .map(|td| td.claims)
            .map_err(|e| AuthError::Invalid(e.to_string()))
    }

    /// Extract Bearer token from an `Authorization` header value.
    /// Scheme match is case-insensitive per RFC 6750 §2.1.
    pub fn extract_bearer(header: &str) -> Result<&str, AuthError> {
        let (scheme, token) = header.split_once(' ').ok_or(AuthError::MissingToken)?;
        if !scheme.eq_ignore_ascii_case("bearer") || token.is_empty() {
            return Err(AuthError::MissingToken);
        }
        Ok(token)
    }
}

// ── RbacPolicy ────────────────────────────────────────────────────────────────

/// Maps `ToolCategory` to the minimum required role.
pub struct RbacPolicy;

impl RbacPolicy {
    /// Returns `Ok(())` when `claims` satisfies the category's required role.
    pub fn check(claims: &Claims, category: ToolCategory) -> Result<(), AuthError> {
        let required = Self::required_role(category);
        if required.is_empty() || claims.has_role(required) || claims.has_role("admin") {
            Ok(())
        } else {
            Err(AuthError::Forbidden {
                required,
                actual: claims.roles.clone(),
            })
        }
    }

    fn required_role(category: ToolCategory) -> &'static str {
        match category {
            ToolCategory::Read => "",             // any authenticated caller
            ToolCategory::Mutation => "operator",
            ToolCategory::Destructive => "admin",
            ToolCategory::Admin => "admin",
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{
        sync::OnceLock,
        time::{SystemTime, UNIX_EPOCH},
    };

    // Both ring and aws-lc-rs are in the dep tree (surrealdb pulls both);
    // jsonwebtoken panics unless exactly one CryptoProvider is installed.
    static CRYPTO: OnceLock<()> = OnceLock::new();
    fn setup_crypto() {
        CRYPTO.get_or_init(|| {
            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
        });
    }

    fn now_plus(secs: u64) -> u64 {
        SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + secs
    }

    fn cfg() -> JwtConfig {
        JwtConfig::from_secret(b"test-secret-key-long-enough-for-hs256-32b").expect("32+ bytes")
    }

    fn claims(roles: &[&str]) -> Claims {
        Claims { sub: "test".into(), exp: now_plus(3600), roles: roles.iter().map(|r| r.to_string()).collect() }
    }

    #[test]
    fn sign_and_validate_roundtrip() {
        setup_crypto();
        let cfg = cfg();
        let c = claims(&["operator"]);
        let token = cfg.sign(&c).unwrap();
        let back = cfg.validate(&token).unwrap();
        assert_eq!(back.sub, "test");
        assert!(back.has_role("operator"));
    }

    #[test]
    fn expired_token_rejected() {
        setup_crypto();
        let cfg = cfg();
        let c = Claims { sub: "t".into(), exp: 1, roles: vec![] };
        let token = cfg.sign(&c).unwrap();
        assert!(cfg.validate(&token).is_err());
    }

    #[test]
    fn rbac_read_allows_any() {
        let c = claims(&[]);
        assert!(RbacPolicy::check(&c, ToolCategory::Read).is_ok());
    }

    #[test]
    fn rbac_mutation_requires_operator() {
        let c_plain   = claims(&[]);
        let c_operator = claims(&["operator"]);
        assert!(RbacPolicy::check(&c_plain,    ToolCategory::Mutation).is_err());
        assert!(RbacPolicy::check(&c_operator, ToolCategory::Mutation).is_ok());
    }

    #[test]
    fn rbac_destructive_requires_admin() {
        let c_operator = claims(&["operator"]);
        let c_admin    = claims(&["admin"]);
        assert!(RbacPolicy::check(&c_operator, ToolCategory::Destructive).is_err());
        assert!(RbacPolicy::check(&c_admin,    ToolCategory::Destructive).is_ok());
    }

    #[test]
    fn extract_bearer_strips_prefix() {
        assert_eq!(JwtConfig::extract_bearer("Bearer abc123").unwrap(), "abc123");
        assert!(JwtConfig::extract_bearer("Basic xyz").is_err());
        assert!(JwtConfig::extract_bearer("Bearer ").is_err());
    }

    #[test]
    fn extract_bearer_is_case_insensitive() {
        assert_eq!(JwtConfig::extract_bearer("bearer tok").unwrap(), "tok");
        assert_eq!(JwtConfig::extract_bearer("BEARER tok").unwrap(), "tok");
        assert_eq!(JwtConfig::extract_bearer("BeArEr tok").unwrap(), "tok");
    }

    #[test]
    fn from_secret_rejects_short_key() {
        let result = JwtConfig::from_secret(b"too-short");
        assert!(matches!(result.err(), Some(AuthError::WeakSecret(9))));
    }

    #[test]
    fn from_secret_accepts_32_byte_key() {
        assert!(JwtConfig::from_secret(&[0u8; 32]).is_ok());
    }
}
