use std::ops::DerefMut;

use itertools::Itertools;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};

use crate::model::boathouse::Boathouse;

use super::location::Location;
use super::user::User;

#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Boat {
    pub id: i64,
    pub name: String,
    pub amount_seats: i64,
    pub location_id: i64,
    pub owner: Option<i64>,
    pub year_built: Option<i64>,
    pub boatbuilder: Option<String>,
    pub default_destination: Option<String>,
    #[serde(default = "bool::default")]
    convert_handoperated_possible: bool,
    #[serde(default = "bool::default")]
    default_shipmaster_only_steering: bool,
    #[serde(default = "bool::default")]
    skull: bool,
    #[serde(default = "bool::default")]
    external: bool,
    pub deleted: bool,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum BoatDamage {
    None,
    Light,
    Locked,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct BoatWithDetails {
    #[serde(flatten)]
    pub(crate) boat: Boat,
    damage: BoatDamage,
    on_water: bool,
    reserved_today: bool,
}

#[derive(FromForm)]
pub struct BoatToAdd<'r> {
    pub name: &'r str,
    pub amount_seats: i64,
    pub year_built: Option<i64>,
    pub boatbuilder: Option<&'r str>,
    pub default_shipmaster_only_steering: bool,
    pub convert_handoperated_possible: bool,
    pub default_destination: Option<&'r str>,
    pub skull: bool,
    pub external: bool,
    pub location_id: Option<i64>,
    pub owner: Option<i64>,
}

#[derive(FromForm)]
pub struct BoatToUpdate<'r> {
    pub name: &'r str,
    pub amount_seats: i64,
    pub year_built: Option<i64>,
    pub boatbuilder: Option<&'r str>,
    pub default_shipmaster_only_steering: bool,
    pub default_destination: Option<&'r str>,
    pub skull: bool,
    pub convert_handoperated_possible: bool,
    pub external: bool,
    pub location_id: i64,
    pub owner: Option<i64>,
}

