use chrono::{DateTime, TimeDelta, Utc};
use deadpool_diesel::postgres::Connection;
use diesel::{
    dsl::{AsSelect, Eq, Gt, IsNotDistinctFrom, Select},
    pg::Pg,
    prelude::*,
};
use uuid::Uuid;

use crate::{app_error::AppError, schema::csrf_tokens::dsl::*};

const TOKEN_PREFIX: &'static str = "csrf-";

#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::csrf_tokens)]
#[diesel(check_for_backend(Pg))]
pub struct CsrfToken {
    pub id: Uuid,
    pub user_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
}

impl CsrfToken {
    fn all() -> Select<csrf_tokens, AsSelect<CsrfToken, Pg>> {
        csrf_tokens.select(Self::as_select())
    }

    pub fn is_not_expired() -> Gt<created_at, DateTime<Utc>> {
        let ttl = TimeDelta::hours(24);
        let min_created_at: DateTime<Utc> = Utc::now() - ttl;
        created_at.gt(min_created_at)
    }

    pub fn with_user_id(token_user_id: Option<Uuid>) -> IsNotDistinctFrom<user_id, Option<Uuid>> {
        user_id.is_not_distinct_from(token_user_id)
    }

    pub fn with_token_id(token_id: Uuid) -> Eq<id, Uuid> {
        id.eq(token_id)
    }
}

pub async fn generate_csrf_token(
    db_conn: &Connection,
    with_user_id: Option<Uuid>,
) -> Result<String, AppError> {
    let token_id = Uuid::new_v4();
    db_conn
        .interact(move |conn| {
            diesel::insert_into(csrf_tokens)
                .values((
                    id.eq(token_id),
                    user_id.eq(with_user_id),
                    created_at.eq(diesel::dsl::now),
                ))
                .execute(conn)
        })
        .await
        .unwrap()?;
    Ok(format!("{}{}", TOKEN_PREFIX, token_id.simple().to_string()))
}

pub async fn validate_csrf_token(
    db_conn: &Connection,
    token: &str,
    with_user_id: Option<Uuid>,
) -> Result<bool, AppError> {
    let token_id = match Uuid::try_parse(&token[TOKEN_PREFIX.len()..]) {
        Ok(token_id) => token_id,
        Err(_) => return Ok(false),
    };
    Ok(db_conn
        .interact(move |conn| {
            CsrfToken::all()
                .filter(CsrfToken::with_token_id(token_id))
                .filter(CsrfToken::with_user_id(with_user_id))
                .filter(CsrfToken::is_not_expired())
                .first(conn)
                .optional()
        })
        .await
        .unwrap()?
        .is_some())
}
