use chrono::{DateTime, Utc};
use sqlx::PgPool;

use super::{Area, Function, Result, Role};

#[derive(Clone, Debug)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
    pub password: Option<String>,
    pub salt: Option<String>,
    pub role: Role,
    pub function: Function,
    pub area_id: i32,
    pub area: Option<Area>,
    pub locked: bool,
    pub last_login: Option<DateTime<Utc>>,
    pub receive_notifications: bool,
}

impl User {
    pub async fn create(
        pool: &PgPool,
        name: &str,
        email: &str,
        role: Role,
        function: Function,
        area_id: i32,
    ) -> Result<i32> {
        sqlx::query!(
            r#"
                INSERT INTO user_ (name, email, role, function, areaId)
                VALUES ($1, $2, $3, $4, $5)
                RETURNING id;
            "#,
            name,
            email,
            role as Role,
            function as Function,
            area_id
        )
        .fetch_one(pool)
        .await
        .and_then(|r| Ok(r.id))
    }

    pub async fn create_with_password(
        pool: &PgPool,
        name: &str,
        email: &str,
        password: &str,
        salt: &str,
        role: Role,
        function: Function,
        area_id: i32,
    ) -> Result<i32> {
        let b = sqlx::query!(
            r#"
                INSERT INTO user_ (name, email, password, salt, role, function, areaId)
                VALUES ($1, $2, $3, $4, $5, $6, $7)
                RETURNING id;
            "#,
            name,
            email,
            password,
            salt,
            role as Role,
            function as Function,
            area_id
        )
        .fetch_one(pool)
        .await
        .and_then(|r| Ok(r.id));

        b
    }

    pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<Option<User>> {
        let record = sqlx::query!(
            r#"
                SELECT id,
                    name,
                    email,
                    password,
                    salt,
                    role AS "role: Role",
                    function AS "function: Function",
                    areaId,
                    locked,
                    lastLogin,
                    receiveNotifications
                FROM user_
                WHERE id = $1;
            "#,
            id,
        )
        .fetch_optional(pool)
        .await?;

        let user = record.and_then(|u| Some(User {
            id: u.id,
            name: u.name,
            email: u.email,
            password: u.password,
            salt: u.salt,
            role: u.role,
            function: u.function,
            area_id: u.areaid,
            area: None,
            locked: u.locked,
            last_login: u.lastlogin,
            receive_notifications: u.receivenotifications,
        }));

        Ok(user)
    }

    pub async fn read_for_login(pool: &PgPool, email: &str) -> anyhow::Result<User> {
        let record = sqlx::query!(
            r#"
                SELECT id,
                    name,
                    email,
                    password,
                    salt,
                    role AS "role: Role",
                    function AS "function: Function",
                    areaId,
                    locked,
                    lastLogin,
                    receiveNotifications
                FROM user_
                WHERE email = $1 AND locked = FALSE AND password IS NOT NULL AND salt IS NOT NULL;
            "#,
            email,
        )
        .fetch_one(pool)
        .await?;

        let result = User {
            id: record.id,
            name: record.name,
            email: record.email,
            password: record.password,
            salt: record.salt,
            role: record.role,
            function: record.function,
            area_id: record.areaid,
            area: None,
            locked: record.locked,
            last_login: record.lastlogin,
            receive_notifications: record.receivenotifications,
        };

        Ok(result)
    }

    pub async fn read_all(pool: &PgPool) -> anyhow::Result<Vec<User>> {
        let records = sqlx::query!(
            r#"
                SELECT id,
                    name,
                    email,
                    password,
                    salt,
                    role AS "role: Role",
                    function AS "function: Function",
                    areaId,
                    locked,
                    lastLogin,
                    receiveNotifications
                FROM user_;
            "#,
        )
        .fetch_all(pool)
        .await?;

        let result = records
            .iter()
            .map(|record| User {
                id: record.id,
                name: record.name.clone(),
                email: record.email.clone(),
                password: record.password.clone(),
                salt: record.salt.clone(),
                role: record.role.clone(),
                function: record.function.clone(),
                area_id: record.areaid,
                area: None,
                locked: record.locked,
                last_login: record.lastlogin,
                receive_notifications: record.receivenotifications,
            })
            .collect();

        Ok(result)
    }

