# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

from datetime import timedelta

from api.viewsets import is_regex
from crispy_forms.helper import FormHelper
from django_tables2.views import SingleTableView, MultiTableMixin
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, Count
from django.http import HttpResponseRedirect, Http404, JsonResponse
from django.views.decorators.http import require_GET
from django.views.generic import DetailView, UpdateView, CreateView
from django.views.generic.list import ListView
from django.views.generic.base import RedirectView
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership
from activity.models import Activity
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin

from .models import Food, BasicFood, TransformedFood, QRCode, Order, Dish, Supplement, Recipe
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \
    ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \
    BasicFoodUpdateForms, TransformedFoodUpdateForms, \
    DishForm, SupplementFormSet, SupplementFormSetHelper, OrderForm, RecipeForm, \
    RecipeIngredientsForm, RecipeIngredientsFormSet, UseRecipeForm
from .tables import FoodTable, DishTable, OrderTable, RecipeTable
from .utils import pretty_duration


class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
    """
    Display Food
    """
    model = Food
    tables = [FoodTable, FoodTable, FoodTable, ]
    extra_context = {"title": _('Food')}
    template_name = 'food/food_list.html'

    def get_queryset(self, **kwargs):
        return super().get_queryset(**kwargs).distinct()

    def get_tables(self):
        bureau_role_pk = 4
        clubs = Club.objects.filter(membership__in=Membership.objects.filter(
            user=self.request.user, roles=bureau_role_pk).filter(
                date_end__gte=timezone.now()))

        tables = [FoodTable] * (clubs.count() + 3)
        self.tables = tables
        tables = super().get_tables()
        tables[0].prefix = 'search-'
        tables[1].prefix = 'open-'
        tables[2].prefix = 'served-'
        for i in range(clubs.count()):
            tables[i + 3].prefix = clubs[i].name
        return tables

    def get_tables_data(self):
        # table search
        qs = self.get_queryset().order_by('name')
        if "search" in self.request.GET and self.request.GET['search']:
            pattern = self.request.GET['search']

            # check regex
            valid_regex = is_regex(pattern)
            suffix = '__iregex' if valid_regex else '__istartswith'
            prefix = '^' if valid_regex else ''
            qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
                           | Q(**{f'owner__name{suffix}': prefix + pattern})
                           | Q(**{f'owner__note__alias__name{suffix}': prefix + pattern}))
        else:
            qs = qs.none()
        if "stock" not in self.request.GET or not self.request.GET["stock"] == '1':
            qs = qs.filter(end_of_life='')

        search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
        # table open
        open_table = self.get_queryset().filter(
            Q(polymorphic_ctype__model='transformedfood')
            | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
                expiry_date__lt=timezone.now(), end_of_life='').filter(
                    PermissionBackend.filter_queryset(self.request, Food, 'view'))
        open_table = open_table.union(self.get_queryset().filter(
            Q(end_of_life='', order__iexact='open')
        ).filter(
            PermissionBackend.filter_queryset(self.request, Food, 'view'))).order_by('expiry_date')
        # table served
        served_table = self.get_queryset().order_by('-pk').filter(
            end_of_life='', is_ready=True).exclude(
                Q(polymorphic_ctype__model='basicfood',
                  basicfood__date_type='DLC',
                  expiry_date__lte=timezone.now(),)
                | Q(polymorphic_ctype__model='transformedfood',
                    expiry_date__lte=timezone.now(),
                    ))
        # tables club
        bureau_role_pk = 4
        clubs = Club.objects.filter(membership__in=Membership.objects.filter(
            user=self.request.user, roles=bureau_role_pk).filter(
                date_end__gte=timezone.now()))
        club_table = []
        for club in clubs:
            club_table.append(self.get_queryset().order_by('expiry_date').filter(
                owner=club, end_of_life='').filter(
                    PermissionBackend.filter_queryset(self.request, Food, 'view')
            ))

        return [search_table, open_table, served_table] + club_table

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        tables = context['tables']
        # for extends base_search.html we need to name 'search_table' in 'table'
        for name, table in zip(['table', 'open', 'served'], tables):
            context[name] = table
        context['club_tables'] = tables[3:]

        context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')

        context['can_add_recipe'] = PermissionBackend.check_perm(self.request, 'food.recipe_add')

        context['can_view_recipes'] = PermissionBackend.check_perm(self.request, 'food.recipe_view')

        context["open_activities"] = Activity.objects.filter(activity_type__name="Perm bouffe", open=True)

        return context


