use std::{ffi::OsStr, io::Cursor, path::PathBuf};

use log::info;
use uuid::Uuid;

use crate::{
    configuration::CONFIGURATION,
    db::{
        self, add_icon, add_ingredient, add_recipe, add_resource, find_ingredients_for_recipe,
        find_recipes, get_icon_by_name, get_resource_by_name,
        models::{NewDbIcon, NewDbRecipe, NewDbResource},
    },
    types::{ID, Icon, IdOrNone, Ingredient, Recipe, Resource},
};

pub async fn download_content(url: &str) -> Result<String, anyhow::Error> {
    // Some simple CLI args requirements...

    let res = reqwest::get(url).await;

    if let Err(e) = res {
        info!("Failed to download file from {}: {}", url, e);
        anyhow::bail!("Failed to download file from {}: {}", url, e);
    }

    let res = res.unwrap();

    let body = res.text().await;

    match body {
        Ok(body) => {
            info!("Downloaded file from {} successfully", url);
            Ok(body)
        }
        Err(e) => {
            info!("Failed to download file from {}: {}", url, e);
            anyhow::bail!("Failed to download file from {}: {}", url, e);
        }
    }
}

fn get_icon_file(icon: &Icon) -> PathBuf {
    let mut file = PathBuf::from(
        CONFIGURATION
            .get()
            .unwrap()
            .main
            .image_resource_store_directory
            .clone(),
    );

    file.push(icon.name.clone());

    let mut counter = 1;
    let file_name = file.as_path().file_stem().unwrap_or(OsStr::new("icon"));
    let extension = file.as_path().extension().unwrap_or(OsStr::new(""));

    let mut file = file.clone();

    while file.as_path().exists() {
        let new_file_name = format!(
            "{}-{}.{}",
            file_name.to_string_lossy(),
            counter,
            extension.to_string_lossy()
        );
        counter += 1;

        file.set_file_name(new_file_name);
    }

    file.to_path_buf()
}

async fn download_file(url: &str, file_name: &PathBuf) -> Result<(), anyhow::Error> {
    let response = match reqwest::get(url).await {
        Ok(res) => res,
        Err(e) => {
            info!("Failed to download file from {}: {}", url, e);
            anyhow::bail!("Failed to download file from {}: {}", url, e);
        }
    };

    let mut file = std::fs::File::create(file_name.as_path())?;
    let mut content = Cursor::new(response.bytes().await?);
    std::io::copy(&mut content, &mut file)?;
    Ok(())
}

pub async fn process_recipe(recipe: &Recipe) -> Result<ID, anyhow::Error> {
    let resource_uuid = get_or_add_resource(&recipe.resource.to_resource()).await?;
    match resource_uuid {
        IdOrNone::NONE => {
            anyhow::bail!("Error adding resource: {}", recipe.resource.name);
        }
        _ => get_or_add_recipe(&recipe, &resource_uuid.to_id()).await,
    }
}

async fn get_or_add_recipe(recipe: &Recipe, resource_id: &ID) -> Result<ID, anyhow::Error> {
    let recipes = match find_recipes(recipe.recipe_type.as_str(), &resource_id.as_uuid()) {
        Ok(recipes) => recipes,
        Err(e) => {
            anyhow::bail!("Error finding recipes: {}", e);
        }
    };

    let ids: Vec<String> = recipe
        .ingredients
        .iter()
        .map(|i| i.resource.name.clone())
        .collect();

    for recipe_to_check in recipes {
        if recipe_to_check.quantity == recipe.resource.quantity as i32
            && recipe_to_check.duration == recipe.duration.millis as i32
            && recipe_to_check.unit == recipe.duration.unit
        {
            match find_ingredients_for_recipe(&recipe_to_check, &ids) {
                Ok(ingredients) => {
                    // Wenn alle Zutaten vorhanden sind existiert das Rezept
                    if ingredients.is_some() && ingredients.unwrap() == recipe.ingredients.len() {
                        return Ok(ID::EXISTING(recipe_to_check.id));
                    }
                }
                Err(e) => {
                    anyhow::bail!("Error finding ingredients for recipe: {}", e);
                }
            }
        }
    }

    let new_recipe = NewDbRecipe {
        resource: resource_id.as_uuid(),
        quantity: recipe.resource.quantity as i32,
        duration: recipe.duration.millis as i32,
        unit: recipe.duration.unit.clone(),
        state: None,
        recipe_type: recipe.recipe_type.as_string().clone(),
    };

    let recipe_id = match add_recipe(&new_recipe) {
        Ok(recipe_id) => {
            info!(
                "Added recipe {} to database with ID {}",
                recipe.resource.name, recipe_id
            );
            recipe_id
        }
        Err(e) => {
            anyhow::bail!(
                "Failed to add recipe for resource {} to database: {}",
                recipe.resource.name,
                e
            );
        }
    };

    for ingredient in &recipe.ingredients {
        add_ingredient_for_recipe(ingredient, &recipe_id).await?;
    }

    Ok(ID::NEW(recipe_id))
}

