use diesel::prelude::*;
use log::error;
use uuid::Uuid;

use crate::{
    db::{
        PgPooledConnection,
        models::{
            DbIcon, DbIngredient, DbRecipe, DbResource, NewDbIcon, NewDbIngredient, NewDbRecipe,
            NewDbResource,
        },
        schema::ingredient::resource,
    },
    types::{ID, IdOrNone},
};

fn get_connection() -> PgPooledConnection {
    crate::db::DB_POOL.get().unwrap().get_connection().unwrap()
}

pub fn get_icon_by_id(icon_id: Uuid) -> Result<Option<DbIcon>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::icon::*;

    let conn: &mut PgConnection = &mut get_connection();

    match dsl::icon.find(icon_id).first::<DbIcon>(conn) {
        Ok(icon_by_id) => Ok(Some(icon_by_id)),
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching icon by ID: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn get_icon_by_name(icon_name: &str) -> Result<Option<DbIcon>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::icon::*;

    let conn: &mut PgConnection = &mut get_connection();

    match dsl::icon
        .filter(dsl::name.eq(icon_name))
        .limit(1)
        .load::<DbIcon>(conn)
    {
        Ok(it) => {
            if it.len() > 0 {
                Ok(Some(it[0].clone()))
            } else {
                Ok(None)
            }
        }
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching icon by name: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn add_icon(new_icon: &NewDbIcon) -> Result<IdOrNone, Box<dyn std::error::Error>> {
    use crate::db::schema::icon::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_icon) = get_icon_by_name(&new_icon.name)? {
        Ok(IdOrNone::EXISTING(existing_icon.id))
    } else {
        match diesel::insert_into(dsl::icon)
            .values(new_icon)
            .returning(dsl::id)
            .get_result::<Uuid>(conn)
        {
            Ok(new_icon_id) => Ok(IdOrNone::NEW(new_icon_id)),
            Err(e) => {
                error!("Error adding new icon: {}", e);
                Err(Box::new(e))
            }
        }
    }
}

pub fn update_icon(update_icon: &DbIcon) -> Result<(), Box<dyn std::error::Error>> {
    use crate::db::schema::icon::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_icon) = get_icon_by_name(&update_icon.name)? {
        match diesel::update(dsl::icon.find(existing_icon.id))
            .set(update_icon)
            .execute(conn)
        {
            Ok(_) => Ok(()),
            Err(e) => {
                error!("Error updating icon: {}", e);
                Err(Box::new(e))
            }
        }
    } else {
        Err(Box::new(diesel::result::Error::NotFound))
    }
}

pub fn get_resource_by_id(
    resource_id: Uuid,
) -> Result<Option<DbResource>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::resource::*;

    let conn: &mut PgConnection = &mut get_connection();

    match dsl::resource.find(resource_id).first::<DbResource>(conn) {
        Ok(it) => Ok(Some(it)),
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching resource by ID: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn get_resource_by_name(
    resource_name: &str,
) -> Result<Option<DbResource>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::resource::*;

    let conn: &mut PgConnection = &mut get_connection();

    match dsl::resource
        .filter(name.eq(resource_name))
        .limit(1)
        .load::<DbResource>(conn)
    {
        Ok(it) => {
            if it.len() > 0 {
                Ok(Some(it[0].clone()))
            } else {
                Ok(None)
            }
        }
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching resource by name: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn add_resource(new_resource: &NewDbResource) -> Result<IdOrNone, Box<dyn std::error::Error>> {
    use crate::db::schema::resource::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_resource) = get_resource_by_name(&new_resource.name)? {
        Ok(IdOrNone::EXISTING(existing_resource.id))
    } else {
        match diesel::insert_into(dsl::resource)
            .values(new_resource)
            .returning(dsl::id)
            .get_result::<Uuid>(conn)
        {
            Ok(new_resource_id) => Ok(IdOrNone::NEW(new_resource_id)),
            Err(e) => {
                error!("Error adding new resource: {}", e);
                Err(Box::new(e))
            }
        }
    }
}

pub fn update_resource(update_resource: &DbResource) -> Result<(), Box<dyn std::error::Error>> {
    use crate::db::schema::resource::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_resource) = get_resource_by_name(&update_resource.name)? {
        diesel::update(dsl::resource.find(existing_resource.id))
            .set(update_resource)
            .execute(conn)?;
        Ok(())
    } else {
        Err(Box::new(diesel::result::Error::NotFound))
    }
}

