use std::error::Error;

use base64::{
    engine::{self, general_purpose},
    Engine,
};

use chrono::{DateTime, Utc};
use hyper::http::HeaderValue;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
#[allow(dead_code)]
struct JwtHeader {
    alg: String,
    typ: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[allow(dead_code)]
enum ExpValue {
    Integer(i64),
    String(String),
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[allow(dead_code)]
struct JwtPayload {
    #[serde(deserialize_with = "deserialize_exp")]
    exp: i64,
    iss: Option<String>,
    user: Option<String>,
}

fn deserialize_exp<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::de::Error;

    #[derive(Deserialize)]
    #[serde(untagged)]
    enum ExpValue {
        String(String),
        Number(i64),
    }

    match ExpValue::deserialize(deserializer)? {
        ExpValue::String(s) => s.parse().map_err(D::Error::custom),
        ExpValue::Number(n) => Ok(n),
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[allow(dead_code)]
pub struct JwtToken {
    header: JwtHeader,
    payload: JwtPayload,
    signature: Vec<u8>,
    expiration_date: DateTime<Utc>,
    token: String,
}

impl JwtToken {
    fn decode_token_using_engine(
        token: &str,
        engine: engine::GeneralPurpose,
    ) -> Result<Self, Box<dyn Error + Send + Sync>> {
        let mut parts = token.split('.');
        let header = parts.next().unwrap();
        let payload = parts.next().unwrap();
        let signature = parts.next().unwrap();

        let header = engine.decode(header)?;
        let payload = engine.decode(payload)?;
        let signature = engine.decode(signature)?;

        // Parse jwt header
        let header: JwtHeader = serde_json::from_slice(&header)?;

        // Parse jwt payload
        let payload: JwtPayload = serde_json::from_slice(&payload)?;

        // Parse jwt expiration date
        let timestamp = DateTime::from_timestamp(payload.exp, 0)
            .unwrap()
            .naive_utc();
        let expiration_date = DateTime::from_naive_utc_and_offset(timestamp, Utc);

        Ok(JwtToken {
            header,
            payload,
            signature,
            expiration_date,
            token: token.to_string(),
        })
    }

    pub fn new(token: &str) -> Result<Self, Box<dyn Error + Send + Sync>> {
        // STUPID: My mock server uses a different encoding than the real server, so we have to
        // try both encodings here.

        log::debug!("Attempting to decode JWT token: {}", token);

        let result = Self::decode_token_using_engine(token, general_purpose::STANDARD).or(
            Self::decode_token_using_engine(token, general_purpose::URL_SAFE_NO_PAD),
        );

        if let Err(ref e) = result {
            log::error!("Failed to decode JWT token: {}", e);
            log::error!("Token length: {}", token.len());
            log::error!("Token parts: {:?}", token.split('.').collect::<Vec<_>>());
        }

        result
    }

    pub fn dummy() -> Self {
        JwtToken {
            header: JwtHeader {
                alg: "none".to_string(),
                typ: "JWT".to_string(),
            },
            payload: JwtPayload {
                exp: 0,
                iss: None,
                user: None,
            },
            signature: vec![],
            expiration_date: Utc::now(),
            token: "".to_string(),
        }
    }

    pub fn is_valid(&self) -> bool {
        self.expiration_date > Utc::now()
    }

    pub fn to_header_value(&self) -> HeaderValue {
        format!("Bearer {}", self.token).parse().unwrap()
    }

    pub fn to_string(&self) -> String {
        self.token.clone()
    }
}
