use std::fmt::Display;

use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};

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

#[derive(Clone, Debug)]
pub struct Availabillity {
    pub id: i32,
    pub user_id: i32,
    pub user: Option<User>,
    pub date: NaiveDate,
    pub start_time: Option<NaiveTime>,
    pub end_time: Option<NaiveTime>,
    pub comment: Option<String>,
}

#[derive(PartialEq, Eq, Debug)]
pub enum AvailabillityAssignmentState {
    // availabillity is not assigned at all or at least not timely conflicting
    Unassigned,
    // availabillity is assigned for another event that is timely conflicting
    Conflicting,
    // availabillity is assigned to this event as Posten
    AssignedPosten(i32),
    // availabillity is assigned to this event as Führungsassistent
    AssignedFührungsassistent(i32),
    // availabillity is assigned to this event as Wachhabender
    AssignedWachahabender(i32),
}

impl Display for AvailabillityAssignmentState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AvailabillityAssignmentState::Unassigned => write!(f, "nicht zugewiesen"),
            AvailabillityAssignmentState::Conflicting => write!(f, "bereits anders zugewiesen"),
            AvailabillityAssignmentState::AssignedPosten(_) => write!(f, "zugewiesen als Posten"),
            AvailabillityAssignmentState::AssignedFührungsassistent(_) => write!(f, "zugewiesen als Führungsassistent"),
            AvailabillityAssignmentState::AssignedWachahabender(_) => write!(f, "zugewiesen als Wachhabender"),
        }
    }
}

impl Availabillity {
    pub async fn create(
        pool: &PgPool,
        user_id: i32,
        date: NaiveDate,
        start_time: Option<NaiveTime>,
        end_time: Option<NaiveTime>,
        comment: Option<String>,
    ) -> Result<()> {
        query!(
            r#"
            INSERT INTO availabillity (userId, date, startTime, endTime, comment)
            VALUES ($1, $2, $3, $4, $5);
            "#,
            user_id,
            date,
            start_time,
            end_time,
            comment
        )
        .execute(pool)
        .await?;

        Ok(())
    }

    pub async fn read_by_date_and_area_including_user(
        pool: &PgPool,
        date: NaiveDate,
        area_id: i32,
    ) -> Result<Vec<Availabillity>> {
        let records = query!(
            r##"
                             SELECT
                                 availabillity.id,
                                 availabillity.userId,
                                 availabillity.date,
                                 availabillity.startTime,
                                 availabillity.endTime,
                                 availabillity.comment,
                                 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
                             FROM availabillity
                             JOIN user_ ON availabillity.userId = user_.id
                             WHERE availabillity.date = $1
                             AND user_.areaId = $2;
        "##,
            date,
            area_id
        )
        .fetch_all(pool)
        .await?;

        let availabillities = records
            .iter()
            .map(|r| Availabillity {
                id: r.id,
                user_id: r.userid,
                user: Some(User {
                    id: r.userid,
                    name: r.name.clone(),
                    email: r.email.clone(),
                    password: r.password.clone(),
                    salt: r.salt.clone(),
                    role: r.role.clone(),
                    function: r.function.clone(),
                    area_id: r.areaid,
                    area: None,
                    locked: r.locked,
                    last_login: r.lastlogin,
                    receive_notifications: r.receivenotifications,
                }),
                date: r.date,
                start_time: r.starttime,
                end_time: r.endtime,
                comment: r.comment.clone(),
            })
            .collect();

        Ok(availabillities)
    }

    pub async fn read_by_id_including_user(
        pool: &PgPool,
        id: i32,
    ) -> Result<Option<Availabillity>> {
        let record = query!(
            r##"
                             SELECT
                                 availabillity.id,
                                 availabillity.userId,
                                 availabillity.date,
                                 availabillity.startTime,
                                 availabillity.endTime,
                                 availabillity.comment,
                                 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
                             FROM availabillity
                             LEFT JOIN assignment ON availabillity.Id = assignment.availabillityId
                             JOIN user_ ON availabillity.userId = user_.id
                             WHERE availabillity.id = $1;
        "##,
            id
        )
        .fetch_optional(pool)
        .await?;

        let availabillity = record.and_then(|r| {
            Some(Availabillity {
                id: r.id,
                user_id: r.userid,
                user: Some(User {
                    id: r.userid,
                    name: r.name.clone(),
                    email: r.email.clone(),
                    password: r.password.clone(),
                    salt: r.salt.clone(),
                    role: r.role.clone(),
                    function: r.function.clone(),
                    area_id: r.areaid,
                    area: None,
                    locked: r.locked,
                    last_login: r.lastlogin,
                    receive_notifications: r.receivenotifications,
                }),
                date: r.date,
                start_time: r.starttime,
                end_time: r.endtime,
                comment: r.comment.clone(),
            })
        });

        Ok(availabillity)
    }

    pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<Option<Availabillity>> {
        let record = query!("SELECT * FROM availabillity WHERE id = $1", id)
            .fetch_optional(pool)
            .await?;

        let availabillity = record.and_then(|record| {
            Some(Availabillity {
                id: record.id,
                user_id: record.userid,
                user: None,
                date: record.date,
                start_time: record.starttime,
                end_time: record.endtime,
                comment: record.comment.clone(),
            })
        });

        Ok(availabillity)
    }

    pub async fn read_for_export(
        pool: &PgPool,
        date_range: (NaiveDate, NaiveDate),
        area_id: i32,
    ) -> Result<Vec<Availabillity>> {
        let records = query!(
            r##"
                             SELECT
                                 availabillity.id,
                                 availabillity.userId,
                                 availabillity.date,
                                 availabillity.startTime,
                                 availabillity.endTime,
                                 availabillity.comment,
                                 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.name AS areaName
                             FROM availabillity
                             JOIN user_ ON availabillity.userId = user_.id
                             JOIN area ON user_.areaId = area.id
                             WHERE user_.areaId = $1 AND
                             availabillity.date >= $2 AND
                             availabillity.date <= $3;
        "##,
            area_id,
            date_range.0,
            date_range.1
        )
        .fetch_all(pool)
        .await?;

        let availabillities = records
            .iter()
            .map(|r| Availabillity {
                id: r.id,
                user_id: r.userid,
                user: Some(User {
                    id: r.userid,
                    name: r.name.clone(),
                    email: r.email.clone(),
                    password: r.password.clone(),
                    salt: r.salt.clone(),
                    role: r.role.clone(),
                    function: r.function.clone(),
                    area_id: r.areaid,
                    area: Some(Area {
                        id: r.areaid,
                        name: r.areaname.clone(),
                    }),
                    locked: r.locked,
                    last_login: r.lastlogin,
                    receive_notifications: r.receivenotifications,
                }),
                date: r.date,
                start_time: r.starttime,
                end_time: r.endtime,
                comment: r.comment.clone(),
            })
            .collect();

        Ok(availabillities)
    }

    pub async fn update(
        pool: &PgPool,
        id: i32,
        start_time: Option<NaiveTime>,
        end_time: Option<NaiveTime>,
        comment: Option<&String>,
    ) -> Result<()> {
        query!(
            "UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4",
            start_time,
            end_time,
            comment,
            id
        )
        .execute(pool)
        .await?;

        Ok(())
    }

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