//! passport-rs
//!
//! A library for generating JWT passports following [RFC-8225](https://tools.ietf.org/html/rfc8225)
//!
//! Usage:
//! ```rust
//! let passport_builder =
//!     passport::PassportBuilder::new(String::from("https://cert.example.org/passport.cer"), passport::Identity::URI(String::from("https://matrix.to/#/@alice:example.org")))
//!         .add_destination(passport::Identity::URI(String::from("https://matrix.to/#/@bob:example.org")))
//!         .set_expires_in(Some(512));
//!
//! let jwt = passport_builder.encode(
//!     &passport::EncodingKey::from_secret(b"test_secret"),
//!     passport::Algorithm::HS512,
//! ).unwrap();
//! ```

/// Structs for representing and parsing JWT data from configuration files
pub mod config;

use std::collections::HashMap;
use std::fmt;

use chrono;
use jsonwebtoken::{DecodingKey, Header, encode};
pub use jsonwebtoken::{Algorithm, EncodingKey};
use serde::{Deserialize, Serialize, Serializer};

/// Wrapper for both JWT and Serde errors
pub enum Error {
    /// Wrapped [`jsonwebtoken::errors::Error`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/errors/struct.Error.html)
    JWT(jsonwebtoken::errors::Error),
    /// Wrapped [`serde_json::Error`](https://docs.serde.rs/serde_json/struct.Error.html)
    Serde(serde_json::Error),
}

impl From<jsonwebtoken::errors::Error> for Error {
    fn from(err: jsonwebtoken::errors::Error) -> Self {
        Self::JWT(err)
    }
}

impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Self {
        Self::Serde(err)
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::JWT(e) => e.fmt(f),
            Error::Serde(e) => e.fmt(f),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::JWT(e) => e.fmt(f),
            Error::Serde(e) => e.fmt(f),
        }
    }
}

/// Given the key and an algorithm it will return an [`EncodingKey`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/struct.EncodingKey.html)
pub fn make_encoding_key(key: &[u8], algorithm: Algorithm) -> jsonwebtoken::errors::Result<EncodingKey> {
    match algorithm {
        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
            Ok(EncodingKey::from_secret(key))
        }
        Algorithm::ES256 | Algorithm::ES384 => {
            EncodingKey::from_ec_pem(key)
        }
        Algorithm::RS256
        | Algorithm::RS384
        | Algorithm::RS512
        | Algorithm::PS256
        | Algorithm::PS384
        | Algorithm::PS512 => EncodingKey::from_rsa_pem(key),
    }
}

/// Given the key and an algorithm it will return a [`DecodingKey`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/struct.DecodingKey.html)
pub fn make_decoding_key(key: &[u8], algorithm: Algorithm) -> jsonwebtoken::errors::Result<DecodingKey<'_>> {
    match algorithm {
        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
            Ok(DecodingKey::from_secret(key))
        }
        Algorithm::ES256 | Algorithm::ES384 => {
            DecodingKey::from_ec_pem(key).map(DecodingKey::into_static)
        }
        Algorithm::RS256
        | Algorithm::RS384
        | Algorithm::RS512
        | Algorithm::PS256
        | Algorithm::PS384
        | Algorithm::PS512 => DecodingKey::from_rsa_pem(key),
    }
}

/// Factory for building passports
pub struct PassportBuilder {
    certificate_url: String,
    claims: PassportClaims,
    expires_in: Option<u32>,
}

impl PassportBuilder {
    /// Creates a new builder
    pub fn new(certificate_url: String, origin: Identity) -> Self {
        Self {
            certificate_url,
            claims: PassportClaims::new(origin),
            expires_in: None,
        }
    }

    /// Adds an entry to the `media_keys` claim for new passports 
    pub fn add_media_key(mut self, key: MediaKey) -> Self {
        self.claims = self.claims.add_media_key(key);
        self
    }

    /// Adds a destination to new passports
    pub fn add_destination(mut self, identity: Identity) -> Self {
        self.claims = self.claims.add_destination(identity);
        self
    }

