use crate::{database::PsqlData, errors::SignupError};
use futures_util::TryStreamExt;
use pbkdf2::{
    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
    Pbkdf2,
};
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::error::Error;

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub password: String,
    pub admin: bool,
}

impl User {
    pub async fn read_by_name(
        pool: &PgPool,
        username: &String,
    ) -> Result<Box<Self>, Box<dyn Error>> {
        let result = sqlx::query_as!(Self, "SELECT * FROM users WHERE username = $1", username,)
            .fetch_one(pool)
            .await?;

        Ok(Box::new(result))
    }
}

impl PsqlData for User {
    async fn read_all(pool: &PgPool) -> Result<Vec<Box<Self>>, Box<dyn Error>> {
        crate::psql_read_all!(Self, pool, "users")
    }

    async fn read(pool: &PgPool, id: i32) -> Result<Box<Self>, Box<dyn Error>> {
        crate::psql_read!(Self, pool, id, "users")
    }

    async fn insert(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        sqlx::query!(
            "INSERT INTO users (username, password, admin) VALUES ($1, $2, $3)",
            self.username,
            self.password,
            self.admin,
        )
        .execute(pool)
        .await?;

        Ok(())
    }

    async fn update(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        sqlx::query!(
            "UPDATE users SET username=$1, password=$2 WHERE id=$3",
            self.username,
            self.password,
            self.id,
        )
        .execute(pool)
        .await?;

        Ok(())
    }

    async fn delete(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        let id = &self.id;
        crate::psql_delete!(id, pool, "users")
    }
}

pub async fn create_user(
    username: &str,
    password: &str,
    pool: &PgPool,
) -> Result<i32, SignupError> {
    let salt = SaltString::generate(&mut OsRng);

    // Hash password to PHC string ($pbkdf2-sha256$...)
    let hashed_password = Pbkdf2
        .hash_password(password.as_bytes(), &salt)
        .unwrap()
        .to_string();

    const INSERT_QUERY: &str =
        "INSERT INTO users (username, password, admin) VALUES ($1, $2, $3) RETURNING id;";

    let fetch_one = sqlx::query_as(INSERT_QUERY)
        .bind(username)
        .bind(hashed_password)
        .bind(false)
        .fetch_one(pool)
        .await;

    match fetch_one {
        Ok((user_id,)) => Ok(user_id),
        Err(sqlx::Error::Database(database))
            if database.constraint() == Some("users_username_key") =>
        {
            return Err(SignupError::UsernameExists);
        }
        Err(_) => {
            return Err(SignupError::InternalError);
        }
    }
}