class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
    """
    A view to add qrcode
    """
    model = QRCode
    template_name = 'food/qrcode.html'
    form_class = QRCodeForms
    extra_context = {"title": _("Add a new QRCode")}

    def get(self, *args, **kwargs):
        qrcode = kwargs["slug"]
        if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
            pk = self.model.objects.get(qr_code_number=qrcode).food_container.pk
            return HttpResponseRedirect(reverse_lazy("food:food_view", kwargs={"pk": pk}))
        else:
            return super().get(*args, **kwargs)

    @transaction.atomic
    def form_valid(self, form):
        qrcode_food_form = QRCodeForms(data=self.request.POST)
        if not qrcode_food_form.is_valid():
            return self.form_invalid(form)

        qrcode = form.save(commit=False)
        qrcode.qr_code_number = self.kwargs['slug']
        qrcode._force_save = True
        qrcode.save()
        qrcode.refresh_from_db()
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['slug'] = self.kwargs['slug']

        # get last 10 BasicFood objects with distincts 'name' ordered by '-pk'
        # we can't use .distinct and .order_by with differents columns hence the generator
        context['last_items'] = [food for food in BasicFood.get_lastests_objects(10, 'name', '-pk')]
        return context

    def get_success_url(self, **kwargs):
        self.object.refresh_from_db()
        return reverse_lazy('food:food_view', kwargs={'pk': self.object.food_container.pk})

    def get_sample_object(self):
        return QRCode(
            qr_code_number=self.kwargs['slug'],
            food_container_id=1,
        )


class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    A view to add basicfood
    """
    model = BasicFood
    form_class = BasicFoodForms
    extra_context = {"title": _("Add an aliment")}
    template_name = "food/food_update.html"

    def get_sample_object(self):
        # We choose a club which may work or BDE else
        food = BasicFood(
            name="",
            owner_id=1,
            expiry_date=timezone.now(),
            is_ready=True,
            arrival_date=timezone.now(),
            date_type='DLC',
        )

        for membership in self.request.user.memberships.all():
            club_id = membership.club.id
            food.owner_id = club_id
            if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
                return food

        return food

    @transaction.atomic
    def form_valid(self, form):
        if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
            return HttpResponseRedirect(reverse_lazy('food:qrcode_create', kwargs={'slug': self.kwargs['slug']}))
        food_form = BasicFoodForms(data=self.request.POST)
        if not food_form.is_valid():
            return self.form_invalid(form)

        food = form.save(commit=False)
        food.is_ready = False
        food.save()
        food.refresh_from_db()

        qrcode = QRCode()
        qrcode.qr_code_number = self.kwargs['slug']
        qrcode.food_container = food
        qrcode.save()

        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        self.object.refresh_from_db()
        return reverse_lazy('food:basicfood_view', kwargs={"pk": self.object.pk})

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)

        copy = self.request.GET.get('copy', None)
        if copy is not None:
            food = BasicFood.objects.get(pk=copy)

            for field in context['form'].fields:
                if field == 'allergens':
                    context['form'].fields[field].initial = getattr(food, field).all()
                elif field == 'traces':
                    context['form'].fields[field].initial = getattr(food, field).all()
                else:
                    context['form'].fields[field].initial = getattr(food, field)

        return context


class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    A view to add transformedfood
    """
    model = TransformedFood
    form_class = TransformedFoodForms
    extra_context = {"title": _("Add a meal")}
    template_name = "food/food_update.html"

    def get_sample_object(self):
        # We choose a club which may work or BDE else
        food = TransformedFood(
            name="",
            owner_id=1,
            expiry_date=timezone.now(),
            is_ready=True,
        )

        for membership in self.request.user.memberships.all():
            club_id = membership.club.id
            food.owner_id = club_id
            if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
                return food

        return food

    @transaction.atomic
    def form_valid(self, form):
        form.instance.expiry_date = timezone.now() + timedelta(days=3)
        form.instance.is_ready = False
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        self.object.refresh_from_db()
        return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})


MAX_FORMS = 100


