//! [`CsrfToken`], [`Key`] and functions to encode & decode expiring claims.

use rand::{Rng, distributions::Alphanumeric};
use ::cookie::Cookie;
use time::{Duration, OffsetDateTime};

pub use crate::signed::Key;

use crate::{Error, request::Parts, response::Response};

/// A cookie-based CSRF token to be used with [`crate::request::Body::into_form_csrf`].
pub struct CsrfToken {
    token: String,
    from_client: bool,
}

impl CsrfToken {
    /// Retrieves the CSRF token from a `csrf` cookie or generates
    /// a new token and stores it as a cookie if it doesn't exist.
    pub fn from_parts(request: &mut Parts, response: &mut Response) -> Self {
        if let Some(cookie) = request.cookies().get("csrf") {
            return CsrfToken{token: cookie.value().to_string(), from_client: true}
        }
        let val: String = rand::thread_rng().sample_iter(Alphanumeric).take(16).collect();
        let mut c = Cookie::new("csrf", val.clone());
        c.set_secure(Some(true));
        c.set_max_age(Some(Duration::hours(1)));
        response.set_cookie(c);
        CsrfToken{token: val, from_client: false}
    }

    /// Wraps the token in a hidden HTML input.
    pub fn html_input(&self) -> String {
        format!("<input name=csrf type=hidden value=\"{}\">", self.token)
    }

    pub(crate) fn matches(&self, str: String) -> Result<(), Error> {
        if !self.from_client {
            return Err(Error::bad_request("expected csrf cookie".to_string()))
        }
        if self.token != str {
            return Err(Error::bad_request("csrf parameter doesn't match csrf cookie".to_string()))
        }
        Ok(())
    }
}

/// Join a string and an expiry date together into a string.
pub fn encode_expiring_claim(claim: &str, expiry_date: OffsetDateTime) -> String {
    format!("{}:{}", claim, expiry_date.unix_timestamp())
}

/// Extract the string, failing if the expiry date is in the past.
pub fn decode_expiring_claim(value: String) -> Result<String,&'static str> {
    let mut parts = value.splitn(2, ':');
    let claim = parts.next().ok_or("expected colon")?;
    let expiry_date = parts.next().ok_or("expected colon")?;
    let expiry_date: i64 = expiry_date.parse().map_err(|_| "failed to parse timestamp")?;

    if expiry_date > OffsetDateTime::now_utc().unix_timestamp() {
        Ok(claim.to_string())
    } else {
        Err("token is expired")
    }
}