#
# Copyright 2022
# - Maréchal <thgirod@hotmail.com
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from functools import cached_property
from urllib.parse import unquote

from django.http import HttpRequest
from django.utils.translation import gettext as _
from pydantic import ValidationError

from eboutic.models import get_eboutic_products
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema


class BasketForm:
    """Class intended to perform checks on the request sended to the server when
    the user submits his basket from /eboutic/.

    Because it must check an unknown number of fields, coming from a cookie
    and needing some databases checks to be performed, inheriting from forms.Form
    or using formset would have been likely to end in a big ball of wibbly-wobbly hacky stuff.
    Thus this class is a pure standalone and performs its operations by its own means.
    However, it still tries to share some similarities with a standard django Form.

    Examples:
        ::

            def my_view(request):
                form = BasketForm(request)
                form.clean()
                if form.is_valid():
                    # perform operations
                else:
                    errors = form.get_error_messages()

                    # return the cookie that was in the request, but with all
                    # incorrects elements removed
                    cookie = form.get_cleaned_cookie()

    You can also use a little shortcut by directly calling `form.is_valid()`
    without calling `form.clean()`. In this case, the latter method shall be
    implicitly called.
    """

    def __init__(self, request: HttpRequest):
        self.user = request.user
        self.cookies = request.COOKIES
        self.error_messages = set()
        self.correct_items = []

    def clean(self) -> None:
        """Perform all the checks, but return nothing.
        To know if the form is valid, the `is_valid()` method must be used.

        The form shall be considered as valid if it meets all the following conditions :
            - it contains a "basket_items" key in the cookies of the request given in the constructor
            - this cookie is a list of objects formatted this way : `[{'id': <int>, 'quantity': <int>,
             'name': <str>, 'unit_price': <float>}, ...]`. The order of the fields in each object does not matter
            - all the ids are positive integers
            - all the ids refer to products available in the EBOUTIC
            - all the ids refer to products the user is allowed to buy
            - all the quantities are positive integers
        """
        try:
            basket = PurchaseItemList.validate_json(
                unquote(self.cookies.get("basket_items", "[]"))
            )
        except ValidationError:
            self.error_messages.add(_("The request was badly formatted."))
            return
        if len(basket) == 0:
            self.error_messages.add(_("Your basket is empty."))
            return
        existing_ids = {product.id for product in get_eboutic_products(self.user)}
        for item in basket:
            # check a product with this id does exist
            if item.product_id in existing_ids:
                self.correct_items.append(item)
            else:
                self.error_messages.add(
                    _(
                        "%(name)s : this product does not exist or may no longer be available."
                    )
                    % {"name": item.name}
                )
                continue
        # this function does not return anything.
        # instead, it fills a set containing the collected error messages
        # an empty set means that no error was seen thus everything is ok
        # and the form is valid.
        # a non-empty set means there was at least one error thus
        # the form is invalid

    def is_valid(self) -> bool:
        """Return True if the form is correct else False.

        If the `clean()` method has not been called beforehand, call it.
        """
        if not self.error_messages and not self.correct_items:
            self.clean()
        return not self.error_messages

    @cached_property
    def errors(self) -> list[str]:
        return list(self.error_messages)

    @cached_property
    def cleaned_data(self) -> list[PurchaseItemSchema]:
        return self.correct_items