class ManageIngredientsView(LoginRequiredMixin, UpdateView):
    """
    A view to manage ingredient for a transformed food
    """
    model = TransformedFood
    fields = ['ingredients']
    extra_context = {"title": _("Manage ingredients of:")}
    template_name = 'food/manage_ingredients.html'

    @transaction.atomic
    def form_valid(self, form):
        old_ingredients = list(self.object.ingredients.all()).copy()
        old_allergens = list(self.object.allergens.all()).copy()
        old_traces = list(self.object.traces.all()).copy()
        self.object.ingredients.clear()
        for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS):
            prefix = 'form-' + str(i) + '-'

            ingredient = None
            if form.data[prefix + 'qrcode'] not in ['0', '', 'NaN']:
                ingredient = QRCode.objects.get(pk=form.data[prefix + 'qrcode']).food_container

            elif form.data[prefix + 'name'] != '':
                ingredient = Food.objects.get(pk=form.data[prefix + 'name'])

            if form.data.get(prefix + 'add_all_same_name') == 'on':
                ingredients = Food.objects.filter(name=ingredient.name, owner=ingredient.owner, end_of_life='')
            else:
                ingredients = [ingredient]

            for ingredient in ingredients:
                self.object.ingredients.add(ingredient)
                if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
                    ingredient.end_of_life = _('Fully used in {meal}'.format(
                        meal=self.object.name))
                    ingredient.save()

        # We recalculate new expiry date and allergens
        self.object.expiry_date = self.object.creation_date + self.object.shelf_life
        self.object.allergens.clear()
        self.object.traces.clear()

        for ingredient in self.object.ingredients.iterator():
            if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
                self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
            self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
            self.object.traces.set(self.object.traces.union(ingredient.traces.all()))

        self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
        return HttpResponseRedirect(self.get_success_url())

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['title'] += ' ' + self.object.name
        formset = ManageIngredientsFormSet()
        ingredients = self.object.ingredients.all()
        formset.extra += ingredients.count() + MAX_FORMS
        context['form'] = ManageIngredientsForm()
        context['ingredients_count'] = ingredients.count()
        display = [True] * (1 + ingredients.count()) + [False] * (formset.extra - ingredients.count() - 1)
        context['formset'] = zip(display, formset)
        context['ingredients'] = []
        for ingredient in ingredients:
            qr = QRCode.objects.filter(food_container=ingredient)

            context['ingredients'].append({
                'food_pk': ingredient.pk,
                'food_name': ingredient.name,
                'qr_pk': '' if qr.count() == 0 else qr[0].pk,
                'qr_number': '' if qr.count() == 0 else qr[0].qr_code_number,
                'fully_used': 'true' if ingredient.end_of_life else '',
            })

        return context

    def get_success_url(self, **kwargs):
        return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})


