from django.db.models import F
from profiles.models import Pantry
from recipes.models import Ingredient, UnitConversion
import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from .models import Favorite
from django.db.models import Q


def get_owned_raw_ingredients(user):
    pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
    unit_conversions = UnitConversion.objects.select_related(
        'higher_unit', 'lower_unit').all()

    owned_raw_ingredient_ids = set()

    for pantry_id in pantry_ids:
        pantry = Pantry.objects.select_related(
            'unit', 'raw_ingredient').get(id=pantry_id)
        raw_ingredient_id = pantry.raw_ingredient_id

        ingredients = Ingredient.objects.filter(
            Q(raw_ingredient_id=raw_ingredient_id) &
            Q(raw_ingredient__pantry__user=user)
        ).select_related('unit')

        for ingredient in ingredients:
            if pantry.unit is None:
                owned_raw_ingredient_ids.add(raw_ingredient_id)
            elif ingredient.unit is None:
                continue
            else:
                if ingredient.unit == pantry.unit:
                    if pantry.amount >= ingredient.amount:
                        owned_raw_ingredient_ids.add(raw_ingredient_id)
                else:
                    if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
                        if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
                            higher = pantry
                            lower = ingredient
                        else:
                            higher = ingredient
                            lower = pantry

                        conversion = UnitConversion.objects.get(
                            higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
                        if conversion:
                            converted_amount = conversion.value * higher.amount
                            if higher == pantry and converted_amount >= lower.amount:
                                owned_raw_ingredient_ids.add(raw_ingredient_id)
                            elif higher == ingredient and lower.amount >= converted_amount:
                                owned_raw_ingredient_ids.add(raw_ingredient_id)
                        else:
                            common_unit_id = find_common_unit(
                                higher.unit.id, lower.unit.id, unit_conversions
                            )
                            if common_unit_id:
                                converted_higher_amount = convert_amount(
                                    higher.amount, higher.unit.id, common_unit_id, unit_conversions
                                )
                                converted_lower_amount = convert_amount(
                                    lower.amount, lower.unit.id, common_unit_id, unit_conversions
                                )
                                if converted_higher_amount >= converted_lower_amount:
                                    owned_raw_ingredient_ids.add(
                                        raw_ingredient_id)
                    else:
                        if ingredient.unit.category == pantry.unit.category:
                            if pantry.unit.hierarchy < ingredient.unit.hierarchy:
                                owned_raw_ingredient_ids.add(raw_ingredient_id)
                            elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
                                if pantry.amount >= ingredient.amount:
                                    owned_raw_ingredient_ids.add(
                                        raw_ingredient_id)
                        elif pantry.unit.category == 'item':
                            owned_raw_ingredient_ids.add(raw_ingredient_id)

    return owned_raw_ingredient_ids


def find_common_unit(higher_unit_id, lower_unit_id, unit_conversions):
    if higher_unit_id == lower_unit_id:
        return higher_unit_id

    conversion = unit_conversions.filter(
        higher_unit_id=higher_unit_id, lower_unit_id=lower_unit_id
    ).first()
    if conversion:
        return lower_unit_id

    conversion = unit_conversions.filter(
        higher_unit_id=lower_unit_id, lower_unit_id=higher_unit_id
    ).first()
    if conversion:
        return higher_unit_id

    for conversion in unit_conversions:
        if conversion.higher_unit_id == higher_unit_id:
            common_unit = find_common_unit(
                conversion.lower_unit_id, lower_unit_id, unit_conversions
            )
            if common_unit:
                return common_unit
    return None


def convert_amount(amount, from_unit_id, to_unit_id, unit_conversions):
    if from_unit_id == to_unit_id:
        return amount

    conversion = unit_conversions.filter(
        higher_unit_id=from_unit_id, lower_unit_id=to_unit_id
    ).first()
    if conversion:
        return amount * conversion.value

    conversion = unit_conversions.filter(
        higher_unit_id=to_unit_id, lower_unit_id=from_unit_id
    ).first()
    if conversion:
        return amount / conversion.value

    common_unit_id = find_common_unit(
        from_unit_id, to_unit_id, unit_conversions)
    if common_unit_id:
        return convert_amount(
            convert_amount(amount, from_unit_id,
                           common_unit_id, unit_conversions),
            common_unit_id, to_unit_id, unit_conversions
        )

    return None


def get_owned_recipe_ingredients(user, recipe_id):
    pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
    unit_conversions = UnitConversion.objects.select_related(
        'higher_unit', 'lower_unit').all()

    owned_ingredient_ids = set()

    for pantry_id in pantry_ids:
        pantry = Pantry.objects.select_related(
            'unit', 'raw_ingredient').get(id=pantry_id)
        raw_ingredient_id = pantry.raw_ingredient_id

        ingredients = Ingredient.objects.filter(
            recipe_id=recipe_id, raw_ingredient_id=raw_ingredient_id, raw_ingredient__pantry__user=user).select_related('unit')

        for ingredient in ingredients:
            if pantry.unit is None:
                owned_ingredient_ids.add(ingredient.id)
            elif ingredient.unit is None:
                continue
            else:
                if ingredient.unit == pantry.unit:
                    if pantry.amount >= ingredient.amount:
                        owned_ingredient_ids.add(ingredient.id)
                else:
                    if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
                        if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
                            higher = pantry
                            lower = ingredient
                        else:
                            higher = ingredient
                            lower = pantry

                        conversion = UnitConversion.objects.get(
                            higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
                        if conversion:
                            converted_amount = conversion.value * higher.amount
                            if higher == pantry and converted_amount >= lower.amount:
                                owned_ingredient_ids.add(ingredient.id)
                            elif higher == ingredient and lower.amount >= converted_amount:
                                owned_ingredient_ids.add(ingredient.id)
                        else:
                            common_unit_id = find_common_unit(
                                higher.unit.id, lower.unit.id, unit_conversions
                            )
                            if common_unit_id:
                                converted_higher_amount = convert_amount(
                                    higher.amount, higher.unit.id, common_unit_id, unit_conversions
                                )
                                converted_lower_amount = convert_amount(
                                    lower.amount, lower.unit.id, common_unit_id, unit_conversions
                                )
                                if converted_higher_amount >= converted_lower_amount:
                                    owned_ingredient_ids.add(ingredient.id)
                    else:
                        if ingredient.unit.category == pantry.unit.category:
                            if pantry.unit.hierarchy < ingredient.unit.hierarchy:
                                owned_ingredient_ids.add(ingredient.id)
                            elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
                                if pantry.amount >= ingredient.amount:
                                    owned_ingredient_ids.add(ingredient.id)
                        elif pantry.unit.category == 'item':
                            owned_ingredient_ids.add(ingredient.id)

    return owned_ingredient_ids


def jaccard_similarity(owned, set1, set2):
    intersection = len(owned)
    union = len(set1.union(set2))    
    similarity = intersection / union
    return similarity


def calculate_top_recipes_by_ingredients(user):    
    pantry = set(Pantry.objects.filter(user=user).values_list('raw_ingredient_id', flat=True))
    # Get valid recipes
    recipe_ingredients = Ingredient.objects.filter(        
        raw_ingredient__pantry__user=user
    ).values('recipe_id', 'raw_ingredient_id', 'id', user_id=F('raw_ingredient__pantry__user_id'))

    data = {}
    for ingredient in recipe_ingredients:
        user_id = ingredient['user_id']
        recipe_id = ingredient['recipe_id']
        raw_ingredient_id = ingredient['raw_ingredient_id']
        # ingredient_id = ingredient['id']

        if recipe_id in data:
            data[recipe_id]['raw_ingredient_ids'].add(raw_ingredient_id)
        else:
            data[recipe_id] = {
                'user_id': user_id,
                'recipe_id': recipe_id,
                'raw_ingredient_ids': {raw_ingredient_id}
            }

    for recipe_id, recipe_data in data.items():
        raw_ingredient_ids = set(Ingredient.objects.filter(recipe_id=recipe_id).values_list('raw_ingredient_id', flat=True))
        owned_raw_ingredient = set(get_owned_recipe_ingredients(user, recipe_id))        
        similarity = jaccard_similarity(
            owned_raw_ingredient, pantry, raw_ingredient_ids)
        
        recipe_data['similarity'] = similarity * 100

    sorted_recipes = sorted(
        data.values(), key=lambda x: x['similarity'], reverse=True)

    return sorted_recipes


def create_interaction_matrix():
    favorites = Favorite.objects.all()
    user_ids = []
    recipe_ids = []

    for favorite in favorites:
        user_ids.append(favorite.user_id)
        recipe_ids.append(favorite.recipe_id)

    unique_user_ids = list(set(user_ids))
    unique_recipe_ids = list(set(recipe_ids))

    interaction_matrix = pd.DataFrame(
        0, index=unique_user_ids, columns=unique_recipe_ids)

    for i in range(len(user_ids)):
        user_id = user_ids[i]
        recipe_id = recipe_ids[i]
        interaction_matrix.loc[user_id, recipe_id] = 1

    return interaction_matrix


def create_interaction_matrix_hybrid():
    favorites = Favorite.objects.all()
    user_ids = []
    recipe_ids = []
    ratings = []

    for favorite in favorites:
        user_ids.append(favorite.user_id)
        recipe_ids.append(favorite.recipe_id)
        ratings.append(favorite.rating)  # Menambahkan rating

    unique_user_ids = list(set(user_ids))
    unique_recipe_ids = list(set(recipe_ids))

    interaction_matrix = pd.DataFrame(
        0, index=unique_user_ids, columns=unique_recipe_ids)

    for i in range(len(user_ids)):
        user_id = user_ids[i]
        recipe_id = recipe_ids[i]
        rating = ratings[i]
        # Menggunakan rating sebagai nilai
        interaction_matrix.loc[user_id, recipe_id] = rating

    return interaction_matrix


def calculate_cosine_similarity():
    interaction_matrix = create_interaction_matrix()
    similarity_matrix = cosine_similarity(interaction_matrix.T)
    similarity_df = pd.DataFrame(
        similarity_matrix, index=interaction_matrix.columns, columns=interaction_matrix.columns)
    return similarity_df, interaction_matrix


def get_recommendations_for_user(user_id):
    similarity_matrix, interaction_matrix = calculate_cosine_similarity()

    if user_id in interaction_matrix.index:
        scores = np.dot(interaction_matrix.values, similarity_matrix)

        recommended_items = [interaction_matrix.columns[i]
                             for i in np.argsort(scores[interaction_matrix.index == user_id, :])[0][::-1]]

        recommendations = []

        min_score = np.min(scores)
        max_score = np.max(scores)

        if min_score == max_score:
            return []

        # Normalize the scores to a range of 0-100
        normalized_scores = 100 * \
            (scores - min_score) / (max_score - min_score)

        for item in recommended_items:
            score = scores[interaction_matrix.index ==
                           user_id, interaction_matrix.columns == item][0]
            similarity_score = normalized_scores[interaction_matrix.index ==
                                                 user_id, interaction_matrix.columns == item][0]
            if score > 0:
                recommendation = {
                    'recipe_id': item,
                    'similarity': round(similarity_score, 2),
                    'score': round(score, 2)
                }
                recommendations.append(recommendation)
        return recommendations
    else:
        return []


def get_hybrid_recommendation(user_id):
    similarity_matrix_cbf, _ = calculate_cosine_similarity()

    interaction_matrix = create_interaction_matrix()  # Move this line here

    if user_id in interaction_matrix.index:
        similarity_matrix_ibcf = cosine_similarity(interaction_matrix.T)
        similarity_matrix = 0.6 * similarity_matrix_cbf + 0.4 * similarity_matrix_ibcf

        recommended_items = list(interaction_matrix.columns)
        scores = np.dot(interaction_matrix.values, similarity_matrix)
        scores_filtered = scores[interaction_matrix.index == user_id, :]
        sorted_indices = np.argsort(scores_filtered[0])[::-1]
        recommended_items_sorted = [interaction_matrix.columns[i]
                                    for i in sorted_indices if interaction_matrix.columns[i] in recommended_items]

        return recommended_items_sorted
    else:
        return []
