extern crate chrono;
extern crate jsonwebtoken as jwt;
extern crate serde;
extern crate serde_json;
extern crate uuid;
extern crate yaml_rust;

use core::chrono::prelude::*;
use core::uuid::Uuid;
use std::collections::HashMap;
use thiserror::Error;

/// Orizentic Errors
#[derive(Debug, Error)]
pub enum Error {
    /// An underlying JWT decoding error. May be replaced with Orizentic semantic errors to better
    /// encapsulate the JWT library.
    #[error("JWT failed to decode: {0}")]
    JWTError(jwt::errors::Error),
    /// Token decoded and verified but was not present in the database.
    #[error("Token not recognized")]
    UnknownToken,
}

/// ResourceName is application-defined and names a resource to which access should be controlled
#[derive(Debug, PartialEq, Clone)]
pub struct ResourceName(pub String);

/// Permissions are application-defined descriptions of what can be done with the named resource
#[derive(Debug, PartialEq, Clone)]
pub struct Permissions(pub Vec<String>);

/// Issuers are typically informative, but should generally describe who or what created the token
#[derive(Debug, PartialEq, Clone)]
pub struct Issuer(pub String);

/// Time to live is the number of seconds until a token expires. This is used for creating tokens
/// but tokens store their actual expiration time.
#[derive(Debug, PartialEq, Clone)]
pub struct TTL(pub chrono::Duration);

/// Username, or Audience in JWT terms, should describe who or what is supposed to be using this
/// token
#[derive(Debug, PartialEq, Clone)]
pub struct Username(String);

impl From<Username> for String {
    fn from(u: Username) -> String {
        u.0.clone()
    }
}

impl From<&str> for Username {
    fn from(s: &str) -> Username {
        Username(s.to_owned())
    }
}

#[derive(Debug, PartialEq, Clone)]
pub struct Secret(pub Vec<u8>);

/// A ClaimSet represents one set of permissions and claims. It is a standardized way of specifying
/// the owner, issuer, expiration time, relevant resources, and specific permissions on that
/// resource. By itself, this is only an informative data structure and so should never be trusted
/// when passed over the wire. See `VerifiedToken` and `UnverifiedToken`.
#[derive(Debug, PartialEq, Clone)]
pub struct ClaimSet {
    pub id: String,
    pub audience: Username,
    pub expiration: Option<DateTime<Utc>>,
    pub issuer: Issuer,
    pub issued_at: DateTime<Utc>,
    pub resource: ResourceName,
    pub permissions: Permissions,
}

impl ClaimSet {
    /// Create a new `ClaimSet`. This will return a claimset with the expiration time calculated
    /// from the TTL if the TTL is provided. No expiration will be set if no TTL is provided.
    pub fn new(
        issuer: Issuer,
        ttl: Option<TTL>,
        resource_name: ResourceName,
        user_name: Username,
        perms: Permissions,
    ) -> ClaimSet {
        let issued_at: DateTime<Utc> = Utc::now().with_nanosecond(0).unwrap();
        let expiration = match ttl {
            Some(TTL(ttl_)) => issued_at.checked_add_signed(ttl_),
            None => None,
        };
        ClaimSet {
            id: String::from(Uuid::new_v4().to_hyphenated().to_string()),
            audience: user_name,
            expiration,
            issuer,
            issued_at,
            resource: resource_name,
            permissions: perms,
        }
    }

    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string(&(ClaimSetJS::from_claimset(self)))
    }

    pub fn from_json(text: &String) -> Result<ClaimSet, serde_json::Error> {
        serde_json::from_str(&text).map(|x| ClaimSetJS::to_claimset(&x))
    }
}

/// ClaimSetJS is an intermediary data structure between JWT serialization and a more usable
/// ClaimSet.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClaimSetJS {
    jti: String,
    aud: String,
    exp: Option<i64>,
    iss: String,
    iat: i64,
    sub: String,
    perms: Vec<String>,
}

impl ClaimSetJS {
    pub fn from_claimset(claims: &ClaimSet) -> ClaimSetJS {
        ClaimSetJS {
            jti: claims.id.clone(),
            aud: claims.audience.0.clone(),
            exp: claims.expiration.map(|t| t.timestamp()),
            iss: claims.issuer.0.clone(),
            iat: claims.issued_at.timestamp(),
            sub: claims.resource.0.clone(),
            perms: claims.permissions.0.clone(),
        }
    }

    pub fn to_claimset(&self) -> ClaimSet {
        ClaimSet {
            id: self.jti.clone(),
            audience: Username(self.aud.clone()),
            expiration: self.exp.map(|t| Utc.timestamp(t, 0)),
            issuer: Issuer(self.iss.clone()),
            issued_at: Utc.timestamp(self.iat, 0),
            resource: ResourceName(self.sub.clone()),
            permissions: Permissions(self.perms.clone()),
        }
    }
}

/// The Orizentic Context encapsulates a set of claims and an associated secret. This provides the
/// overall convenience of easily creating and validating tokens. Generated claimsets are stored
/// here on the theory that, even with validation, only those claims actually stored in the
/// database should be considered valid.
pub struct OrizenticCtx(Secret, HashMap<String, ClaimSet>);

