# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)

# License OPL-1 (https://www.odoo.com/documentation/19.0/legal/licenses.html).

from typing import Dict, List

from odoo import _, _lt, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_compare
class PrintProductLabelSection(models.Model):
    _name = "print.product.label.section"
    _description = 'Template Sections of Product Labels'
    _order = 'sequence'

    def _default_sequence(self):
        return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1

    sequence = fields.Integer(default=_default_sequence)
    template_id = fields.Many2one(
        comodel_name='print.product.label.template',
        string='Template',
        ondelete='cascade',
        required=True,
    )
    template_preview_html = fields.Html(compute='_compute_template_preview_html')
    preview = fields.Boolean(default=True, help='Show the sample label.')
    type = fields.Selection(
        selection=[
            ('text', 'Text'),
            ('field', 'Model Field'),
            ('price', 'Price'),
            ('promo_price', 'Promo Price'),
            ('multi_price', 'Multiple prices (by quantity)'),
            ('product_attributes', 'Product Attributes'),
            ('image', 'Image'),
        ],
        default='text',
        required=True,
    )
    value = fields.Char()
    value_format = fields.Char(string='Format', help='Format for date and digit fields.')
    value_prefix = fields.Char()
    value_suffix = fields.Char()
    image = fields.Binary(attachment=True)
    field_id = fields.Many2one(
        comodel_name='ir.model.fields',
        string='Field',
        ondelete='cascade',
        domain="[('id', 'in', field_ids)]",
    )
    field_name = fields.Char(related='field_id.name')
    field_ids = fields.Many2many(
        comodel_name='ir.model.fields',
        string='Available Fields',
        help='Technical field for a domain',
        compute='_compute_field_ids',
    )
    field_ttype = fields.Selection(related='field_id.ttype')
    relation_model_id = fields.Many2one(
        comodel_name='ir.model',
        compute='_compute_relation_model_id',
    )
    relation_field_id = fields.Many2one(
        comodel_name='ir.model.fields',
        help='The first level of the relation field. Allow you to select related fields'
             ' in case when you have chosen the "Field" with the type "many2one".',
    )
    relation_field_ttype = fields.Selection(
        related='relation_field_id.ttype',
        string="Relation Field Type",
    )
    nested_relation_model_id = fields.Many2one(
        comodel_name='ir.model',
        compute='_compute_nested_relation_model_id',
    )
    nested_relation_field_id = fields.Many2one(
        comodel_name='ir.model.fields',
        help='The second level of relation field. Allow you to select related fields in '
             'case when you have chosen the "Relation Field" with the type "many2one".',
    )
    height = fields.Float(
        string='Height, mm', digits=(10, 2), help='Section Height in mm.',
    )
    width = fields.Float(digits=(10, 2), help='Section Width.')
    width_measure = fields.Selection(
        selection=[
            ('%', 'Percents'),
            ('mm', 'mm'),
        ],
        default='%',
        required=True,
    )
    position = fields.Selection(
        selection=[
            ('none', 'Full Width'),
            ('left', 'Left Side'),
            ('right', 'Right Side'),
        ],
        string='Float',
        default='none',
        required=True,
    )
    line_height = fields.Float(digits=(10, 2), default=1.0, help='Section Line Height Ratio.')
    align = fields.Selection(
        selection=[
            ('left', 'Left'),
            ('center', 'Center'),
            ('right', 'Right'),
            ('justify', 'Justify'),
        ],
        default='center',
        required=True,
    )
    font_name = fields.Selection(
        selection=[
            ('Lato', 'Lato'),
            ('Roboto', 'Roboto'),
            ('Open_Sans', 'Open Sans'),
            ('Montserrat', 'Montserrat'),
            ('Oswald', 'Oswald'),
            ('Raleway', 'Raleway'),
            ('Tajawal', 'Tajawal'),
            ('Fira_Mono', 'Fira Mono'),
        ],
        string='Font',
        help='Specify the name of a custom font for this label section.',
    )
    font_size = fields.Float(digits=(10, 2), default=12, required=True)
    font_size_measure = fields.Selection(
        selection=[('px', 'Pixels'), ('mm', 'mm')],
        string='Measure',
        default='px',
        required=True,
    )
    font_weight = fields.Selection(
        selection=[
            ('100', '100'),
            ('normal', 'normal'),
            ('bold', 'bold'),
            ('900', '900'),
        ],
        default='normal',
        required=True,
    )
    letter_spacing = fields.Float(digits=(10, 2), help='Space between letters, in mm.')
    text_decoration = fields.Selection(
        selection=[
            ('line-through', 'Line Through'),
            ('none', 'None'),
        ],
        default='none',
    )
    text_color = fields.Char(default='#000000')
    widget = fields.Selection(
        selection=[
            ('price', 'Price'),
            ('barcode', 'Barcode'),
            ('qr_code', 'QR code'),
            ('image', 'Image'),
            ('attribute_list', 'Attribute Values'),
        ],
    )
    with_product_attribute_name = fields.Boolean(
        string='With Attribute Name',
        help='Place a product attribute name before the attribute value on labels.',
    )
    attribute_ids = fields.Many2many(
        comodel_name='product.attribute',
        string='Allowed Attributes',
        help='Specify product attributes to show. '
             'If no one is selected, all attribute that are related to a product will be shown.'
    )
    barcode_is_humanreadable = fields.Boolean(string='Human-readable')
    barcode_symbology = fields.Selection(
        selection=[
            ('auto', 'Auto'),
            ('EAN8', 'EAN8'),
            ('EAN13', 'EAN13'),
            ('Code128', 'Code128'),
        ],
        string='Symbology',
        default='auto',
        required=True,
    )
    shorten_url = fields.Boolean(
        string='Shorten URL',
        help='If the section value is a URL, you can short it with the Odoo internal '
             'link tracker to get a link like this one: https://your-domain/r/aBc.',
    )
    make_url_absolute = fields.Boolean(
        string='Absolute URL',
        help="If the section value is a URL, and it's a relative, you can make it "
             "absolute and add your base domain by activating this option.",
    )
    currency_position = fields.Selection(
        selection=[
            ('default', 'By default'),
            ('before', 'Before price'),
            ('after', 'After price'),
            ('none', 'Without currency code'),
        ],
        default='default',
        required=True,
    )
    multi_price_limit = fields.Integer(
        help='Specify the limit to restrict a number of prices.',
        default=10,
    )
    multi_price_order = fields.Selection(
        selection=[
            ('desc', 'In descending order'),
            ('asc', 'In ascending order'),
        ],
        string='Price Order',
        default='desc',
        required=True,
    )
    padding_top = fields.Float(digits=(10, 2), help='Page Right Padding, in mm.')
    padding_bottom = fields.Float(digits=(10, 2), help='Page Bottom Padding, in mm.')
    padding_left = fields.Float(digits=(10, 2), help='Page Left Padding, in mm.')
    padding_right = fields.Float(digits=(10, 2), help='Page Right Padding, in mm.')
    margin_top = fields.Float(digits=(10, 2), help='Page Right Margin, in mm.')
    margin_bottom = fields.Float(digits=(10, 2), help='Page Bottom Margin, in mm.')
    margin_left = fields.Float(digits=(10, 2), help='Page Left Margin, in mm.')
    margin_right = fields.Float(digits=(10, 2), help='Page Right Margin, in mm.')
    with_border_top = fields.Boolean(string="Border Top")
    with_border_bottom = fields.Boolean(string="Border Bottom")
    with_border_left = fields.Boolean(string="Border Left")
    with_border_right = fields.Boolean(string="Border Right")
    border_width = fields.Integer(default=1, help='Border Width, in px')
    with_background = fields.Boolean(string="Background")
    background_color = fields.Char(default='#BBBBBB')
    active = fields.Boolean(default=True)

    @api.constrains('height')
    def _check_height(self):
        for section in self:
            if not section.height:
                raise ValidationError(_('The section height must be set.'))

    @api.constrains('type', 'widget')
    def _check_widget_image(self):
        for section in self:
            if section.type != 'field' and section.widget == 'image':
                raise ValidationError(_('You can use the widget "Image" only for the "Model Fields" section types.'))

    @api.depends('type')
    def _compute_field_ids(self):
        for section in self:
            if section.type == 'field':
                domain = [('model', '=', 'print.product.label.line')] + self._get_field_domain()
                available_fields = self.env['ir.model.fields'].search(domain)
                section.field_ids = [(6, 0, available_fields.ids)]
            else:
                section.field_ids = None

    @api.depends('type', 'field_id')
    def _compute_relation_model_id(self):
        for section in self:
            if section.type == 'field' and section.field_id.ttype in self.relation_field_types():
                section.relation_model_id = self.env['ir.model'].search([
                    ('model', '=', section.field_id.relation)
                ])[:1].id
            else:
                section.relation_model_id = None

    @api.depends('type', 'field_id', 'relation_field_id')
    def _compute_nested_relation_model_id(self):
        for section in self:
            if (
                    section.type == 'field'
                    and section.field_id
                    and section.field_id.ttype in self.relation_field_types()
                    and section.relation_field_id
                    and section.relation_field_id.ttype in self.relation_field_types()
            ):
                section.nested_relation_model_id = self.env['ir.model'].search([
                    ('model', '=', section.relation_field_id.relation),
                ])[:1].id
            else:
                section.nested_relation_model_id = None

    @api.depends('template_id')
    def _compute_template_preview_html(self):
        for section in self:
            # flake8: noqa: E501
            section.template_preview_html = section.template_id.with_context(editable_section_id=section.id).preview_html

    @api.onchange('type')
    def _onchange_type(self):
        for section in self:
            if section.type != 'field':
                section.field_id = section.relation_field_id = section.nested_relation_field_id = False

    @api.onchange('field_id')
    def _onchange_field_id(self):
        for section in self:
            if section.relation_field_id.model_id != section.relation_model_id:
                section.relation_field_id = section.nested_relation_field_id = False

    @api.onchange('relation_field_id')
    def _onchange_relation_field_id(self):
        for section in self:
            if section.nested_relation_field_id.model_id != section.nested_relation_model_id:
                section.nested_relation_field_id = False

    @api.onchange('type', 'field_id', 'widget')
    def _onchange_widget(self):
        for section in self:
            # Reset the "Price" widget
            if section.widget == 'price' and section.field_id != self.env.ref(
                    'fusion_labels_pro.field_print_product_label_line__price'):
                section.widget = False
            # Reset the value format
            section.value_format = False

    @api.model
    def binary_field_types(self):
        return ['binary']

    @api.model
    def text_field_types(self):
        return ['char', 'text', 'html', 'selection']

    @api.model
    def digit_field_types(self):
        return ['float', 'monetary', 'integer']

    @api.model
    def date_field_types(self):
        return ['date', 'datetime']

    @api.model
    def non_relation_field_types(self):
        return self.binary_field_types() + self.text_field_types() \
            + self.digit_field_types() + self.date_field_types()

    @api.model
    def relation_field_types(self):
        return ['many2one']

    @api.model
    def multi_relation_field_types(self):
        return ['many2many', 'one2many']

    @api.model
    def _get_field_domain(self):
        return [('ttype', 'in', self.non_relation_field_types() + self.relation_field_types())]  # flake8: noqa: E501

    def get_float_position(self):
        self.ensure_one()
        width = 100 if self.width_measure == '%' and self.width > 100 else self.width
        return "width: %(width).2f%(measure)s; float: %(float)s;" % {
            'width': width,
            'measure': self.width_measure,
            'float': self.position,
        }

    def get_barcode_size(self) -> Dict[str, int]:
        self.ensure_one()
        # flake8: noqa: E501
        multiplier = int(self.env['ir.config_parameter'].sudo().get_param('fusion_labels_pro.barcode_multiplier', 1))
        return {'width': 600 * multiplier, 'height': 100 * multiplier}

    def get_border_style(self):
        self.ensure_one()
        border_style = ''
        for side in ['top', 'bottom', 'left', 'right']:
            if self['with_border_%s' % side]:
                border_style += 'border-%(side)s: %(width)dpx solid #000; ' % {
                    'side': side,
                    'width': self.border_width,
                }
        return border_style

    def get_background_style(self):
        self.ensure_one()
        bg_style = ''
        if self.with_background and self.background_color:
            bg_style += f'background-color: {self.background_color}; '
        return bg_style

    def get_font_name(self) -> str:
        self.ensure_one()
        return self.font_name.replace('_', ' ') if self.font_name else ''

    def get_attribute_data(self, label=None) -> List[Dict]:
        self.ensure_one()
        section = self
        attribute_data = []
        if label:
            product = label.product_id
            for attribute in product.product_tmpl_id.attribute_line_ids.mapped('attribute_id'):
                # Process only allowed attributes
                if section.attribute_ids and attribute not in section.attribute_ids:
                    continue

                attr_vals = {'name': attribute.name, 'values': []}
                attribute_values = product.product_tmpl_id.attribute_line_ids.value_ids.filtered(
                    lambda av: av.attribute_id == attribute
                )
                for val in attribute_values:
                    value_data = {'name': val.name, 'active': False}
                    if val in product.product_template_attribute_value_ids.mapped('product_attribute_value_id'):
                        value_data['active'] = True
                    attr_vals['values'].append(value_data)
                attribute_data.append(attr_vals)
        return attribute_data

    def get_html_style(self, label=None) -> str:
        self.ensure_one()
        style = "overflow: hidden; " \
                "height: %(height).2fmm; " \
                "padding: %(padding_top).2fmm %(padding_right).2fmm" \
                " %(padding_bottom).2fmm %(padding_left).2fmm; " \
                "margin: %(margin_top).2fmm %(margin_right).2fmm" \
                " %(margin_bottom).2fmm %(margin_left).2fmm; " \
                "text-align: %(align)s; " \
                "font-size: %(font_size)s; " \
                "color: %(text_color)s; " \
                "line-height: %(line_height).2f; " \
                "letter-spacing: %(letter_spacing).2fmm; " \
                "font-weight: %(font_weight)s; "\
                "text-decoration: %(text_decoration)s;" % {
                    'height': self.height,
                    'align': self.align,
                    'font_size': '%.2f%s' % (self.font_size, self.font_size_measure),
                    'text_color': self.text_color,
                    'line_height': self.line_height,
                    'letter_spacing': self.letter_spacing,
                    'font_weight': self.font_weight,
                    'padding_top': self.padding_top,
                    'padding_right': self.padding_right,
                    'padding_bottom': self.padding_bottom,
                    'padding_left': self.padding_left,
                    'margin_top': self.margin_top,
                    'margin_right': self.margin_right,
                    'margin_bottom': self.margin_bottom,
                    'margin_left': self.margin_left,
                    'text_decoration': self.text_decoration or 'none',
                }
        # Section width settings
        style += "clear: both;" if self.position == 'none' else self.get_float_position()
        # Section borders
        style += self.get_border_style()
        # Section background
        style += self.get_background_style()
        # Hide the crossed regular price section if a price and the promo price are equal
        # pylint: disable-msg=too-many-boolean-expressions
        if ((self.type == 'price' or self.type == 'field' and self.field_name == 'price')
                and 'promo_price' in self.template_id.section_ids.mapped('type')
                and label and float_compare(label.price, label.promo_price, precision_digits=2) == 0
        ):
            style += "opacity: 0;"
        # Section font family
        if self.font_name:
            style += f'font-family: "{self.get_font_name()}";'
        return style

    def _format_digit_value(self, value) -> str:
        self.ensure_one()
        try:
            digit_value = ('%s' % (self.value_format or '%s')) % value
        except (ValueError, TypeError):
            digit_value = 'ERROR! Check the format'
        return digit_value

    def _get_field_value(self, record, field) -> str:
        """Return a value of the "field" for the "record"."""
        self.ensure_one()
        section = self

        # Retrieving a value
        if field.ttype == 'selection':
            selection = record.fields_get([field.name])[field.name].get('selection', [])
            vals = dict(selection)
            value = vals.get(record[field.name])
        else:
            value = record[field.name]

        # Format empty values
        value = value if value is not False else ''

        # Format values for the digit and date fields
        if value and not section.widget:

            # Format values for the digit fields
            if field.ttype in self.digit_field_types():
                value = section._format_digit_value(value)

            # Format values for the date fields
            elif field.ttype in ['date', 'datetime']:
                lang = self.env['res.lang']._lang_get(self.env.lang) or self.env.ref('base.lang_en')
                value = value.strftime(section.value_format or lang.date_format)

        return value

    @api.model
    def process_price_value(self, label, price: float, currency=None) -> float:
        """
        Post-processing of the price value before converting to the string.
        Method to override.
        """
        return price

    def _get_price_value(self, label, pricelist=False, min_quantity=1.0) -> str:
        self.ensure_one()
        section = self
        product = label.product_id
        if pricelist:
            price = pricelist._get_product_price(product, min_quantity)
        else:
            price = label.price
        currency = pricelist.currency_id if pricelist else label.currency_id

        # Post-processing before converting to the string
        value = self.process_price_value(label, price, currency=currency)

        # Converting to the string
        value = section._format_digit_value(value)

        if section.currency_position != 'none':
            # flake8: noqa: E501
            before = section.currency_position == 'before' or section.currency_position == 'default' and currency.position == 'before'
            if before:
                value = '%s %s' % (currency.symbol, value)
            else:
                value = '%s %s' % (value, currency.symbol)
        return value

    @api.model
    def get_pricelist_items(self, product, pricelist, sort_reverse=False):
        """Collect all pricelist rules that affect the current product."""
        price_rules = pricelist.item_ids.filtered(
            lambda l: l.product_id == product and l.min_quantity
        )
        price_rules |= pricelist.item_ids.filtered(
            lambda l: l.product_tmpl_id == product.product_tmpl_id
            and not l.product_id and l.min_quantity
        )
        price_rules |= self.env['product.pricelist.item'].search([
            ('pricelist_id', '=', pricelist.id),
            ('categ_id', 'parent_of', product.categ_id.id),
            ('min_quantity', '!=', 0),
        ])
        # Remove rules with duplicated min quantity values
        res = self.env['product.pricelist.item'].browse()
        for rule in price_rules:
            if rule.min_quantity not in res.mapped('min_quantity'):
                res += rule
        return res.sorted('min_quantity', reverse=sort_reverse)

    @api.model
    def get_short_url(self, url: str, title: str) -> str:
        # Search a short link, if it exists
        link = self.env['link.tracker'].search([('url', '=', url if url.startswith('http') else f'http://{url}')])
        if not link:
            # Create a new short link, if it does not exist
            link = self.env['link.tracker'].sudo().create({
                'url': url,
                'title': title,
            })
        return link.short_url

    def get_value(self, label):
        """Return value for a section depending on label.
        :param label: record of "print.product.label.line" model
        :return: str
        """
        # flake8: noqa: E501
        # pylint: disable=too-many-branches
        self.ensure_one()
        section = self.sudo()  # Add sudo to allow users without "Administration/Access Rights" generate labels
        value = ''
        allowed_text_field_types = self.text_field_types() + self.digit_field_types() + self.date_field_types()
        allowed_relation_field_types = self.relation_field_types()

        # There are three relation levels:
        # Level 0 - the product label level
        # Level 1 - the level of relation field of the product label model
        # Level 2 - the level of relation field of the relation field

        if section.type == 'text':
            value = section.value or ''

        elif section.type == 'price':
            value = section._get_price_value(label, label.wizard_id.pricelist_id)

        elif section.type == 'promo_price' and label.wizard_id.sale_pricelist_id:
            value = section._get_price_value(label, label.wizard_id.sale_pricelist_id)

        elif section.type == 'multi_price' and label.wizard_id.pricelist_id:
            pricelist = label.wizard_id.pricelist_id
            qty_prices = []
            for pl_rule in self.get_pricelist_items(
                    label.product_id, pricelist,
                    sort_reverse=section.multi_price_order == 'asc',
            )[:section.multi_price_limit]:
                qty_prices.append({
                    'qty': pl_rule.min_quantity,
                    'amount': section._get_price_value(label, label.wizard_id.pricelist_id, min_quantity=pl_rule.min_quantity),
                    'currency': pricelist.currency_id.symbol,
                })
            value = qty_prices

        # pylint: disable=too-many-nested-blocks
        elif section.type == 'field' and section.field_id:

            # Level 0
            # Text and digit fields
            if section.field_ttype in allowed_text_field_types:
                value = section._get_field_value(label, section.field_id)
            # Relation fields
            elif section.relation_field_id and section.field_id.ttype in allowed_relation_field_types:

                # Level 1
                record = label[section.field_id.name]
                if record:
                    # Text and digit fields of the relation field
                    if section.relation_field_ttype in allowed_text_field_types:
                        value = section._get_field_value(record, section.relation_field_id)

                    # Nested relation fields of the relation field
                    elif section.relation_field_ttype in allowed_relation_field_types:

                        # Level 2
                        nested_record = record[section.relation_field_id.name]
                        if nested_record:
                            # Text and digit fields of the relation field
                            if section.nested_relation_field_id.ttype in allowed_text_field_types:
                                value = section._get_field_value(
                                    nested_record, section.nested_relation_field_id
                                )

        # Post-processing

        # Prefix and Suffix
        if value and section.value_prefix:
            value = f"{section.value_prefix}{value}"
        if value and section.value_suffix:
            value = f"{value}{section.value_suffix}"

        # Make a URL an absolute, if it's relative
        if value and section.make_url_absolute and not value.startswith('http'):
            value = f"{section.get_base_url().rstrip('/')}{value}"

        if value and section.shorten_url and not self._context.get('preview_mode'):
            # Generate a shorten URL
            value = self.get_short_url(value, "%s - %s" % (section.template_id.name, section.display_name))

        return value

    def get_image_url(self, label) -> str:
        """
        Return URL for a section binary field depending on label.
        :param label: record of "print.product.label.line" model
        :return: str
        """
        self.ensure_one()
        section = self.sudo()  # Add sudo to allow users without "Administration/Access Rights" generate labels
        res = ''
        if (
                section.type != 'field'
                or section.relation_field_ttype != 'many2one' and (
                    not section.relation_model_id
                    or not section.relation_field_id or section.relation_field_id.ttype != 'binary'
                )
                # Nested fields
                or section.relation_field_ttype == 'many2one' and (
                    not section.nested_relation_model_id
                    or not section.nested_relation_field_id or section.nested_relation_field_id.ttype != 'binary'
                )
        ):
            return res

        model_name = section.relation_model_id.model
        field_name = section.field_id.name
        record = label[field_name]
        relation_field_name = section.relation_field_id.name

        # Nested fields
        if section.relation_field_ttype == 'many2one' and section.nested_relation_field_id.ttype == 'binary':
            model_name = section.nested_relation_field_id.model
            record = label[field_name][section.relation_field_id.name]
            relation_field_name = section.nested_relation_field_id.name

        if record:
            res = '/web/image/%s/%d/%s' % (model_name, record.id, relation_field_name)
        return res

    @api.depends('type', 'value', 'field_id', 'field_id.field_description',
                 'relation_field_id', 'relation_field_id.field_description', 'widget')
    def _compute_display_name(self):
        for section in self:
            section.display_name = "%s%s" % (
                section.type == 'text'
                and (section.value and f"{_lt('Text:')} {section.value}" or _lt('Blank'))
                or section.type == 'price' and _lt('Price')
                or section.type == 'promo_price' and _lt('Promo Price')
                or section.type == 'multi_price' and _lt('Multiple prices (by qty)')
                or section.type == 'product_attributes' and (_('Product Attributes%s') % (
                    f" [{', '.join(attr.name for attr in section.attribute_ids)}]" if section.attribute_ids else ''
                ))
                or section.type == 'image' and _lt('Image')
                or section.type == 'field'
                and "%s %s" % (
                    section.field_id.field_description,
                    section.relation_field_id
                    and section.relation_field_id.field_description or '',
                ),
                section.widget and (" (widget: %s)" % section.widget) or '',
            )

    def action_pdf_preview(self):
        self.ensure_one()
        return self.template_id.action_pdf_preview()