    /// Optionally passports expire `expires_in` seconds after creation
    pub fn set_expires_in(mut self, expires_in: Option<u32>) -> Self {
        self.expires_in = expires_in;
        self
    }

    /// Creates and encodes a new passport
    pub fn encode(mut self, key: &EncodingKey, algorithm: Algorithm) -> Result<String, Error> {
        let header = Header {
            typ: Some(String::from("passport")),
            alg: algorithm,
            cty: None,
            jku: None,
            kid: None,
            x5u: Some(self.certificate_url.clone()),
            x5t: None,
        };

        self.claims = self.claims.set_issuing_time(self.expires_in);
        Ok(encode(&header, &self.claims, key)?)
    }
}
/// Extended [JWT Claims](https://tools.ietf.org/html/rfc8225#section-5) for passports 
#[derive(Serialize, Deserialize)]
pub struct PassportClaims {
    #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "dest")]
    pub destination: HashMap<IdentityForms, Vec<String>>,

    #[serde(rename = "orig")]
    pub origin: Identity,

    #[serde(rename = "iat")]
    pub issued_at: Option<u32>,

    #[serde(default, skip_serializing_if = "Option::is_none", rename = "exp")]
    pub expires_at: Option<u32>,

    #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "mky")]
    pub media_keys: Vec<MediaKey>,
}

impl PassportClaims {
    fn new(origin: Identity) -> Self {
        Self {
            destination: HashMap::new(),
            origin,
            issued_at: None,
            expires_at: None,
            media_keys: Vec::new(),
        }
    }

    fn add_media_key(mut self, key: MediaKey) -> Self {
        self.media_keys.push(key);
        self
    }

    fn add_destination(mut self, identity: Identity) -> Self {
        let inner = identity.clone().into_inner();
        let key = IdentityForms::from(&identity);
        self.destination
            .entry(key)
            .or_insert(Vec::new())
            .push(inner);
        self
    }

    fn set_issuing_time(mut self, expires_in: Option<u32>) -> Self {
        let now = chrono::Utc::now().timestamp() as u32;
        self.issued_at = Some(now);
        self.expires_at = expires_in.map(|t| t + now);
        self
    }
}

/// Ways of repsenting [identities](https://tools.ietf.org/html/rfc8225#section-5.2.1)
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq)]
pub enum IdentityForms {
    /// [Telephone Number](https://tools.ietf.org/html/rfc8225#section-5.2.1.1)
    #[serde(rename = "tn")]
    TelephoneNumber,
    /// [URI](https://tools.ietf.org/html/rfc8225#section-5.2.1.2)
    #[serde(rename = "uri")]
    URI,
}

/// Represents and holds the different forms of [identification](https://tools.ietf.org/html/rfc8225#section-5.2.1)
#[derive(Deserialize, Clone)]
pub enum Identity {
    /// [Telephone Number](https://tools.ietf.org/html/rfc8225#section-5.2.1.1)
    #[serde(rename = "tn")]
    TelephoneNumber(String),
    /// [URI](https://tools.ietf.org/html/rfc8225#section-5.2.1.2)
    #[serde(rename = "uri")]
    URI(String),
}

impl Serialize for Identity {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut data: HashMap<IdentityForms, String> = HashMap::new();
        data.insert(IdentityForms::from(self), self.clone().into_inner());
        serializer.serialize_newtype_struct("Identity", &data)
    }
}

impl From<&Identity> for IdentityForms {
    fn from(identity: &Identity) -> Self {
        match identity {
            Identity::TelephoneNumber(_) => IdentityForms::TelephoneNumber,
            Identity::URI(_) => IdentityForms::URI,
        }
    }
}

impl Identity {
    fn into_inner(self) -> String {
        match self {
            Self::TelephoneNumber(num) => num,
            Self::URI(uri) => uri,
        }
    }
}

#[derive(Serialize, Deserialize)]
/// Represents the [Media Key](https://tools.ietf.org/html/rfc8225#section-5.2.2) claim
pub struct MediaKey {
    algorithm: String,
    digest: String,
}