class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    A view to add ingredient to a meal
    """
    model = Food
    extra_context = {"title": _("Add the ingredient:")}
    form_class = AddIngredientForms
    template_name = 'food/food_update.html'

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['title'] += ' ' + self.object.name
        return context

    @transaction.atomic
    def form_valid(self, form):
        meals = TransformedFood.objects.filter(pk__in=form.data.getlist('ingredients')).all()
        if not meals:
            return HttpResponseRedirect(reverse_lazy('food:food_view', kwargs={"pk": self.object.pk}))
        for meal in meals:
            old_ingredients = list(meal.ingredients.all()).copy()
            old_allergens = list(meal.allergens.all()).copy()
            old_traces = list(meal.traces.all()).copy()
            meal.ingredients.add(self.object.pk)
            # update allergen and expiry date if necessary
            if not (self.object.polymorphic_ctype.model == 'basicfood'
                    and self.object.date_type == 'DDM'):
                meal.expiry_date = min(meal.expiry_date, self.object.expiry_date)
            meal.allergens.set(meal.allergens.union(self.object.allergens.all()))
            meal.traces.set(meal.traces.union(self.object.traces.all()))
            meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
            if 'fully_used' in form.data:
                if not self.object.end_of_life:
                    self.object.end_of_life = _(f'Food fully used in : {meal.name}')
                else:
                    self.object.end_of_life += ', ' + meal.name
        if 'fully_used' in form.data:
            self.object.is_ready = False
        self.object.save()
        # We redirect only the first parent
        parent_pk = meals[0].pk
        return HttpResponseRedirect(self.get_success_url(parent_pk=parent_pk))

    def get_success_url(self, **kwargs):
        return reverse_lazy('food:transformedfood_view', kwargs={"pk": kwargs['parent_pk']})


class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    A view to update Food
    """
    model = Food
    extra_context = {"title": _("Update an aliment")}
    template_name = 'food/food_update.html'

    @transaction.atomic
    def form_valid(self, form):
        form.instance.creater = self.request.user
        food = Food.objects.get(pk=self.kwargs['pk'])
        old_allergens = list(food.allergens.all()).copy()
        old_traces = list(food.traces.all()).copy()

        if food.polymorphic_ctype.model == 'transformedfood':
            old_ingredients = food.ingredients.all()
            form.instance.shelf_life = timedelta(
                seconds=int(form.data['shelf_life']) * 60 * 60)

        food_form = self.get_form_class()(data=self.request.POST)
        if not food_form.is_valid():
            return self.form_invalid(form)
        ans = super().form_valid(form)
        if food.polymorphic_ctype.model == 'transformedfood':
            form.instance.save(old_ingredients=old_ingredients)
        else:
            form.instance.save(old_allergens=old_allergens, old_traces=old_traces)
        return ans

    def get_form_class(self, **kwargs):
        food = Food.objects.get(pk=self.kwargs['pk'])
        if food.polymorphic_ctype.model == 'basicfood':
            return BasicFoodUpdateForms
        else:
            return TransformedFoodUpdateForms

    def get_form(self, **kwargs):
        form = super().get_form(**kwargs)
        if 'shelf_life' in form.initial:
            hours = form.initial['shelf_life'].days * 24 + form.initial['shelf_life'].seconds // 3600
            form.initial['shelf_life'] = hours
        return form

    def get_success_url(self, **kwargs):
        self.object.refresh_from_db()
        return reverse_lazy('food:food_view', kwargs={"pk": self.object.pk})


class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
    """
    A view to see a food
    """
    model = Food
    extra_context = {"title": _('Details of:')}
    context_object_name = "food"
    template_name = "food/food_detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        fields = ["name", "owner", "expiry_date", "allergens", "traces", "is_ready", "end_of_life", "order"]

        fields = dict([(field, getattr(self.object, field)) for field in fields])
        if fields["is_ready"]:
            fields["is_ready"] = _("Yes")
        else:
            fields["is_ready"] = _("No")
        fields["allergens"] = ", ".join(
            allergen.name for allergen in fields["allergens"].all())
        fields["traces"] = ", ".join(
            trace.name for trace in fields["traces"].all())

        context["fields"] = [(
            Food._meta.get_field(field).verbose_name.capitalize(),
            value) for field, value in fields.items()]
        if self.object.QR_code.exists():
            context["QR_code"] = self.object.QR_code.first()
        context["meals"] = self.object.transformed_ingredient_inv.all()
        context["update"] = PermissionBackend.check_perm(self.request, "food.change_food")
        context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood"))
        return context

    def get(self, *args, **kwargs):
        if Food.objects.filter(pk=kwargs['pk']).count() != 1:
            return Http404
        model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model
        if 'stop_redirect' in kwargs and kwargs['stop_redirect']:
            return super().get(*args, **kwargs)
        kwargs = {'pk': kwargs['pk']}
        if model == 'basicfood':
            return HttpResponseRedirect(reverse_lazy("food:basicfood_view", kwargs=kwargs))
        return HttpResponseRedirect(reverse_lazy("food:transformedfood_view", kwargs=kwargs))


class BasicFoodDetailView(FoodDetailView):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        fields = ['arrival_date', 'date_type']
        for field in fields:
            context["fields"].append((
                BasicFood._meta.get_field(field).verbose_name.capitalize(),
                getattr(self.object, field)
            ))
        return context

    def get(self, *args, **kwargs):
        if Food.objects.filter(pk=kwargs['pk']).count() == 1:
            kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'basicfood')
        return super().get(*args, **kwargs)


class TransformedFoodDetailView(FoodDetailView):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["fields"].append((
            TransformedFood._meta.get_field("creation_date").verbose_name.capitalize(),
            self.object.creation_date
        ))
        context["fields"].append((
            TransformedFood._meta.get_field("shelf_life").verbose_name.capitalize(),
            pretty_duration(self.object.shelf_life)
        ))
        context["foods"] = self.object.ingredients.all()
        context["manage_ingredients"] = True
        return context

    def get(self, *args, **kwargs):
        if Food.objects.filter(pk=kwargs['pk']).count() == 1:
            kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood')
        return super().get(*args, **kwargs)