async fn add_ingredient_for_recipe(
    ingredient: &Ingredient,
    recipe_id: &Uuid,
) -> Result<(), anyhow::Error> {
    let resource_id = match get_or_add_resource(&ingredient.resource).await {
        Ok(uuid) => match uuid {
            IdOrNone::EXISTING(id) => id,
            IdOrNone::NEW(id) => id,
            IdOrNone::NONE => {
                anyhow::bail!(
                    "Error adding resource for ingredient {}: None",
                    ingredient.resource.name
                );
            }
        },
        Err(e) => {
            anyhow::bail!(
                "Error adding resource for ingredient {}: {}",
                ingredient.resource.name,
                e
            );
        }
    };

    let new_ingredient = db::models::NewDbIngredient {
        recipe: *recipe_id,
        resource: resource_id,
        quantity: ingredient.quantity as i32,
        state: None,
    };

    match add_ingredient(&new_ingredient) {
        Ok(_) => Ok(()),
        Err(e) => {
            anyhow::bail!("Error adding ingredient for recipe {}: {}", recipe_id, e);
        }
    }
}

async fn get_or_add_resource(resource: &Resource) -> Result<IdOrNone, anyhow::Error> {
    let resource_id = match get_resource_by_name(&resource.name) {
        Ok(res) => res,
        Err(e) => {
            anyhow::bail!("Error fetching resource by name: {}", e);
        }
    };

    if let Some(resource) = resource_id {
        return Ok(IdOrNone::EXISTING(resource.id));
    }

    let icon_id = match get_or_add_icon(&resource.icon).await? {
        IdOrNone::EXISTING(icon_id) => Some(icon_id),
        IdOrNone::NEW(icon_id) => Some(icon_id),
        IdOrNone::NONE => None,
    };

    let new_resource = NewDbResource {
        name: resource.name.clone(),
        title: resource.title.clone(),
        url: resource.url.clone(),
        icon: icon_id,
        state: None,
    };

    match add_resource(&new_resource) {
        Ok(resource_id) => {
            info!(
                "Added resource {} to database with ID {}",
                new_resource.name, resource_id
            );
            Ok(resource_id)
        }
        Err(e) => {
            anyhow::bail!(
                "Failed to add resource {} to database: {}",
                new_resource.name,
                e
            );
        }
    }
}

async fn get_or_add_icon(icon: &Option<Icon>) -> Result<IdOrNone, anyhow::Error> {
    if icon.is_none() {
        return Ok(IdOrNone::NONE);
    }

    let icon = icon.as_ref().unwrap();

    let db_icon = match get_icon_by_name(&icon.name) {
        Ok(db_icon) => db_icon,
        Err(e) => {
            anyhow::bail!("Error fetching icon by name: {}", e);
        }
    };

    if let Some(icon) = db_icon {
        return Ok(IdOrNone::EXISTING(icon.id));
    }

    let file = get_icon_file(icon);
    println!("Downloading icon {} to {}", icon.name, file.display());
    download_file(&icon.url, &file).await?;

    let new_icon: NewDbIcon = NewDbIcon {
        name: icon.name.clone(),
        url: icon.url.clone(),
        path: file.to_string_lossy().to_string(),
        content_type: icon.content_type.clone(),
        width: icon.width as i32,
        height: icon.height as i32,
        state: None,
    };

    match add_icon(&new_icon) {
        Ok(icon) => {
            info!("Added icon {} to database with ID {}", new_icon.name, icon);
            Ok(icon)
        }
        Err(e) => {
            anyhow::bail!("Failed to add icon {} to database: {}", new_icon.name, e);
        }
    }
}