pub fn get_ingredient_by_id(
    ingredient_id: &Uuid,
) -> Result<Option<DbIngredient>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::ingredient::*;

    let conn: &mut PgConnection = &mut get_connection();

    match dsl::ingredient
        .find(ingredient_id)
        .first::<DbIngredient>(conn)
    {
        Ok(it) => Ok(Some(it)),
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching ingredient by ID: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn add_ingredient(
    new_ingredient: &NewDbIngredient,
) -> Result<IdOrNone, Box<dyn std::error::Error>> {
    use crate::db::schema::ingredient::*;

    let conn: &mut PgConnection = &mut get_connection();

    match diesel::insert_into(dsl::ingredient)
        .values(new_ingredient)
        .returning(dsl::id)
        .get_result::<Uuid>(conn)
    {
        Ok(it) => Ok(IdOrNone::NEW(it)),
        Err(e) => {
            error!("Error adding new ingredient: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn update_ingredient(
    update_ingredient: &DbIngredient,
) -> Result<(), Box<dyn std::error::Error>> {
    use crate::db::schema::ingredient::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_ingredient) = get_ingredient_by_id(&update_ingredient.id)? {
        match diesel::update(dsl::ingredient)
            .filter(dsl::id.eq(existing_ingredient.id))
            .set(update_ingredient)
            .execute(conn)
        {
            Ok(_) => Ok(()),
            Err(e) => {
                error!("Error updating ingredient: {}", e);
                Err(Box::new(e))
            }
        }
    } else {
        Err(Box::new(diesel::result::Error::NotFound))
    }
}

pub fn get_recipe_by_id(by_id: &Uuid) -> Result<Option<DbRecipe>, Box<dyn std::error::Error>> {
    // Implement the logic to find and return the icon based on the name
    use crate::db::schema::recipe::dsl::*;

    let conn: &mut PgConnection = &mut get_connection();

    match recipe.find(by_id).first::<DbRecipe>(conn) {
        Ok(it) => Ok(Some(it)),
        Err(diesel::result::Error::NotFound) => Ok(None),
        Err(e) => {
            error!("Error fetching recipe by ID: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn add_recipe(new_recipe: &NewDbRecipe) -> Result<Uuid, Box<dyn std::error::Error>> {
    use crate::db::schema::recipe::*;

    let conn: &mut PgConnection = &mut get_connection();

    match diesel::insert_into(dsl::recipe)
        .values(new_recipe)
        .returning(dsl::id)
        .get_result::<Uuid>(conn)
    {
        Ok(new_id) => Ok(new_id),
        Err(e) => {
            error!("Error adding new recipe: {}", e);
            Err(Box::new(e))
        }
    }
}

pub fn update_recipe(update_recipe: &DbRecipe) -> Result<(), Box<dyn std::error::Error>> {
    use crate::db::schema::recipe::*;

    let conn: &mut PgConnection = &mut get_connection();

    if let Some(existing_recipe) = get_recipe_by_id(&update_recipe.id)? {
        match diesel::update(dsl::recipe.find(existing_recipe.id))
            .set(update_recipe)
            .execute(conn)
        {
            Ok(_) => Ok(()),
            Err(e) => {
                error!("Error updating recipe: {}", e);
                Err(Box::new(e))
            }
        }
    } else {
        Err(Box::new(diesel::result::Error::NotFound))
    }
}

pub fn find_recipes(
    by_recipe_type: &str,
    resource_id: &Uuid,
) -> Result<Vec<DbRecipe>, Box<dyn std::error::Error>> {
    use crate::db::schema::recipe::*;

    let conn: &mut PgConnection = &mut get_connection();

    let results = dsl::recipe
        .filter(dsl::recipe_type.eq(by_recipe_type))
        .filter(dsl::resource.eq(resource_id))
        .load::<DbRecipe>(conn)?;

    Ok(results)
}

pub fn find_ingredients_for_recipe(
    recipe: &DbRecipe,
    other: &Vec<String>,
) -> Result<Option<usize>, Box<dyn std::error::Error>> {
    use crate::db::schema::*;

    let conn: &mut PgConnection = &mut get_connection();

    let result_filtered = ingredient::table
        .inner_join(resource::table)
        .filter(ingredient::recipe.eq(recipe.id))
        .filter(resource::name.eq_any(other))
        .count()
        .get_result::<i64>(conn)?;

    let results_all = ingredient::table
        .inner_join(resource::table)
        .count()
        .get_result::<i64>(conn)?;

    if result_filtered == results_all {
        Ok(Some(results_all as usize))
    } else {
        Ok(None)
    }
}