    pub async fn read_all_including_area(pool: &PgPool) -> Result<Vec<User>> {
        let records = sqlx::query!(
            r#"
                SELECT
                    user_.id AS userId,
                    user_.name,
                    user_.email,
                    user_.password,
                    user_.salt,
                    user_.role AS "role: Role",
                    user_.function AS "function: Function",
                    user_.areaId,
                    user_.locked,
                    user_.lastLogin,
                    user_.receiveNotifications,
                    area.id,
                    area.name AS areaName
                FROM user_
                JOIN area ON user_.areaId = area.id
            "#
        )
        .fetch_all(pool)
        .await?;

        let results = records
            .iter()
            .map(|record| User {
                id: record.userid,
                name: record.name.clone(),
                email: record.email.clone(),
                password: record.password.clone(),
                salt: record.salt.clone(),
                role: record.role.clone(),
                function: record.function.clone(),
                area_id: record.areaid,
                area: Some(Area {
                    id: record.areaid,
                    name: record.areaname.clone(),
                }),
                locked: record.locked,
                last_login: record.lastlogin,
                receive_notifications: record.receivenotifications,
            })
            .collect();

        Ok(results)
    }

    pub async fn read_all_by_area(pool: &PgPool, area_id: i32) -> Result<Vec<User>> {
        let records = sqlx::query!(
            r#"
                SELECT id,
                    name,
                    email,
                    password,
                    salt,
                    role AS "role: Role",
                    function AS "function: Function",
                    areaId,
                    locked,
                    lastLogin,
                    receiveNotifications
                FROM user_
                WHERE areaId = $1;
            "#,
            area_id
        )
        .fetch_all(pool)
        .await?;

        let result = records
            .iter()
            .map(|record| User {
                id: record.id,
                name: record.name.clone(),
                email: record.email.clone(),
                password: record.password.clone(),
                salt: record.salt.clone(),
                role: record.role.clone(),
                function: record.function.clone(),
                area_id: record.areaid,
                area: None,
                locked: record.locked,
                last_login: record.lastlogin,
                receive_notifications: record.receivenotifications,
            })
            .collect();

        Ok(result)
    }

    pub async fn update(
        pool: &PgPool,
        id: i32,
        email: Option<&str>,
        name: Option<&str>,
        password: Option<&str>,
        salt: Option<&str>,
        role: Option<Role>,
        function: Option<Function>,
        area_id: Option<i32>,
        receive_notifications: Option<bool>,
        locked: Option<bool>,
    ) -> Result<()> {
        let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
        let mut separated = query_builder.separated(", ");

        if let Some(email) = email {
            separated.push("email = ");
            separated.push_bind_unseparated(email);
        }

        if let Some(name) = name {
            separated.push("name = ");
            separated.push_bind_unseparated(name);
        }

        if let Some(password) = password {
            separated.push("password = ");
            separated.push_bind_unseparated(password);
        }

        if let Some(salt) = salt {
            separated.push("salt = ");
            separated.push_bind_unseparated(salt);
        }

        if let Some(role) = role {
            separated.push("role = ");
            separated.push_bind_unseparated(role as Role);
        }

        if let Some(function) = function {
            separated.push("function = ");
            separated.push_bind_unseparated(function as Function);
        }

        if let Some(area_id) = area_id {
            separated.push("areaId = ");
            separated.push_bind_unseparated(area_id);
        }

        if let Some(receive_notifications) = receive_notifications {
            separated.push("receiveNotifications = ");
            separated.push_bind_unseparated(receive_notifications);
        }

        if let Some(locked) = locked {
            separated.push("locked = ");
            separated.push_bind_unseparated(locked);
        }

        query_builder.push(" WHERE id = ");
        query_builder.push_bind(id);
        query_builder.push(";");

        query_builder.build().execute(pool).await?;

        Ok(())
    }

    pub async fn update_login_timestamp(pool: &PgPool, id: i32) -> anyhow::Result<()> {
        sqlx::query!("UPDATE user_ SET lastLogin = NOW() WHERE id = $1;", id)
            .execute(pool)
            .await?;

        Ok(())
    }

    pub async fn delete(pool: &PgPool, id: i32) -> Result<()> {
        sqlx::query!("DELETE FROM user_ WHERE id = $1;", id)
            .execute(pool)
            .await?;

        Ok(())
    }
}
