# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

from odoo import _, api, fields, models
from odoo.fields import Domain
from odoo.exceptions import ValidationError
from odoo.http import request


class ProductProduct(models.Model):
    _inherit = 'product.product'
    _mail_post_access = 'read'

    variant_ribbon_id = fields.Many2one(string="Variant Ribbon", comodel_name='product.ribbon')
    website_id = fields.Many2one(related='product_tmpl_id.website_id', readonly=False)

    product_variant_image_ids = fields.One2many(
        string="Extra Variant Images",
        comodel_name='product.image',
        inverse_name='product_variant_id',
    )

    base_unit_count = fields.Float(
        string="Base Unit Count",
        help="Display base unit price on your eCommerce pages. Set to 0 to hide it for this"
             " product.",
        # Force NUMERIC with unlimited precision, as for `uom.uom.relative_factor`,
        # to support very small ratios, e.g. one unit in a box of 10000.
        digits=0,
        required=True,
        default=1,
    )
    base_unit_id = fields.Many2one(
        string="Custom Unit of Measure",
        help="Define a custom unit to display in the price per unit of measure field.",
        comodel_name='website.base.unit',
    )
    base_unit_price = fields.Monetary(
        string="Price Per Unit",
        compute='_compute_base_unit_price',
    )
    base_unit_name = fields.Char(
        help="Displays the custom unit for the products if defined or the selected unit of measure"
            " otherwise.",
        compute='_compute_base_unit_name',
    )

    website_url = fields.Char(
        string="Website URL",
        help="The full URL to access the document through the website.",
        compute='_compute_product_website_url',
    )

    #=== COMPUTE METHODS ===#

    def _get_base_unit_price(self, price):
        self.ensure_one()
        return self.base_unit_count and price / self.base_unit_count

    @api.depends('lst_price', 'base_unit_count')
    def _compute_base_unit_price(self):
        for product in self:
            if not product.id:
                product.base_unit_price = 0
            else:
                product.base_unit_price = product._get_base_unit_price(product.lst_price)

    @api.depends('uom_name', 'base_unit_id')
    def _compute_base_unit_name(self):
        for product in self:
            product.base_unit_name = product.base_unit_id.name or product.uom_name

    @api.depends_context('lang')
    @api.depends('product_tmpl_id.website_url', 'product_template_attribute_value_ids')
    def _compute_product_website_url(self):
        for product in self:
            url = product.product_tmpl_id.website_url
            if pavs := product.product_template_attribute_value_ids.product_attribute_value_id:
                pav_ids = [str(pav.id) for pav in pavs]
                url = f'{url}?attribute_values={",".join(pav_ids)}'
            product.website_url = url

    #=== CONSTRAINT METHODS ===#

    @api.constrains('base_unit_count')
    def _check_base_unit_count(self):
        if any(product.base_unit_count < 0 for product in self):
            raise ValidationError(_(
                "The value of Base Unit Count must be greater than 0."
                " Use 0 to hide the price per unit on this product."
            ))

    #=== BUSINESS METHODS ===#

    def _prepare_variant_values(self, combination):
        variant_dict = super()._prepare_variant_values(combination)
        variant_dict['base_unit_count'] = self.base_unit_count
        return variant_dict

    def website_publish_button(self):
        self.ensure_one()
        return self.product_tmpl_id.website_publish_button()

    def open_website_url(self):
        self.ensure_one()
        res = self.product_tmpl_id.open_website_url()
        res['url'] = self.website_url
        return res

    def action_unschedule(self):
        """Keep variants aligned with their template scheduling."""
        return self.product_tmpl_id.action_unschedule()

    def _get_images(self):
        """Return a list of records implementing `image.mixin` to
        display on the carousel on the website for this variant.

        This returns a list and not a recordset because the records might be
        from different models (template, variant and image).

        It contains in this order: the main image of the variant (which will fall back on the main
        image of the template, if unset), the Variant Extra Images, and the Template Extra Images.
        """
        self.ensure_one()
        variant_images = list(self.product_variant_image_ids)
        template_images = list(self.product_tmpl_id.product_template_image_ids)
        return [self] + variant_images + template_images

    def _get_combination_info_variant(self, **kwargs):
        """Return the variant info based on its combination.
        See `_get_combination_info` for more information.
        """
        self.ensure_one()
        return self.product_tmpl_id._get_combination_info(
            combination=self.product_template_attribute_value_ids,
            product_id=self.id,
            **kwargs)

    def _website_show_quick_add(self):
        self.ensure_one()
        if not self.filtered_domain(self.env['website']._product_domain()):
            return False
        website = self.env['website'].get_current_website()
        return not (
            website.prevent_sale
            and website._prevent_product_sale(self, not self._get_contextual_price())
        )

    def _is_add_to_cart_allowed(self):
        self.ensure_one()
        if self.env.user.has_group('base.group_system'):
            return True
        if not self.active or not self.website_published:
            return False
        if not self.filtered_domain(self.env['website']._product_domain()):
            return False
        website = self.env['website'].get_current_website()
        if (
            website.prevent_sale
            and website._prevent_product_sale(self, not self._get_contextual_price())
        ):
            return False
        return website.has_ecommerce_access()

    @api.onchange('public_categ_ids')
    def _onchange_public_categ_ids(self):
        if self.public_categ_ids:
            self.website_published = True
        else:
            self.website_published = False

    def _to_markup_data(self, website):
        """ Generate JSON-LD markup data for the current product.

        :param website website: The current website.
        :return: The JSON-LD markup data.
        :rtype: dict
        """
        self.ensure_one()

        product_price = request.pricelist._get_product_price(
            self, quantity=1, currency=website.currency_id
        )
        # Use sudo to access cross-company taxes.
        product_taxes_sudo = self.sudo().taxes_id._filter_taxes_by_company(self.env.company)
        taxes = request.fiscal_position.map_tax(product_taxes_sudo)
        price = self.product_tmpl_id._apply_taxes_to_price(
            product_price, website.currency_id, product_taxes_sudo, taxes, self, website=website
        )

        base_url = website.get_base_url()
        markup_data = {
            '@context': 'https://schema.org',
            '@type': 'Product',
            'name': self.with_context(display_default_code=False).display_name,
            'url': f'{base_url}{self.website_url}',
            'image': f'{base_url}{website.image_url(self, "image_1920")}',
            'offers': {
                '@type': 'Offer',
                'price': price,
                'priceCurrency': website.currency_id.name,
            },
        }
        if self.website_meta_description or self.description_sale:
            markup_data['description'] = self.website_meta_description or self.description_sale
        if self.barcode:
            markup_data['gtin'] = self.barcode
        return markup_data

    def _get_image_1920_url(self):
        """ Returns the local url of the product main image.

        Note: self.ensure_one()

        :rtype: str
        """
        self.ensure_one()
        return self.env['website'].image_url(self, 'image_1920')

    def _get_extra_image_1920_urls(self):
        """ Returns the local url of the product additional images, no videos. This includes the
        variant specific images first and then the template images.

        Note: self.ensure_one()

        :rtype: list[str]
        """
        self.ensure_one()
        return [
            self.env['website'].image_url(extra_image, 'image_1920')
            for extra_image in self.product_variant_image_ids + self.product_template_image_ids
            if extra_image.image_128  # only images, no video urls
        ]

    def write(self, vals):
        if 'active' in vals and not vals['active']:
            # unlink draft lines containing the archived product
            self.env['sale.order.line'].sudo().search([
                ('state', '=', 'draft'),
                ('product_id', 'in', self.ids),
                ('order_id', 'any', [('website_id', '!=', False)]),
            ]).unlink()
        return super().write(vals)

    def _is_in_wishlist(self):
        if not self:
            return False
        self.ensure_one()
        return self in self.env['product.wishlist'].current().mapped('product_id')

    def _prepare_categories_for_display(self):
        """On the comparison page group on the same line the values of each
        product that concern the same attributes, and then group those
        attributes per category.

        The returned categories are ordered following their default order.

        :return: OrderedDict [{
            product.attribute.category: OrderedDict [{
                product.attribute: OrderedDict [{
                    product: [product.template.attribute.value]
                }]
            }]
        }]
        """
        attributes = self.product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id.sorted()
        categories = OrderedDict([(cat, OrderedDict()) for cat in attributes.category_id.sorted()])
        if any(not pa.category_id for pa in attributes):
            # category_id is not required and the mapped does not return empty
            categories[self.env['product.attribute.category']] = OrderedDict()
        for pa in attributes:
            categories[pa.category_id][pa] = OrderedDict([(
                product,
                product.product_template_attribute_value_ids.filtered(
                    lambda ptav: ptav.attribute_id == pa
                ) or  # If no_variant, show all possible values
                product.attribute_line_ids.filtered(lambda ptal: ptal.attribute_id == pa).value_ids
            ) for product in self])
        return categories

    def _get_image_1024_url(self):
        """ Returns the local url of the product main image.
        Note: self.ensure_one()
        :rtype: str
        """
        self.ensure_one()
        return self.env['website'].image_url(self, 'image_1024')

    def _mail_get_operation_for_mail_message_operation(self, message_operation):
        if (
            message_operation == 'create'
            and not self.env.user._is_internal()
            and not self.env['website'].is_view_active('website_sale.product_comment')
        ):
            return [(Domain.TRUE, 'write')]
        return super()._mail_get_operation_for_mail_message_operation(message_operation)
