use crate::types::{
    Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState, ResourceWithQuantity,
};
use regex::Regex;
use select::{
    document::Document,
    node::Node,
    predicate::{Attr, Name, Or},
};
use std::collections::HashMap;

#[derive(Debug, PartialEq, Eq)]
enum ParseType {
    Link { url: String, title: String },
    Img(Icon),
    Count(u32),
    Resource(String),
    ResourceAdd,
    ResourceLast,
    DashDash,
    Duration(Duration),
}

type ResourceTmp = (
    Option<String>,   // name
    Option<String>,   // title
    Option<Icon>,     // icon
    Option<u32>,      // count
    Option<Duration>, // duration
);

pub fn parse(html: &str) -> Vec<Recipe> {
    let document = Document::from(html);

    let mut map_resource: HashMap<String, Resource> = HashMap::new();
    let mut recipes: Vec<Recipe> = Vec::new();

    parse_source(&document, &mut map_resource, &mut recipes);

    parse_dst(
        &document,
        "Raffination",
        RecipeType::Refining,
        &mut map_resource,
        &mut recipes,
    );

    parse_dst(
        &document,
        "Herstellung",
        RecipeType::Production,
        &mut map_resource,
        &mut recipes,
    );

    parse_dst(
        &document,
        "Raffination_2",
        RecipeType::Refining,
        &mut map_resource,
        &mut recipes,
    );

    parse_dst(
        &document,
        "Kochen",
        RecipeType::Cooking,
        &mut map_resource,
        &mut recipes,
    );

    recipes
}

fn first<T>(iter: &mut impl Iterator<Item = T>) -> Option<T> {
    iter.next()
}

