use std::time::{SystemTime, UNIX_EPOCH};

use rand::Rng;
use rocket::{
    Request,
    http::Status,
    request::{self, FromRequest, Outcome},
};
use rocket_db_pools::Connection;
use sha2::{Digest, Sha256};
use sqlx::postgres::PgQueryResult;

use crate::db::DbConn;

#[derive(Debug, Clone)]
pub struct Session {
    pub token: String,
    pub user_id: usize,
}

impl Session {
    pub fn new(user_id: usize) -> Self {
        let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
        let random: u32 = rand::rng().random();
        let token = format!("{}-{}", current_time.as_secs(), random);
        let hashed = format!("{:x}", Sha256::digest(token.as_bytes()));
        Self {
            token: hashed,
            user_id,
        }
    }

    pub async fn commit(&self, db: &mut Connection<DbConn>) -> Result<PgQueryResult, sqlx::Error> {
        sqlx::query!(
            "INSERT INTO sessions (user_id, token) VALUES ($1, $2)",
            self.user_id as i32,
            self.token,
        )
        .execute(&mut ***db)
        .await
    }
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for Session {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
        if let Some(c) = request.cookies().get_private("session") {
            let mut pool = match request.guard::<Connection<DbConn>>().await {
                Outcome::Success(pool) => pool,
                _ => return Outcome::Error((Status::Unauthorized, ())),
            };

            let value = c.value();
            let result = sqlx::query!(
                "SELECT user_id, token FROM sessions WHERE token = $1 AND expires_at > NOW()",
                value
            )
            .fetch_optional(&mut **pool)
            .await
            .expect("query failed!");

            if let Some(session) = result {
                Outcome::Success(Self {
                    user_id: session.user_id as usize,
                    token: session.token,
                })
            } else {
                Outcome::Error((Status::Unauthorized, ()))
            }
        } else {
            Outcome::Error((Status::Unauthorized, ()))
        }
    }
}
