//! # API tokens management

use crate::app_config::AppConfig;
use crate::constants;
use crate::utils::time_utils::time;
use actix_http::Method;
use basic_jwt::{JWTPrivateKey, JWTPublicKey};
use std::path::Path;
use std::str::FromStr;

#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
pub struct TokenID(pub uuid::Uuid);

impl TokenID {
    /// Parse a string as a token id
    pub fn parse(t: &str) -> anyhow::Result<Self> {
        Ok(Self(uuid::Uuid::parse_str(t)?))
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct TokenRight {
    verb: TokenVerb,
    path: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct TokenRights(Vec<TokenRight>);

impl TokenRights {
    pub fn check_error(&self) -> Option<&'static str> {
        for r in &self.0 {
            if !r.path.starts_with("/api/") {
                return Some("All API rights shall start with /api/");
            }

            if r.path.len() > constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH {
                return Some("An API path shall not exceed maximum URL size!");
            }
        }
        None
    }

    pub fn contains(&self, verb: TokenVerb, path: &str) -> bool {
        let req_path_split = path.split('/').collect::<Vec<_>>();

        'root: for r in &self.0 {
            if r.verb != verb {
                continue 'root;
            }

            let mut last_idx = 0;
            for (idx, part) in r.path.split('/').enumerate() {
                if idx >= req_path_split.len() {
                    continue 'root;
                }

                if part != "*" && part != req_path_split[idx] {
                    continue 'root;
                }

                last_idx = idx;
            }

            // Check we visited the whole path
            if last_idx + 1 == req_path_split.len() {
                return true;
            }
        }

        false
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct Token {
    pub id: TokenID,
    pub name: String,
    pub description: String,
    created: u64,
    updated: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pub_key: Option<JWTPublicKey>,
    pub rights: TokenRights,
    pub last_used: u64,
    pub ip_restriction: Option<ipnetwork::IpNetwork>,
    pub max_inactivity: Option<u64>,
}

impl Token {
    /// Turn the token into a JSON string
    fn save(&self) -> anyhow::Result<()> {
        let json = serde_json::to_string(self)?;

        std::fs::write(AppConfig::get().api_token_definition_path(self.id), json)?;

        Ok(())
    }

    /// Load token information from a file
    fn load_from_file(path: &Path) -> anyhow::Result<Self> {
        Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
    }

    /// Check whether a token is expired or not
    pub fn is_expired(&self) -> bool {
        if let Some(max_inactivity) = self.max_inactivity {
            if max_inactivity + self.last_used < time() {
                return true;
            }
        }

        false
    }

    /// Check whether last_used shall be updated or not
    pub fn should_update_last_activity(&self) -> bool {
        self.last_used + 3600 < time()
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum TokenVerb {
    GET,
    POST,
    PUT,
    PATCH,
    DELETE,
}

impl TokenVerb {
    pub fn as_method(&self) -> Method {
        match self {
            TokenVerb::GET => Method::GET,
            TokenVerb::POST => Method::POST,
            TokenVerb::PUT => Method::PUT,
            TokenVerb::PATCH => Method::PATCH,
            TokenVerb::DELETE => Method::DELETE,
        }
    }
}

impl FromStr for TokenVerb {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "GET" => Ok(TokenVerb::GET),
            "POST" => Ok(TokenVerb::POST),
            "PUT" => Ok(TokenVerb::PUT),
            "PATCH" => Ok(TokenVerb::PATCH),
            "DELETE" => Ok(TokenVerb::DELETE),
            _ => Err(()),
        }
    }
}

/// Structure used to create a token
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct NewToken {
    pub name: String,
    pub description: String,
    pub rights: TokenRights,
    pub ip_restriction: Option<ipnetwork::IpNetwork>,
    pub max_inactivity: Option<u64>,
}

impl NewToken {
    /// Check for error in token
    pub fn check_error(&self) -> Option<&'static str> {
        if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH {
            return Some("Name is too short!");
        }

        if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH {
            return Some("Name is too long!");
        }

        if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH {
            return Some("Description is too short!");
        }

        if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH {
            return Some("Description is too long!");
        }

        if let Some(err) = self.rights.check_error() {
            return Some(err);
        }

        if let Some(t) = self.max_inactivity {
            if t < 3600 {
                return Some("API tokens shall be valid for at least 1 hour!");
            }
        }

        None
    }
}

/// Create a new Token
pub async fn create(t: &NewToken) -> anyhow::Result<(Token, JWTPrivateKey)> {
    let priv_key = JWTPrivateKey::generate_ec384_signing_key()?;
    let pub_key = priv_key.to_public_key()?;

    let token = Token {
        name: t.name.to_string(),
        description: t.description.to_string(),
        id: TokenID(uuid::Uuid::new_v4()),
        created: time(),
        updated: time(),
        pub_key: Some(pub_key),
        rights: t.rights.clone(),
        last_used: time(),
        ip_restriction: t.ip_restriction,
        max_inactivity: t.max_inactivity,
    };

    token.save()?;

    Ok((token, priv_key))
}

/// Get the entire list of api tokens
pub async fn full_list() -> anyhow::Result<Vec<Token>> {
    let mut list = Vec::new();
    for f in std::fs::read_dir(AppConfig::get().api_tokens_path())? {
        list.push(Token::load_from_file(&f?.path())?);
    }
    Ok(list)
}

/// Get the information about a single token
pub async fn get_single(id: TokenID) -> anyhow::Result<Token> {
    Token::load_from_file(&AppConfig::get().api_token_definition_path(id))
}

/// Update API tokens rights
pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<()> {
    let mut token = get_single(id).await?;
    token.rights = rights;
    token.updated = time();
    token.save()?;
    Ok(())
}

/// Set last_used value of token
pub async fn refresh_last_used(id: TokenID) -> anyhow::Result<()> {
    let mut token = get_single(id).await?;
    token.last_used = time();
    token.save()?;
    Ok(())
}

/// Delete an API token
pub async fn delete(id: TokenID) -> anyhow::Result<()> {
    let path = AppConfig::get().api_token_definition_path(id);
    if path.exists() {
        std::fs::remove_file(path)?;
    }
    Ok(())
}

#[cfg(test)]
mod test {
    use crate::api_tokens::{TokenRight, TokenRights, TokenVerb};

    #[test]
    fn test_rights_patch() {
        let rights = TokenRights(vec![
            TokenRight {
                path: "/api/vm/*".to_string(),
                verb: TokenVerb::GET,
            },
            TokenRight {
                path: "/api/vm/a".to_string(),
                verb: TokenVerb::PUT,
            },
            TokenRight {
                path: "/api/vm/a/other".to_string(),
                verb: TokenVerb::DELETE,
            },
            TokenRight {
                path: "/api/net/create".to_string(),
                verb: TokenVerb::POST,
            },
        ]);

        assert!(rights.contains(TokenVerb::GET, "/api/vm/ab"));
        assert!(!rights.contains(TokenVerb::GET, "/api/vm"));
        assert!(!rights.contains(TokenVerb::GET, "/api/vm/ab/c"));
        assert!(rights.contains(TokenVerb::PUT, "/api/vm/a"));
        assert!(!rights.contains(TokenVerb::PUT, "/api/vm/other"));
        assert!(rights.contains(TokenVerb::POST, "/api/net/create"));
        assert!(!rights.contains(TokenVerb::GET, "/api/net/create"));
        assert!(!rights.contains(TokenVerb::POST, "/api/net/b"));
        assert!(!rights.contains(TokenVerb::POST, "/api/net/create/b"));
    }
}