# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS

NOPACKAGE = "nopackage"
SINGLEPACKAGE = "singlepackage"
MULTIPACKAGE = "multiplepackage"


class StockLocation(models.Model):
    _inherit = "stock.location"

    package_restriction = fields.Selection(
        selection=lambda self: self._selection_package_restriction(),
        help="""
            Control if the location can contain products not in a package.

            Options:
              * False (not set): No restriction, the location can contain products
                with and without package
              * Forbidden: The location cannot have products part of a package
              * Mandatory and unique: The location cannot have products not
                part of a package and you cannot have more than 1 package on
                the location
              * Mandatory and not unique:  The location cannot have products
                not part of a package and you may store multiple packages
                on the location
        """,
    )

    @api.model
    def _selection_package_restriction(self):
        return [
            (NOPACKAGE, "Forbidden"),
            (MULTIPACKAGE, "Mandatory"),
            (SINGLEPACKAGE, "Mandatory and unique"),
        ]

    has_package_restriction_violation = fields.Boolean(
        compute="_compute_package_restriction_violation",
        search="_search_has_package_restriction_violation",
    )

    package_restriction_violation_message = fields.Char(
        compute="_compute_package_restriction_violation",
    )

    def _has_package_restriction_violation_query(self):
        self.flush_model()
        self.quant_ids.flush_model()
        query = """
            SELECT stock_quant.location_id,
            (count(distinct(stock_quant.package_id)) > 1)::bool as has_multiple_packages,
            (count(*) FILTER (WHERE stock_quant.package_id IS NULL) > 0)::bool as has_no_package
            FROM stock_quant
            JOIN stock_location ON stock_location.id = stock_quant.location_id
            WHERE
                quantity != 0
                AND stock_location.package_restriction IS NOT NULL
            """
        if self:
            query += "AND stock_quant.location_id in %s"
        query += f"""
            GROUP BY
                stock_quant.location_id, stock_location.package_restriction
            HAVING
                (
                    stock_location.package_restriction = '{NOPACKAGE}'
                    AND count(distinct(stock_quant.package_id)) > 0
                ) OR (
                    stock_location.package_restriction = '{SINGLEPACKAGE}'
                    AND (
                        count(distinct(stock_quant.package_id)) > 1
                        OR count(*) FILTER (WHERE stock_quant.package_id IS NULL) > 0
                    )
                ) OR (
                    stock_location.package_restriction = '{MULTIPACKAGE}'
                    AND count(*) FILTER (WHERE stock_quant.package_id IS NULL) > 0
                )
        """
        return query

    @api.depends("package_restriction")
    def _compute_package_restriction_violation(self):
        errors = {}
        if self.ids:
            self.env.cr.execute(
                self._has_package_restriction_violation_query(), (tuple(self.ids),)
            )
            errors = {r[0]: r[1:] for r in self.env.cr.fetchall()}
        for location in self:
            error = errors.get(location.id)
            if not error:
                location.has_package_restriction_violation = False
                location.package_restriction_violation_message = False
                continue
            location.has_package_restriction_violation = True
            if location.package_restriction == NOPACKAGE:
                location.package_restriction_violation_message = _(
                    "This location should only contain items without package "
                    "but it contains the package(s): {packages}"
                ).format(
                    packages=", ".join(location.quant_ids.package_id.mapped("name"))
                )
            else:
                messages = []
                has_multiple_package, has_no_package = error
                if has_multiple_package:
                    messages.append(
                        _(
                            "This location should only contain a single package "
                            "but it contains the package(s): {packages}"
                        ).format(
                            packages=", ".join(
                                location.quant_ids.package_id.mapped("name")
                            )
                        )
                    )
                if has_no_package:
                    products = location.quant_ids.filtered(
                        lambda q: not q.package_id
                    ).product_id
                    messages.append(
                        _(
                            "This location should only contain items in a package "
                            "but it contains the items of product(s): {products}"
                        ).format(products=", ".join(products.mapped("name")))
                    )
                location.package_restriction_violation_message = "\n".join(messages)

    def _search_has_package_restriction_violation(self, operator, value):
        search_has_violation = (
            # has_restriction_violation != False
            (operator in NEGATIVE_TERM_OPERATORS and not value)
            # has_restriction_violation = True
            or (operator not in NEGATIVE_TERM_OPERATORS and value)
        )
        self.env.cr.execute(self._has_package_restriction_violation_query())
        error_ids = [r[0] for r in self.env.cr.fetchall()]
        if search_has_violation:
            op = "in"
        else:
            op = "not in"
        return [("id", op, error_ids)]

    def _check_package_restriction(self, move_lines=None):
        """Check if the location respect the package restrictions

        :param move_lines: Optional planned move_line to validate its destination location
        :raises ValidationError: if the restriction is not respected
        """
        error_msgs = []
        move_lines = move_lines or self.env["stock.move.line"]
        for location in self:
            if not location.package_restriction:
                continue
            invalid_products = False
            if location.package_restriction == NOPACKAGE:
                if move_lines:
                    move_lines_with_package = move_lines.filtered("result_package_id")
                    if move_lines_with_package:
                        invalid_products = move_lines_with_package.product_id
                else:
                    quants_with_package = location.quant_ids.filtered(
                        lambda q: q.package_id and q.quantity
                    )
                    if quants_with_package:
                        invalid_products = quants_with_package.product_id
                if invalid_products:
                    error_msgs.append(
                        _(
                            "A package is not allowed on the location {location}."
                            "You cannot move the product(s) {product} with a package."
                        ).format(
                            location=location.display_name,
                            product=", ".join(invalid_products.mapped("display_name")),
                        )
                    )
                continue
            # SINGLE or MULTI
            if move_lines:
                move_lines_without_package = move_lines.filtered(
                    lambda ml: not ml.result_package_id
                )
                if move_lines_without_package:
                    invalid_products = move_lines_without_package.product_id
            else:
                quants_without_package = location.quant_ids.filtered(
                    lambda q: not q.package_id and q.quantity
                )
                if quants_without_package:
                    invalid_products = quants_without_package.product_id
            if invalid_products:
                error_msgs.append(
                    _(
                        "A package is mandatory on the location {location}.\n"
                        "You cannot move the product(s) {product} without a package."
                    ).format(
                        location=location.display_name,
                        product=", ".join(invalid_products.mapped("display_name")),
                    )
                )
                continue
            if location.package_restriction == MULTIPACKAGE:
                continue
            # SINGLE
            packages = (
                move_lines.result_package_id
                | location.quant_ids.filtered("quantity").package_id
            )
            if len(packages) > 1:
                error_msgs.append(
                    _("Only one package is allowed on the location {location}.").format(
                        location=location.display_name
                    )
                )
        if error_msgs:
            raise ValidationError("\n".join(error_msgs))