fn first_child_element(node: Node<'_>) -> Option<Node<'_>> {
    if node.children().next().is_none() {
        return None;
    }

    let mut next = node.children().next();

    while next.is_some() {
        let child = next.unwrap();

        if child.name().is_some() {
            return Some(child);
        }

        next = child.next();
    }

    None
}

fn parse_resource_items(
    resource_items: Vec<ParseType>,
    recipe_type: RecipeType,
    map_resource: &mut HashMap<String, Resource>,
) -> Option<Recipe> {
    let mut tmp_resource: ResourceTmp = (None, None, None, None, None);

    let mut ingredient_to_add: Vec<Ingredient> = Vec::new();
    let mut not_add = false;

    for item in resource_items.iter() {
        match item {
            ParseType::Link { url, title } => {
                if tmp_resource.0.is_none() {
                    tmp_resource.0 = Some(title.to_string());
                }
                if tmp_resource.1.is_none() {
                    tmp_resource.1 = Some(url.to_string());
                }

                // println!("Link: {} - {}", url, title);
            }
            ParseType::Img(icon) => {
                if tmp_resource.2.is_none() {
                    tmp_resource.2 = Some(icon.clone());
                }
            }
            ParseType::Count(count) => {
                if tmp_resource.3.is_none() {
                    tmp_resource.3 = Some(*count);
                }
            }
            ParseType::Resource(resource) => {
                // println!("Resource: {}", resource);
                if tmp_resource.0.is_none() {
                    tmp_resource.0 = Some(resource.to_string());
                }
            }
            ParseType::ResourceAdd => {
                if !not_add {
                    add(&tmp_resource, map_resource, &mut ingredient_to_add);
                }

                not_add = false;

                tmp_resource = (None, None, None, None, None); // Reset for next resource
                // println!("ResourceAdd");
            }
            ParseType::ResourceLast => {
                if !not_add {
                    add(&tmp_resource, map_resource, &mut ingredient_to_add);
                }

                not_add = false;

                tmp_resource = (None, None, None, None, None); // Reset for next resource
                // println!("ResourceLast");
            }
            ParseType::Duration(duration) => {
                // println!(">>> Duration: {} {}", duration, unit);

                if tmp_resource.4.is_none() {
                    tmp_resource.4 = Some(duration.clone());
                }
            }
            ParseType::DashDash => {
                not_add = false;
                tmp_resource = (None, None, None, None, None); // Reset for next resource
            }
        }
    }

    let (_, ingredient) = create_resource_and_ingredient(&tmp_resource, map_resource);

    if !ingredient_to_add.is_empty() {
        let recipe = crate::types::Recipe {
            recipe_type: recipe_type,
            resource: ResourceWithQuantity::from_ingredient(ingredient),
            duration: tmp_resource.4.unwrap_or(Duration {
                millis: 0,
                unit: "Stück".to_string(),
            }),
            ingredients: ingredient_to_add,
        };

        return Some(recipe);
    }

    None
}

fn print_recipe(recipes: &Vec<Recipe>, recipe_type: RecipeType) {
    for recipe in recipes
        .iter()
        .filter(|recipe| recipe.recipe_type == recipe_type)
    {
        println!("Recipe Type: {:?}", recipe.recipe_type);
        let icon = match recipe.resource.icon {
            Some(ref icon) => format!("{}", icon),
            None => "(none)".to_string(),
        };

        println!(
            "Resource: {} ({}) => {}",
            recipe.resource.name, recipe.resource.quantity, icon
        );

        println!("Duration: {} ms", recipe.duration.millis);
        println!("Ingredients:");
        for ingredient in &recipe.ingredients {
            println!(
                "- {} ({} x {})",
                ingredient.resource.name, ingredient.quantity, ingredient.resource.title
            );
        }
        println!();
    }
}

fn parse_dst(
    document: &Document,
    id: &str,
    recipe_type: RecipeType,
    map_resource: &mut HashMap<String, Resource>,
    recipes: &mut Vec<Recipe>,
) -> bool {
    let mut dest = document.find(Attr("id", id));
    let dest = first(&mut dest);

    if dest.is_none() {
        return false;
    }

    let dst = dest.unwrap();
    let mut dst = dst.parent().unwrap();

    let mut elt_ul = None;

    while dst.next().is_some() {
        dst = dst.next().unwrap();

        if let Some(name) = dst.name() {
            if name == "ul" {
                elt_ul = Some(dst);
                break;
            }

            if name == "h2" {
                break;
            }

            let first_child = first_child_element(dst);
            if name == "h3"
                && first_child.is_some()
                && first_child.unwrap().name().unwrap() == "span"
                && first_child.unwrap().attr("id").is_some()
            {
                break;
            }
        }
    }

    if elt_ul.is_none() {
        return false;
    }

    let elt_ul = elt_ul.unwrap();
    let li = elt_ul.find(Name("li"));

    for item in li {
        if let Some(recipe) = parse_li_to_resource(&item, recipe_type.clone(), map_resource) {
            recipes.push(recipe);
        }
    }

    return true;
}

fn parse_source(
    document: &Document,
    map_resource: &mut HashMap<String, Resource>,
    recipes: &mut Vec<Recipe>,
) -> bool {
    let mut source = document.find(Attr("id", "Quelle"));
    let source = first(&mut source);

    if source.is_none() {
        return false;
    }

    let source = source.unwrap();
    let mut source = source.parent().unwrap();

    let mut c = 0;
    let mut elt_ul = None;

    while source.next().is_some() {
        source = source.next().unwrap();

        if let Some(name) = source.name() {
            if name == "ul" {
                c += 1;

                if c > 1 {
                    elt_ul = Some(source);
                    break;
                }
            }

            if name == "h2" {
                break;
            }
        }
    }

    if elt_ul.is_none() {
        return false;
    }
    let elt_ul = elt_ul.unwrap();
    let li = elt_ul.find(Name("li"));

    for item in li {
        if let Some(recipe) = parse_li_to_resource(&item, RecipeType::Refining, map_resource) {
            recipes.push(recipe);
        }
    }

    return true;
}

fn parse_li_to_resource(
    item: &Node<'_>,
    recipe_type: RecipeType,
    map_resource: &mut HashMap<String, Resource>,
) -> Option<Recipe> {
    if item.children().next().is_none() {
        return None;
    }

    let mut resource_items: Vec<ParseType> = Vec::new();

    let selector = item.find(Or(
        Name("strong"),
        Or(Or(Name("span"), Name("a")), Or(Name("img"), Name("small"))),
    ));

    for child in selector {
        let name = child.name().unwrap();

        if name == "a" && child.attr("href").is_some() && child.attr("title").is_some() {
            let txt = get_text_next(&child);

            resource_items.push(ParseType::Link {
                url: child.attr("href").unwrap().to_string(),
                title: child.attr("title").unwrap().to_string(),
            });

            if !txt.is_empty() {
                parse_text(&txt, &mut resource_items);
            }
            if txt == "--" {
                resource_items.push(ParseType::DashDash);
            }
        } else if name == "img"
            && child.attr("data-src").is_some()
            && child.attr("width").is_some()
            && child.attr("height").is_some()
            && child.attr("data-image-name").is_some()
        {
            let url = child.attr("data-src").unwrap();
            let name = child.attr("data-image-name").unwrap();
            let width = child.attr("width").unwrap().parse().unwrap_or(0);
            let height = child.attr("height").unwrap().parse().unwrap_or(0);

            resource_items.push(ParseType::Img(Icon {
                name: name.to_string(),
                url: url.to_string(),
                path: None, // Path will be set later
                width,
                height,
                content_type: "image/png".to_string(), // Assuming PNG, adjust as needed
            }));
        } else if name == "span"
            && !child.text().is_empty()
            && (child.parent().unwrap().name().unwrap() == "strong"
                || child.parent().unwrap().name().unwrap() == "span")
        {
            let txt = child.text().trim().to_string();
            resource_items.push(ParseType::Resource(txt));
            let txt = get_text_next(&child.parent().unwrap());
            parse_text(&txt, &mut resource_items);
        } else if name == "strong" {
            // let txt = get_text_next(&child);
            // parse_text(&txt, &mut resource_items);
        } else if name == "small" {
            let txt = get_text(&child);
            parse_text(&txt, &mut resource_items);
        }
    }

    parse_resource_items(resource_items, recipe_type, map_resource)
}

fn to_name(name: &str) -> String {
    normalize_text(name).replace(" ", "_").to_lowercase()
}
fn create_resource_and_ingredient(
    tmp_resource: &ResourceTmp,
    map_resource: &mut HashMap<String, Resource>,
) -> (Resource, Ingredient) {
    let title = tmp_resource.0.as_ref().unwrap().clone();
    let name = to_name(&title);
    let url = tmp_resource.1.clone();
    let icon = tmp_resource.2.clone();
    let count = tmp_resource.3.unwrap_or(1);

    let mut resource = Resource {
        name: name.clone(),
        title,
        url,
        icon,
        state: ResourceState::Unparsed,
    };

    if map_resource.contains_key(&name) {
        let res = map_resource.get_mut(&name).unwrap();

        if res.url.is_some() {
            resource.url = res.url.clone();
        }
        if res.icon.is_some() {
            resource.icon = res.icon.clone();
        }
    }

    let ingredient = Ingredient {
        resource: resource.clone(),
        quantity: count,
    };

    (resource, ingredient)
}

fn add(
    tmp_resource: &ResourceTmp,
    map_resource: &mut HashMap<String, Resource>,
    ingredient_to_add: &mut Vec<Ingredient>,
) {
    if tmp_resource.0.is_none() {
        return;
    }

    let (resource, ingredient) = create_resource_and_ingredient(tmp_resource, map_resource);

    map_resource.insert(resource.name.clone(), resource);

    ingredient_to_add.push(ingredient);
}

fn normalize_text(text: &str) -> String {
    let mut text = text
        .trim()
        .replace('\n', " ")
        .replace('\r', " ")
        .replace('\t', " ");

    for c in text.clone().chars() {
        if c.is_control() || c.is_whitespace() {
            text = text.replace(c, " ");
        }
    }

    while text.contains("  ") {
        text = text.replace("  ", " ");
    }

    text
}

fn get_text(node: &Node<'_>) -> String {
    let mut text = String::new();

    if node.as_text().is_some() {
        text.push_str(node.as_text().unwrap().to_string().as_str());
    }

    if node.children().next().is_some() {
        for child in node.descendants() {
            if child.as_text().is_some() {
                text.push_str(child.as_text().unwrap().to_string().as_str());
            }
        }
    }

    if node.next().is_some() {
        let mut next = node.next();

        while next.is_some() {
            let next_node = next.unwrap();

            if next_node.as_text().is_some() {
                text.push_str(next_node.as_text().unwrap());
            } else {
                break;
            }

            next = next_node.next();
        }
    }

    return normalize_text(&text);
}

fn get_text_next(node: &Node<'_>) -> String {
    if node.as_text().is_some() {
        return normalize_text(&node.as_text().unwrap());
    }

    let next = node.next();

    if !next.is_some() {
        return String::new();
    }

    let next = next.unwrap();

    if next.as_text().is_some() {
        let mut text = next.as_text().unwrap().trim().to_string();
        let mut next = next.next();

        while next.is_some() {
            let node = next.unwrap();
            next = node.next();

            if node.as_text().is_some() {
                text.push_str(node.as_text().unwrap().trim());
            } else if node.name().is_some() && node.name().unwrap() == "i" {
                text.push_str(node.text().trim());
            } else {
                break;
            }
        }

        return normalize_text(&text);
    }

    String::new()
}

fn parse_text(text: &str, resource_items: &mut Vec<ParseType>) {
    let reg_count_next = Regex::new(r"^\s*x(?<count>\d+)\s+(?<end>[→+])\s*$").unwrap();
    let reg_count = Regex::new(r"^\s*x(?<count>\d+)\s*.*$").unwrap();
    let reg_duration =
        Regex::new(r"^.*\(.*(?<duration>\d+(|,\d+))\ssek\./(?<unit>\w+)\s*\)$").unwrap();

    if let Some(res) = reg_count_next.captures(text) {
        let count = res.name("count").unwrap().as_str().parse().unwrap_or(0);

        resource_items.push(ParseType::Count(count));

        let end = res.name("end").unwrap().as_str().to_string();

        if end == "+" {
            resource_items.push(ParseType::ResourceAdd);
        } else {
            resource_items.push(ParseType::ResourceLast);
        }

        return;
    }

    if let Some(res) = reg_count.captures(text) {
        let count = res.name("count").unwrap().as_str().parse().unwrap_or(0);

        resource_items.push(ParseType::Count(count));
    }

    if let Some(res) = reg_duration.captures(text) {
        let duration_str = res.name("duration").unwrap().as_str();
        let duration: f64 = duration_str.replace(',', ".").parse().unwrap_or(0.0);
        let unit = res.name("unit").unwrap().as_str().to_string();
        let duration: u64 = (duration * 1000.0) as u64; // Convert to milliseconds

        resource_items.push(ParseType::Duration(Duration {
            millis: duration,
            unit,
        }));
    }
}