impl Boat {
    pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
        sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
            .fetch_one(db)
            .await
            .ok()
    }
    pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
        sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
            .fetch_one(db.deref_mut())
            .await
            .ok()
    }

    pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
        sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
            .fetch_one(db)
            .await
            .ok()
    }

    pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
        if let Some(owner_id) = self.owner {
            return owner_id == user.id;
        }

        if user.has_role(db, "Rennrudern").await {
            let ottensheim = Location::find_by_name(db, "Ottensheim".into())
                .await
                .unwrap();
            if self.location_id == ottensheim.id {
                return true;
            }
        }

        if self.amount_seats == 1 {
            return true;
        }

        user.has_role(db, "cox").await
    }

    pub async fn shipmaster_allowed_tx(
        &self,
        db: &mut Transaction<'_, Sqlite>,
        user: &User,
    ) -> bool {
        if let Some(owner_id) = self.owner {
            return owner_id == user.id;
        }

        if self.amount_seats == 1 {
            return true;
        }

        user.has_role_tx(db, "cox").await
    }

    pub async fn is_locked(&self, db: &SqlitePool) -> bool {
        sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
    }

    pub async fn has_minor_damage(&self, db: &SqlitePool) -> bool {
        sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
    }

    pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
        sqlx::query!(
            "SELECT * 
FROM boat_reservation
WHERE boat_id =? 
AND date('now') BETWEEN start_date AND end_date;",
            self.id
        )
        .fetch_optional(db)
        .await
        .unwrap()
        .is_some()
    }

    pub async fn on_water(&self, db: &SqlitePool) -> bool {
        sqlx::query!(
            "SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
            self.id
        )
        .fetch_optional(db)
        .await
        .unwrap()
        .is_some()
    }

    async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
        let mut res = Vec::new();
        for boat in boats {
            let mut damage = BoatDamage::None;
            if boat.has_minor_damage(db).await {
                damage = BoatDamage::Light;
            }
            if boat.is_locked(db).await {
                damage = BoatDamage::Locked;
            }
            res.push(BoatWithDetails {
                damage,
                on_water: boat.on_water(db).await,
                reserved_today: boat.reserved_today(db).await,
                boat,
            });
        }
        res
    }

    pub async fn all(db: &SqlitePool) -> Vec<BoatWithDetails> {
        let boats = sqlx::query_as!(
            Boat,
            "
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible 
FROM boat 
WHERE deleted=false
ORDER BY amount_seats DESC
        "
        )
        .fetch_all(db)
        .await
        .unwrap(); //TODO: fixme

        Self::boats_to_details(db, boats).await
    }

    pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> {
        let boats = sqlx::query_as!(
            Boat,
            "
SELECT 
    b.id, 
    b.name, 
    b.amount_seats, 
    b.location_id, 
    b.owner, 
    b.year_built, 
    b.boatbuilder, 
    b.default_shipmaster_only_steering, 
    b.default_destination, 
    b.skull, 
    b.external,
    b.deleted,
    b.convert_handoperated_possible
FROM 
    boat AS b
WHERE 
    b.external = false 
    AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
    AND b.deleted = false
ORDER BY 
    b.name DESC;
        "
        )
        .fetch_all(db)
        .await
        .unwrap(); //TODO: fixme

        Self::boats_to_details(db, boats).await
    }

    pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
        if user.has_role(db, "admin").await {
            return Self::all(db).await;
        }
        let mut boats = if user.has_role(db, "cox").await {
            sqlx::query_as!(
            Boat,
            "
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat 
WHERE owner is null or owner = ?
ORDER BY amount_seats DESC
        ",
        user.id
        )
        .fetch_all(db)
        .await
        .unwrap() //TODO: fixme
        } else {
            sqlx::query_as!(
            Boat,
            "
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible 
FROM boat 
WHERE owner = ? OR (owner is null and amount_seats = 1)
ORDER BY amount_seats DESC
        ",
        user.id
        )
        .fetch_all(db)
        .await
        .unwrap() //TODO: fixme
        };

        if user.has_role(db, "Rennrudern").await {
            let ottensheim = Location::find_by_name(db, "Ottensheim".into())
                .await
                .unwrap();
            let boats_in_ottensheim = sqlx::query_as!(
            Boat,
            "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible 
FROM boat 
WHERE owner is null and location_id = ? 
ORDER BY amount_seats DESC
        ",ottensheim.id)
        .fetch_all(db)
        .await
        .unwrap(); //TODO: fixme
            boats.extend(boats_in_ottensheim.into_iter());
        }
        let boats = boats.into_iter().unique().collect();

        Self::boats_to_details(db, boats).await
    }

    pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
        let boats = sqlx::query_as!(
            Boat,
            "
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat 
INNER JOIN location ON boat.location_id = location.id
WHERE location.name=?
ORDER BY amount_seats DESC
        ",
        location
        )
        .fetch_all(db)
        .await
        .unwrap(); //TODO: fixme

        Self::boats_to_details(db, boats).await
    }

    pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
        sqlx::query!(
            "INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
            boat.name,
            boat.amount_seats,
            boat.year_built,
            boat.boatbuilder,
            boat.default_shipmaster_only_steering,
            boat.default_destination,
            boat.skull,
            boat.external,
            boat.location_id,
            boat.owner,
            boat.convert_handoperated_possible
        )
        .execute(db)
        .await.map_err(|e| e.to_string())?;
        Ok(())
    }

    pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
        sqlx::query!(
            "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
        boat.name,
        boat.amount_seats,
        boat.year_built,
        boat.boatbuilder,
        boat.default_shipmaster_only_steering,
        boat.default_destination,
        boat.skull,
        boat.external,
        boat.location_id,
        boat.owner,
        boat.convert_handoperated_possible,
            self.id
        )
        .execute(db)
        .await.map_err(|e| e.to_string())?;
        Ok(())
    }

    pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
        if let Some(owner_id) = self.owner {
            Some(User::find_by_id(db, owner_id as i32).await.unwrap())
        } else {
            None
        }
    }

    pub async fn delete(&self, db: &SqlitePool) {
        sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
            .execute(db)
            .await
            .unwrap(); //Okay, because we can only create a Boat of a valid id
    }

    pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
        sqlx::query_as!(
            Boathouse,
            "SELECT * FROM boathouse WHERE boat_id like ?",
            self.id
        )
        .fetch_one(db)
        .await
        .ok()
    }
}

#[cfg(test)]
mod test {
    use crate::{
        model::boat::{Boat, BoatToAdd},
        testdb,
    };

    use sqlx::SqlitePool;

    use super::BoatToUpdate;

    #[sqlx::test]
    fn test_find_correct_id() {
        let pool = testdb!();
        let boat = Boat::find_by_id(&pool, 1).await.unwrap();
        assert_eq!(boat.id, 1);
    }

