use base64::Engine;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use ops_keeper::types::OpsClaims;

use crate::error::ControllerError;

pub struct JwtAuth {
    decoding_key: DecodingKey,
    workspace: String,
}

impl JwtAuth {
    pub fn from_public_pem(public_pem: &[u8], workspace: String) -> Result<Self, ControllerError> {
        let der = pem_to_der(public_pem)?;
        Ok(Self {
            decoding_key: DecodingKey::from_ed_der(&der),
            workspace,
        })
    }

    /// Validate JWT signature, audience, nbf/exp, and scope.
    /// Returns the decoded claims if all checks pass.
    pub fn validate(&self, token: &str) -> Result<OpsClaims, ControllerError> {
        let mut validation = Validation::new(Algorithm::EdDSA);
        validation.set_audience(&[&self.workspace]);
        validation.validate_nbf = true;

        let claims = jsonwebtoken::decode::<OpsClaims>(token, &self.decoding_key, &validation)
            .map(|td| td.claims)
            .map_err(|e| ControllerError::Auth(e.to_string()))?;

        validate_scopes(&claims)?;
        Ok(claims)
    }
}

/// Verify that the op embedded in claims is covered by at least one declared scope.
fn validate_scopes(claims: &OpsClaims) -> Result<(), ControllerError> {
    let op = &claims.op;
    let covered = claims.scopes.iter().any(|scope| {
        let op_type_ok = scope.op_type == "*" || scope.op_type == op.op_type;
        let target_ok = scope.target_pattern == "*" || op.target.contains(&scope.target_pattern);
        op_type_ok && target_ok
    });
    if !covered {
        return Err(ControllerError::Auth(format!(
            "op '{}/{}' not covered by any declared scope",
            op.op_type, op.target
        )));
    }
    Ok(())
}

fn pem_to_der(pem: &[u8]) -> Result<Vec<u8>, ControllerError> {
    let pem_str = std::str::from_utf8(pem)
        .map_err(|e| ControllerError::Auth(format!("PEM is not UTF-8: {e}")))?;
    let body: String = pem_str
        .lines()
        .filter(|l| !l.starts_with("-----"))
        .collect();
    base64::engine::general_purpose::STANDARD
        .decode(body.as_bytes())
        .map_err(|e| ControllerError::Auth(format!("PEM base64 decode: {e}")))
}

#[cfg(test)]
mod tests {
    use super::*;
    use ops_keeper::types::{OpsClaims, PendingOp, ScopeEntry, SingleOrVec};

    fn make_claims(op_type: &str, target: &str, scopes: Vec<ScopeEntry>) -> OpsClaims {
        OpsClaims {
            iss: "test-keeper".into(),
            sub: "test-sub".into(),
            aud: SingleOrVec::single("test-ws"),
            scopes,
            seq: 1,
            jti: "test-jti".into(),
            expected_state_version: "v1".into(),
            exp: u64::MAX,
            nbf: 0,
            op: PendingOp {
                op_type: op_type.into(),
                target: target.into(),
                sub: "sub".into(),
                expected_state_version: "v1".into(),
                image: String::new(),
                params: serde_json::Value::Null,
            },
        }
    }

    #[test]
    fn scope_covers_exact_match() {
        let claims = make_claims(
            "deploy",
            "staging-vapora",
            vec![ScopeEntry { op_type: "deploy".into(), target_pattern: "staging-vapora".into() }],
        );
        assert!(validate_scopes(&claims).is_ok());
    }

    #[test]
    fn scope_wildcard_op_type_covers_any() {
        let claims = make_claims(
            "scale",
            "staging-vapora",
            vec![ScopeEntry { op_type: "*".into(), target_pattern: "staging-vapora".into() }],
        );
        assert!(validate_scopes(&claims).is_ok());
    }

    #[test]
    fn scope_mismatch_returns_error() {
        let claims = make_claims(
            "deploy",
            "prod-cluster",
            vec![ScopeEntry { op_type: "scale".into(), target_pattern: "staging-vapora".into() }],
        );
        assert!(validate_scopes(&claims).is_err());
    }
}