/// An UnverifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. As this
/// is unverified, this should only be used for information purposes, such as determining what a
/// user can do with a token even when the decoding key is absent.
#[derive(Debug)]
pub struct UnverifiedToken {
    pub text: String,
    pub claims: ClaimSet,
}

impl UnverifiedToken {
    /// Decode a JWT text string without verification
    pub fn decode_text(text: String) -> Result<UnverifiedToken, Error> {
        let res = jwt::dangerous_unsafe_decode::<ClaimSetJS>(&text);
        match res {
            Ok(res_) => Ok(UnverifiedToken {
                text,
                claims: res_.claims.to_claimset(),
            }),
            Err(err) => Err(Error::JWTError(err)),
        }
    }
}

/// An VerifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. This will
/// only be created by the `validate_function`, and thus will represent a token which has been
/// validated via signature, expiration time, and presence in the database.
#[derive(Debug)]
pub struct VerifiedToken {
    pub text: String,
    pub claims: ClaimSet,
}

impl VerifiedToken {
    /// Given a `VerifiedToken`, pass the resource name and permissions to a user-defined function. The
    /// function should return true if the caller should be granted access to the resource and false,
    /// otherwise. That result will be passed back to the caller.
    pub fn check_authorizations<F: FnOnce(&ResourceName, &Permissions) -> bool>(
        &self,
        f: F,
    ) -> bool {
        f(&self.claims.resource, &self.claims.permissions)
    }
}

impl OrizenticCtx {
    /// Create a new Orizentic Context with an initial set of claims.
    pub fn new(secret: Secret, claims_lst: Vec<ClaimSet>) -> OrizenticCtx {
        let mut hm = HashMap::new();
        for claimset in claims_lst {
            hm.insert(claimset.id.clone(), claimset);
        }
        OrizenticCtx(secret, hm)
    }

    /// Validate a token by checking its signature, that it is not expired, and that it is still
    /// present in the database. Return an error if any check fails, but return a `VerifiedToken`
    /// if it all succeeds.
    pub fn validate_token(&self, token: &UnverifiedToken) -> Result<VerifiedToken, Error> {
        let validator = match token.claims.expiration {
            Some(_) => jwt::Validation::default(),
            None => jwt::Validation {
                validate_exp: false,
                ..jwt::Validation::default()
            },
        };
        let res = jwt::decode::<ClaimSetJS>(&token.text, &(self.0).0, &validator);
        match res {
            Ok(res_) => {
                let claims = res_.claims;
                let in_db = self.1.get(&claims.jti);
                if in_db.is_some() {
                    Ok(VerifiedToken {
                        text: token.text.clone(),
                        claims: claims.to_claimset(),
                    })
                } else {
                    Err(Error::UnknownToken)
                }
            }
            Err(err) => Err(Error::JWTError(err)),
        }
    }

    /// Given a text string, as from a web application's `Authorization` header, decode the string
    /// and then validate the token.
    pub fn decode_and_validate_text(&self, text: String) -> Result<VerifiedToken, Error> {
        // it is necessary to first decode the token because we need the validator to know whether
        // to attempt to validate the expiration. Without that check, the validator will fail any
        // expiration set to None.
        match UnverifiedToken::decode_text(text) {
            Ok(unverified) => self.validate_token(&unverified),
            Err(err) => Err(err),
        }
    }

    /// Add a claimset to the database.
    pub fn add_claimset(&mut self, claimset: ClaimSet) {
        self.1.insert(claimset.id.clone(), claimset);
    }

    /// Remove a claims set from the database so that all additional validation checks fail.
    pub fn revoke_claimset(&mut self, claim: &ClaimSet) {
        self.1.remove(&claim.id);
    }

    /// Revoke a ClaimsSet given its ID, which is set in the `jti` claim of a JWT or the `id` field
    /// of a `ClaimSet`.
    pub fn revoke_by_uuid(&mut self, claim_id: &String) {
        self.1.remove(claim_id);
    }

    /// *NOT IMPLEMENTED*
    pub fn replace_claimsets(&mut self, _claims_lst: Vec<ClaimSet>) {
        unimplemented!()
    }

    /// List all of the `ClaimSet` IDs in the database.
    pub fn list_claimsets(&self) -> Vec<&ClaimSet> {
        self.1.values().map(|item| item).collect()
    }

    /// Find a `ClaimSet` by ID.
    pub fn find_claimset(&self, claims_id: &String) -> Option<&ClaimSet> {
        self.1.get(claims_id)
    }

    /// Encode and sign a claimset, returning the result as a `VerifiedToken`.
    pub fn encode_claimset(&self, claims: &ClaimSet) -> Result<VerifiedToken, Error> {
        let in_db = self.1.get(&claims.id);
        if in_db.is_some() {
            let text = jwt::encode(
                &jwt::Header::default(),
                &ClaimSetJS::from_claimset(&claims),
                &(self.0).0,
            );
            match text {
                Ok(text_) => Ok(VerifiedToken {
                    text: text_,
                    claims: claims.clone(),
                }),
                Err(err) => Err(Error::JWTError(err)),
            }
        } else {
            Err(Error::UnknownToken)
        }
    }
}