    #[sqlx::test]
    fn test_find_wrong_id() {
        let pool = testdb!();
        let boat = Boat::find_by_id(&pool, 1337).await;
        assert!(boat.is_none());
    }

    #[sqlx::test]
    fn test_all() {
        let pool = testdb!();
        let res = Boat::all(&pool).await;
        assert!(res.len() > 3);
    }

    #[sqlx::test]
    fn test_succ_create() {
        let pool = testdb!();

        assert_eq!(
            Boat::create(
                &pool,
                BoatToAdd {
                    name: "new-boat-name".into(),
                    amount_seats: 42,
                    year_built: None,
                    boatbuilder: "Best Boatbuilder".into(),
                    default_shipmaster_only_steering: true,
                    convert_handoperated_possible: false,
                    skull: true,
                    external: false,
                    location_id: Some(1),
                    owner: None,
                    default_destination: None
                }
            )
            .await,
            Ok(())
        );
    }

    #[sqlx::test]
    fn test_duplicate_name_create() {
        let pool = testdb!();

        assert_eq!(
            Boat::create(
                &pool,
                BoatToAdd {
                    name: "Haichenbach".into(),
                    amount_seats: 42,
                    year_built: None,
                    boatbuilder: "Best Boatbuilder".into(),
                    default_shipmaster_only_steering: true,
                    convert_handoperated_possible: false,
                    skull: true,
                    external: false,
                    location_id: Some(1),
                    owner: None,
                    default_destination: None
                }
            )
            .await,
            Err(
                "error returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
                    .into()
            )
        );
    }

    #[sqlx::test]
    fn test_is_locked() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 5)
            .await
            .unwrap()
            .is_locked(&pool)
            .await;
        assert_eq!(res, true);
    }

    #[sqlx::test]
    fn test_is_not_locked() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 4)
            .await
            .unwrap()
            .is_locked(&pool)
            .await;
        assert_eq!(res, false);
    }

    #[sqlx::test]
    fn test_is_not_locked_no_damage() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 3)
            .await
            .unwrap()
            .is_locked(&pool)
            .await;
        assert_eq!(res, false);
    }

    #[sqlx::test]
    fn test_has_minor_damage() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 4)
            .await
            .unwrap()
            .has_minor_damage(&pool)
            .await;
        assert_eq!(res, true);
    }

    #[sqlx::test]
    fn test_has_no_minor_damage() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 5)
            .await
            .unwrap()
            .has_minor_damage(&pool)
            .await;
        assert_eq!(res, false);
    }

    #[sqlx::test]
    fn test_on_water() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 2)
            .await
            .unwrap()
            .on_water(&pool)
            .await;
        assert_eq!(res, true);
    }

    #[sqlx::test]
    fn test_not_on_water() {
        let pool = testdb!();
        let res = Boat::find_by_id(&pool, 4)
            .await
            .unwrap()
            .on_water(&pool)
            .await;
        assert_eq!(res, false);
    }

    #[sqlx::test]
    fn test_succ_update() {
        let pool = testdb!();
        let boat = Boat::find_by_id(&pool, 1).await.unwrap();
        let update = BoatToUpdate {
            name: "my-new-boat-name",
            amount_seats: 3,
            year_built: None,
            boatbuilder: None,
            default_shipmaster_only_steering: false,
            convert_handoperated_possible: false,
            skull: true,
            external: false,
            location_id: 1,
            owner: None,
            default_destination: None,
        };

        boat.update(&pool, update).await.unwrap();

        let boat = Boat::find_by_id(&pool, 1).await.unwrap();
        assert_eq!(boat.name, "my-new-boat-name");
    }

    #[sqlx::test]
    fn test_failed_update() {
        let pool = testdb!();
        let boat = Boat::find_by_id(&pool, 1).await.unwrap();
        let update = BoatToUpdate {
            name: "my-new-boat-name",
            amount_seats: 3,
            year_built: None,
            boatbuilder: None,
            default_shipmaster_only_steering: false,
            convert_handoperated_possible: false,
            skull: true,
            external: false,
            location_id: 999,
            owner: None,
            default_destination: None,
        };

        match boat.update(&pool, update).await {
            Ok(_) => panic!("Update with invalid location should not succeed"),
            Err(e) => assert_eq!(
                e,
                "error returned from database: (code: 787) FOREIGN KEY constraint failed"
            ),
        };

        let boat = Boat::find_by_id(&pool, 1).await.unwrap();
        assert_eq!(boat.name, "Haichenbach");
    }
}