class QRCodeRedirectView(RedirectView):
    """
    Redirects to the QR code creation page from Food List
    """
    def get_redirect_url(self, *args, **kwargs):
        slug = self.request.GET.get('slug')
        if slug:
            return reverse_lazy('food:qrcode_create', kwargs={'slug': slug})
        return reverse_lazy('food:list')


class DishCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Create a dish
    """
    model = Dish
    form_class = DishForm
    extra_context = {"title": _('Create dish')}

    def get_sample_object(self):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        sample_food = TransformedFood(
            name="Sample food",
            owner=activity.organizer,
            expiry_date=timezone.now() + timedelta(days=7),
            is_ready=True,
        )
        sample_dish = Dish(
            main=sample_food,
            price=100,
            activity=activity,
        )
        return sample_dish

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        form = context['form']
        form.helper = FormHelper()
        # Remove form tag on the generation of the form in the template (already present on the template)
        form.helper.form_tag = False
        # The formset handles the set of the supplements
        form_set = SupplementFormSet(instance=form.instance)
        context['formset'] = form_set
        context['helper'] = SupplementFormSetHelper()

        return context

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        if "available" in form.fields:
            del form.fields["available"]
        return form

    @transaction.atomic
    def form_valid(self, form):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])

        form.instance.activity = activity

        ret = super().form_valid(form)

        # For each supplement, we save it
        formset = SupplementFormSet(self.request.POST, instance=form.instance)
        if formset.is_valid():
            for f in formset:
                # We don't save the product if the price is not entered, ie. if the line is empty
                if f.is_valid() and f.instance.price:
                    f.save()
                    f.instance.save()
                else:
                    f.instance = None

        return ret

    def get_success_url(self):
        return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]})


class DishListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    List dishes for this activity
    """
    model = Dish
    table_class = DishTable
    extra_context = {"title": _('Dishes served during')}
    template_name = 'food/dish_list.html'

    def get_queryset(self):
        return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"])

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        context["activity"] = activity

        context["can_add_dish"] = PermissionBackend.check_perm(self.request, 'food.dish_add')

        return context


class DishDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
    """
    View a dish for this activity
    """
    model = Dish
    extra_context = {"title": _('Details of:')}
    context_oject_name = "dish"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["food"] = self.object.main

        context["supplements"] = self.object.supplements.all()

        context["update"] = PermissionBackend.check_perm(self.request, "food.change_dish")

        context["delete"] = not Order.objects.filter(dish=self.get_object()).exists() and PermissionBackend.check_perm(self.request, "food.delete_dish")

        return context


class DishUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    A view to update a dish
    """
    model = Dish
    form_class = DishForm
    extra_context = {"title": _("Update a dish")}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        form = context['form']
        form.helper = FormHelper()
        # Remove form tag on the generation of the form in the template (already present on the template)
        form.helper.form_tag = False
        # The formset handles the set of the supplements
        form_set = SupplementFormSet(instance=form.instance)
        context['formset'] = form_set
        context['helper'] = SupplementFormSetHelper()

        return context

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        if 'main' in form.fields:
            del form.fields["main"]
        return form

    @transaction.atomic
    def form_valid(self, form):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])

        form.instance.activity = activity

        ret = super().form_valid(form)

        # For each supplement, we save it
        formset = SupplementFormSet(self.request.POST, instance=form.instance)
        saved = []
        if formset.is_valid():
            for f in formset:
                # We don't save the product if the price is not entered, ie. if the line is empty
                if f.is_valid() and f.instance.price:
                    f.save()
                    f.instance.save()
                    saved.append(f.instance.pk)
                else:
                    f.instance = None
        # Remove old supplements that weren't given in the form
        Supplement.objects.filter(~Q(pk__in=saved), dish=form.instance).delete()

        return ret

    def get_success_url(self):
        return reverse_lazy('food:dish_detail', kwargs={"activity_pk": self.kwargs["activity_pk"], "pk": self.kwargs["pk"]})


class DishDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
    """
    Delete a dish with no order yet
    """
    model = Dish
    extra_context = {"title": _('Delete dish')}

    def delete(self, request, *args, **kwargs):
        if Order.objects.filter(dish=self.get_object()).exists():
            raise PermissionDenied(_("This dish cannot be deleted because it has already been ordered"))
        return super().delete(request, *args, **kwargs)

    def get_success_url(self):
        return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]})


class OrderCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Order a meal
    """
    model = Order
    form_class = OrderForm
    extra_context = {"title": _('Order food')}

    def get_sample_object(self):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        sample_order = Order(
            user=self.request.user,
            activity=activity,
            dish=Dish.objects.filter(activity=activity).last(),
        )
        return sample_order

    def get_form(self):
        form = super().get_form()

        form.fields["user"].initial = self.request.user
        form.fields["user"].disabled = True

        return form

    def form_valid(self, form):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])

        form.instance.activity = activity
        supplements = Supplement.objects.filter(pk__in=form.data.getlist('supplements')).all()

        form.instance.save(supplements=supplements)
        return HttpResponseRedirect(self.get_success_url())

    def get_success_url(self):
        return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]})


class OrderListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
    """
    List existing Families
    """
    model = Order
    table_class = OrderTable
    extra_context = {"title": _('Order list')}
    paginate_by = 10

    def get_queryset(self, **kwargs):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        return Order.objects.filter(activity=activity).order_by('number')

    def get_tables(self):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        dishes = Dish.objects.filter(activity=activity)

        tables = [OrderTable] * dishes.count()
        self.tables = tables
        tables = super().get_tables()
        for i in range(dishes.count()):
            tables[i].prefix = dishes[i].main.name
        return tables

    def get_tables_data(self):
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        dishes = Dish.objects.filter(activity=activity)

        tables = []

        for dish in dishes:
            tables.append(self.get_queryset().order_by('ordered_at').filter(
                dish=dish, served=False).filter(
                    PermissionBackend.filter_queryset(self.request, Order, 'view')
            ))

        return tables

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"])

        return context


class ServedOrderListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    View served orders
    """
    model = Order
    template_name = 'food/served_order_list.html'
    table_class = OrderTable

    def get_queryset(self):
        return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"], served=True).order_by('-served_at')

    def get_table(self, **kwargs):
        table = super().get_table(**kwargs)

        table.columns.hide("delete")

        return table

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"])

        return context


class KitchenView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    The view to display useful information for the kitchen
    """
    model = Order
    table_class = OrderTable
    template_name = 'food/kitchen.html'
    extra_context = {'title': _('Kitchen')}

    def get_queryset(self):
        return super().get_queryset().filter(~Q(supplements__isnull=True, request=''), activity__pk=self.kwargs["activity_pk"], served=False)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        orders_count = Order.objects.filter(activity__pk=self.kwargs["activity_pk"], served=False).values('dish__main__name').annotate(quantity=Count('id'))

        context["orders"] = {o['dish__main__name']: o['quantity'] for o in orders_count}

        return context

    def get_table(self, **kwargs):
        table = super().get_table(**kwargs)

        hide = ["ordered_at", "serve", "delete"]
        for field in hide:
            table.columns.hide(field)

        return table


class RecipeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Create a recipe
    """
    model = Recipe
    form_class = RecipeForm
    extra_context = {"title": _("Create a recipe")}

    def get_sample_object(self):
        return Recipe(name='Sample recipe')

    @transaction.atomic
    def form_valid(self, form):
        formset = RecipeIngredientsFormSet(self.request.POST)
        if formset.is_valid():
            ingredients = [f.cleaned_data['name'] for f in formset if f.cleaned_data.get('name')]
            self.object = form.save(commit=False)
            self.object.ingredients = ingredients
            self.object.save()
            return super().form_valid(form)
        else:
            return self.form_invalid(form)

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['form'] = RecipeIngredientsForm()
        context['recipe_form'] = self.get_form()
        if self.request.POST:
            context['formset'] = RecipeIngredientsFormSet(self.request.POST,)
        else:
            context['formset'] = RecipeIngredientsFormSet()
        return context

    def get_success_url(self):
        return reverse_lazy('food:recipe_list')


class RecipeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    List all recipes
    """
    model = Recipe
    table_class = RecipeTable
    extra_context = {"title": _('All recipes')}

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)

        context['can_add_recipe'] = PermissionBackend.check_perm(self.request, 'food.recipe_add')

        return context


class RecipeDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
    """
    List all recipes
    """
    model = Recipe
    extra_context = {"title": _('Details of:')}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["ingredients"] = self.object.ingredients
        context["update"] = PermissionBackend.check_perm(self.request, "food.change_recipe")

        return context


class RecipeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    Create a recipe
    """
    model = Recipe
    form_class = RecipeForm
    extra_context = {"title": _("Create a recipe")}

    def get_sample_object(self):
        return Recipe(name='Sample recipe')

    @transaction.atomic
    def form_valid(self, form):
        formset = RecipeIngredientsFormSet(self.request.POST)
        if formset.is_valid():
            ingredients = [f.cleaned_data['name'] for f in formset if f.cleaned_data.get('name')]
            self.object = form.save(commit=False)
            self.object.ingredients = ingredients
            self.object.save()
            return super().form_valid(form)
        else:
            return self.form_invalid(form)

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['form'] = RecipeIngredientsForm()
        context['recipe_form'] = self.get_form()
        if self.request.POST:
            formset = RecipeIngredientsFormSet(self.request.POST,)
        else:
            formset = RecipeIngredientsFormSet()
        ingredients = self.object.ingredients
        context["ingredients_count"] = len(ingredients)
        formset.extra += len(ingredients)
        context["formset"] = formset
        context["ingredients"] = []
        for ingredient in ingredients:
            context["ingredients"].append({"name": ingredient})
        return context

    def get_success_url(self):
        return reverse_lazy('food:recipe_detail', kwargs={"pk": self.object.pk})


class UseRecipeView(LoginRequiredMixin, UpdateView):
    """
    Add ingredients to a TransformedFood using a Recipe
    """
    model = TransformedFood
    fields = ('ingredients',)
    template_name = 'food/use_recipe_form.html'
    extra_context = {"title": _("Use a recipe for:")}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["form"] = UseRecipeForm()
        return context

    def form_valid(self, form):
        old_ingredients = list(self.object.ingredients.all()).copy()
        old_allergens = list(self.object.allergens.all()).copy()
        old_traces = list(self.object.traces.all()).copy()
        if "ingredients" in form.data:
            ingredients_pk = form.data.getlist("ingredients")
            ingredients = Food.objects.all().filter(pk__in=ingredients_pk)
            for ingredient in ingredients:
                self.object.ingredients.add(ingredient)

        # We recalculate new expiry date and allergens
        self.object.expiry_date = self.object.creation_date + self.object.shelf_life
        self.object.allergens.clear()
        self.object.traces.clear()

        for ingredient in self.object.ingredients.iterator():
            if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
                self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
            self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
            self.object.traces.set(self.object.traces.union(ingredient.traces.all()))

        self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
        return HttpResponseRedirect(self.get_success_url())

    def get_success_url(self):
        return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})


@require_GET
def get_ingredients_for_recipe(request):
    recipe_id = request.GET.get('recipe_id')
    if not recipe_id:
        return JsonResponse({'error': 'Missing recipe_id'}, status=400)

    try:
        recipe = Recipe.objects.get(pk=recipe_id)
    except Recipe.DoesNotExist:
        return JsonResponse({'error': 'Recipe not found'}, status=404)

    # 🔧 Supporte les deux cas : ManyToMany ou simple liste
    ingredients_field = recipe.ingredients

    if hasattr(ingredients_field, "values_list"):
        # Cas ManyToManyField
        ingredient_names = list(ingredients_field.values_list('name', flat=True))
    elif isinstance(ingredients_field, (list, tuple)):
        # Cas liste directe
        ingredient_names = ingredients_field
    else:
        return JsonResponse({'error': 'Unsupported ingredients type'}, status=500)

    # Union des Foods dont le nom commence par un nom d’ingrédient
    query = Q()
    for name in ingredient_names:
        valid_regex = is_regex(name)
        suffix = '__iregex' if valid_regex else '__istartswith'
        prefix = '.*' if valid_regex else ''
        query |= Q(**{f'name{suffix}': prefix + name}, end_of_life='')
    qs = Food.objects.filter(query).distinct()
    qs = qs.filter(PermissionBackend.filter_queryset(request, Food, 'view'))

    data = [{'id': f.id, 'name': f.name, 'qr_code_numbers': ", ".join(str(q.qr_code_number) for q in f.QR_code.all())} for f in qs]
    return JsonResponse({'ingredients': data})
