//! API tokens for Obnam server.

use pasetors::{
    self,
    claims::{Claims, ClaimsValidationRules},
    keys::{AsymmetricKeyPair, Generate},
    public,
    token::UntrustedToken,
    version4::V4,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::config::{KeyPairFile, ServerConfig, ServerConfigError};

/// A claim in the API token. The claim represents an allowed action
/// in the API.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ObnamClaim {
    /// Retrieve a chunk if you know its ID.
    ReadChunk,

    /// Create a new chunk.
    CreateChunk,

    /// Find chunks by label.
    FindChunks,

    /// Delete a chunk if you know its ID.
    DeleteChunk,
}

impl ObnamClaim {
    /// Serialize claim to a static string.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::ReadChunk => "obnam_read_chunk",
            Self::CreateChunk => "obnam_write_chunk",
            Self::FindChunks => "obnam_find_chunks",
            Self::DeleteChunk => "obnam_delete_chunk",
        }
    }
}

/// Attempt to create a claim from a string.
impl TryFrom<&str> for ObnamClaim {
    type Error = ObnamClaimsError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "obnam_read" => Ok(Self::ReadChunk),
            "obnam_write" => Ok(Self::CreateChunk),
            "obnam_delete" => Ok(Self::DeleteChunk),
            _ => Err(ObnamClaimsError::Unknown(value.to_string())),
        }
    }
}

/// Errors from handling claims.
#[derive(Debug, thiserror::Error)]
pub enum ObnamClaimsError {
    #[error("unknown Obnam token claim {0:?}")]
    Unknown(String),
}

/// Construct and validate API tokens.
#[derive(Debug)]
pub struct TokenFactory {
    keypair: AsymmetricKeyPair<V4>,
    validation_rules: ClaimsValidationRules,
    server_id: String,
    server_url: String,
}

impl TryFrom<&ServerConfig> for TokenFactory {
    type Error = TokenFactoryError;

    fn try_from(config: &ServerConfig) -> Result<Self, Self::Error> {
        let url = config.listen();
        Ok(Self {
            keypair: config.key_pair()?.clone(),
            validation_rules: Self::validation_rules(url, url),
            server_id: url.to_string(),
            server_url: url.to_string(),
        })
    }
}

impl TokenFactory {
    fn validation_rules(id: &str, url: &str) -> ClaimsValidationRules {
        let mut rules = ClaimsValidationRules::new();
        rules.allow_non_expiring();
        rules.validate_issuer_with(id);
        rules.validate_audience_with(url);
        rules
    }

    /// Create a new token factory for a given server instance. Generate a new key pair.
    pub fn new<S: AsRef<str>, U: AsRef<str>>(
        server_id: S,
        server_url: U,
    ) -> Result<Self, TokenFactoryError> {
        let server_id = server_id.as_ref();
        let server_url = server_url.as_ref();
        Ok(Self {
            keypair: AsymmetricKeyPair::<V4>::generate()
                .map_err(TokenFactoryError::GenerateKeyPair)?,
            validation_rules: Self::validation_rules(server_id, server_url),
            server_id: server_id.to_string(),
            server_url: server_url.to_string(),
        })
    }

    /// Return key pair file so it can be written to a file.
    pub fn key_pair(&self) -> KeyPairFile {
        KeyPairFile::from(&self.keypair)
    }

    /// Create a new token with the specified claims.
    pub fn create_token<S: AsRef<str>>(
        &self,
        user: S,
        extra: &[ObnamClaim],
    ) -> Result<String, TokenFactoryError> {
        let mut claims = Claims::new().map_err(TokenFactoryError::Claims)?;
        claims.non_expiring();
        claims
            .issuer(&self.server_id)
            .map_err(TokenFactoryError::Claim)?;
        claims
            .audience(&self.server_url)
            .map_err(TokenFactoryError::Claim)?;
        claims
            .subject(user.as_ref())
            .map_err(TokenFactoryError::Claim)?;
        for claim in extra {
            claims
                .add_additional(claim.as_str(), true)
                .map_err(TokenFactoryError::AdditionalClaim)?;
        }
        public::sign(&self.keypair.secret, &claims, None, None).map_err(TokenFactoryError::Sign)
    }

    /// Verify that a token is valid.
    pub fn validate(&self, untrusted: &str) -> Result<ObnamTrustedToken, TokenFactoryError> {
        let untrusted =
            UntrustedToken::try_from(untrusted).map_err(TokenFactoryError::Untrusted)?;

        let trusted = public::verify(
            &self.keypair.public,
            &untrusted,
            &self.validation_rules,
            None,
            None,
        )
        .map_err(TokenFactoryError::Verify)?;

        if let Some(claims) = trusted.payload_claims() {
            Ok(ObnamTrustedToken::from(claims))
        } else {
            Ok(ObnamTrustedToken::default())
        }
    }
}

/// Errors from using a token factory.
#[derive(Debug, thiserror::Error)]
pub enum TokenFactoryError {
    #[error("failed to generate key pair")]
    GenerateKeyPair(#[source] pasetors::errors::Error),

    #[error("failed to create a claims data structure")]
    Claims(#[source] pasetors::errors::Error),

    #[error("failed to set a claim in token")]
    Claim(#[source] pasetors::errors::Error),

    #[error("failed to set an additional claim in token")]
    AdditionalClaim(#[source] pasetors::errors::Error),

    #[error("failed to sign token")]
    Sign(#[source] pasetors::errors::Error),

    #[error("failed to create untrusted token")]
    Untrusted(#[source] pasetors::errors::Error),

    #[error("failed to verify token")]
    Verify(#[source] pasetors::errors::Error),

    #[error(transparent)]
    ServerConfig(#[from] ServerConfigError),
}

/// A validated API token.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ObnamTrustedToken {
    read_chunks_allowed: bool,
    find_chunks_allowed: bool,
    create_chunk_allowed: bool,
    delete_chunk_allowed: bool,
}

#[allow(dead_code)]
impl ObnamTrustedToken {
    /// Does token allow reading chunks?
    pub fn read_chunk_allowed(&self) -> bool {
        self.read_chunks_allowed
    }

    /// Does token allow finding chunks?
    pub fn find_chunks_allowed(&self) -> bool {
        self.find_chunks_allowed
    }

    /// Does token allow creating a chunk?
    pub fn create_chunk_allowed(&self) -> bool {
        self.create_chunk_allowed
    }

    /// Does token allow deleting a chunk?
    pub fn delete_allowed(&self) -> bool {
        self.delete_chunk_allowed
    }
}

impl From<&Claims> for ObnamTrustedToken {
    fn from(value: &Claims) -> Self {
        fn is_true(value: &Claims, claim: ObnamClaim) -> bool {
            value.get_claim(claim.as_str()) == Some(&Value::Bool(true))
        }
        Self {
            read_chunks_allowed: is_true(value, ObnamClaim::ReadChunk),
            find_chunks_allowed: is_true(value, ObnamClaim::FindChunks),
            create_chunk_allowed: is_true(value, ObnamClaim::CreateChunk),
            delete_chunk_allowed: is_true(value, ObnamClaim::DeleteChunk),
        }
    }
}

impl std::fmt::Display for ObnamTrustedToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{self:?}")
    }
}
