# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging

from odoo import Command, models, fields, api, _
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)


class PosOrder(models.Model):
    _inherit = "pos.order"

    table_stand_number = fields.Char(string="Table Stand Number")
    self_ordering_table_id = fields.Many2one('restaurant.table', string='Table reference', readonly=True)
    source = fields.Selection(selection_add=[
        ('mobile', 'Self-Order Mobile'),
        ('kiosk', 'Self-Order Kiosk')
    ])

    def write(self, vals):
        if 'table_id' in vals and vals['table_id'] and self.self_ordering_table_id:
            # Clear stale self-order table link when the order is transferred to a new table.
            vals['self_ordering_table_id'] = vals['table_id']
        return super().write(vals)

    @api.model
    def _load_pos_self_data_domain(self, data, config):
        return [('id', '=', False)]

    @api.model
    def remove_from_ui(self, server_ids):
        order_ids = self.env['pos.order'].browse(server_ids)
        order_ids.state = 'cancel'
        self._send_notification(order_ids)
        return super().remove_from_ui(server_ids)

    @api.model
    def sync_from_ui(self, orders):
        result = super().sync_from_ui(orders)
        order_ids = self.browse([order['id'] for order in result['pos.order'] if order.get('id')])
        self._send_notification(order_ids)
        return result

    def cancel_order_from_pos(self):
        orders = super().cancel_order_from_pos()
        success_orders_ids = [o['id'] for o in orders['pos.order'] if o['state'] == 'cancel']
        orders_ids = self.browse(success_orders_ids).exists()
        self._send_notification(orders_ids)
        return orders

    def action_pos_order_cancel(self):
        super().action_pos_order_cancel()
        self._send_notification(self)

    def _send_notification(self, order_ids):
        config_ids = order_ids.config_id
        for config in config_ids:
            config.notify_synchronisation(config.current_session_id.id, self.env.context.get('device_identifier', 0))
            config._notify('ORDER_STATE_CHANGED', {})

    def _send_self_order_receipt(self):
        self.ensure_one()
        if (
            self.state not in ('paid', 'done')
            or not self.email
            or not self.preset_id.mail_template_id
        ):
            return
        try:
            self.action_send_self_order_receipt(self.email, self.preset_id.mail_template_id.id, False, False)
        except UserError as e:
            _logger.warning("Error while sending email: %s", e.args[0])

    def action_send_self_order_receipt(self, email, mail_template_id, ticket_image, basic_image):
        self.ensure_one()
        self.email = email
        mail_template = self.env['mail.template'].browse(mail_template_id)
        if not mail_template:
            raise UserError(_("The mail template with xmlid %s has been deleted.", mail_template_id))
        email_values = {'email_to': email}
        if self.state == 'paid' and ticket_image:
            email_values['attachment_ids'] = self._get_mail_attachments(self.name, ticket_image, basic_image)
        mail_template.send_mail(self.id, force_send=True, email_values=email_values)

    def _send_payment_result(self, payment_result):
        self.ensure_one()
        self.config_id._notify('PAYMENT_STATUS', {
            'payment_result': payment_result,
            'data': {
                'pos.order': self.read(self._load_pos_self_data_fields(self.config_id), load=False),
                'pos.order.line': self.lines.read(self.lines._load_pos_self_data_fields(self.config_id), load=False),
            }
        })
        if payment_result == 'Success':
            self._send_self_order_receipt()
            self._send_order()

    def _load_pos_self_data_fields(self, config):
        return ['id', 'uuid', 'name', 'display_name', 'access_token', 'last_order_preparation_change', 'date_order', 'amount_total', 'amount_paid', 'amount_return', 'user_id', 'amount_tax', 'lines', 'pricelist_id', 'company_id', 'country_code', 'sequence_number', 'session_id',
                'config_id', 'currency_id', 'currency_rate', 'is_refund', 'has_refundable_lines', 'state', 'account_move', 'preset_id', 'floating_order_name', 'general_customer_note', 'internal_note', 'nb_print', 'pos_reference', 'fiscal_position_id', 'payment_ids', 'to_invoice',
                'shipping_date', 'preset_time', 'is_invoiced', 'is_tipped', 'tip_amount', 'ticket_code', 'tracking_number', 'email', 'mobile', 'table_id', 'course_ids',
                'table_stand_number', 'self_ordering_table_id', 'create_date', 'write_date', 'source', 'partner_id', 'customer_count']

    @api.model
    def _check_pos_order_lines(self, pos_config, order, line, fiscal_position_id):
        existing_order = pos_config.env['pos.order']._get_open_order(order)
        existing_lines = existing_order.lines if existing_order.exists() else pos_config.env['pos.order.line']

        if line[0] == Command.DELETE and line[1] in existing_lines.ids:
            return [Command.DELETE, line[1]]
        if line[0] == Command.UNLINK and line[1] in existing_lines.ids:
            return [Command.UNLINK, line[1]]
        if line[0] == Command.CREATE or line[0] == Command.UPDATE:
            line_data = line[2]

            product = pos_config.env['product.product'].browse(line_data.get('product_id'))
            tax_ids = fiscal_position_id.map_tax(product.taxes_id)
            command = Command.CREATE if line[0] == Command.CREATE else Command.UPDATE
            id_to_use = line[1] if line[0] == Command.UPDATE else 0

            return [command, id_to_use, {
                'combo_id': line_data.get('combo_id'),
                'product_id': line_data.get('product_id'),
                'tax_ids': tax_ids.ids,
                'attribute_value_ids': [id for id in line_data.get('attribute_value_ids', []) if isinstance(id, int)],
                'price_unit': line_data.get('price_unit'),
                'qty': line_data.get('qty'),
                'price_subtotal': 0.0,  # always recomputed server-side by recompute_prices().
                'price_subtotal_incl': 0.0,  # always recomputed server-side by recompute_prices().
                'price_extra': line_data.get('price_extra'),
                'price_type': line_data.get('price_type'),
                'full_product_name': line_data.get('full_product_name'),
                'customer_note': line_data.get('customer_note'),
                'uuid': line_data.get('uuid'),
                'id': line_data.get('id'),
                'order_id': existing_order.id if existing_order.exists() else None,
                'combo_parent_id': line_data.get('combo_parent_id'),
                'combo_item_id': line_data.get('combo_item_id'),
                'combo_line_ids': [id for id in line_data.get('combo_line_ids', []) if isinstance(id, int)],
            }]
        return []

    @api.model
    def _check_pos_order_payment(self, device_type, order, payment):
        if device_type != "kiosk" or not payment or len(payment) != 1 or len(payment[0]) != 3:
            return []

        command, payment_id = payment[0][0:2]
        if command != Command.CREATE or payment_id != 0:
            return []

        payment_line = payment[0][2]
        if "amount" not in payment_line or payment_line["amount"] != order.get("amount_total", 0):
            return []

        safe_fields = [
            "payment_method_id", "card_type", "card_brand", "card_no", "cardholder_name",
            "payment_ref_no", "payment_method_authcode", "payment_method_issuer_bank",
            "payment_method_payment_mode", "transaction_id", "ticket", "uuid",
        ] + self.env["pos.payment"]._get_additional_payment_fields()

        return [[Command.CREATE, 0, {
            'amount': order.get('amount_total'),
            'payment_status': 'done',
            **{field: payment_line.get(field) for field in safe_fields}
        }]]

    @api.model
    def _check_pos_order(self, pos_config, order, device_type, table=None):
        company = pos_config.company_id
        preset_id = order['preset_id'] if pos_config.use_presets else False
        preset_id = pos_config.env['pos.preset'].browse(preset_id) if preset_id else False
        floating_order_name = order.get('floating_order_name')

        if not preset_id and pos_config.use_presets:
            raise UserError(_("Invalid preset"))

        existing_order = pos_config.env['pos.order']._get_open_order(order)
        if not existing_order.exists():
            pos_reference, tracking_number = pos_config._get_next_order_refs()
            prefix = f"K{pos_config.id}-" if device_type == "kiosk" else "S"

            if not floating_order_name:
                if device_type == 'kiosk':
                    floating_order_name = f"Table tracker {order['table_stand_number']}" if order.get('table_stand_number') else f"Kiosk Order {tracking_number}"
                elif table:
                    floating_order_name = f"Self-Order T {table.table_number}"
                else:
                    floating_order_name = f"Self-Order {tracking_number}"

            tracking_number = f"{prefix}{tracking_number}"
        else:
            pos_reference = existing_order.pos_reference
            floating_order_name = existing_order.floating_order_name
            tracking_number = existing_order.tracking_number

        fiscal_position_id = preset_id.fiscal_position_id if preset_id else pos_config.default_fiscal_position_id
        pricelist_id = preset_id.pricelist_id if preset_id else pos_config.pricelist_id
        lines = [self._check_pos_order_lines(pos_config, order, line, fiscal_position_id) for line in order.get('lines', [])]
        lines = [line for line in lines if len(line)]
        payment_lines = self._check_pos_order_payment(device_type, order, order.get("payment_ids"))
        payment_lines = [line for line in payment_lines if len(line)]
        partner_id = order.get('partner_id')
        partner = pos_config.env['res.partner'].browse(partner_id) if partner_id else None

        return {
            'id': order.get('id'),
            'table_stand_number': order.get('table_stand_number'),
            'customer_count': order.get('customer_count'),
            'self_ordering_table_id': table.id if table else False,
            'last_order_preparation_change': order.get('last_order_preparation_change'),
            'date_order': str(fields.Datetime.now()),
            'amount_difference': order.get('amount_difference'),
            'amount_tax': order.get('amount_tax'),
            'amount_total': order.get('amount_total'),
            'amount_paid': order.get('amount_paid'),
            'amount_return': order.get('amount_return'),
            'company_id': company.id,
            'pricelist_id': pricelist_id.id if pricelist_id else False,
            'partner_id': order.get('partner_id'),
            'sequence_number': order.get('sequence_number'),
            'session_id': pos_config.current_session_id.id,
            'fiscal_position_id': fiscal_position_id.id if fiscal_position_id else False,
            'preset_id': preset_id.id if preset_id else False,
            'preset_time': order.get('preset_time'),
            'tracking_number': tracking_number,
            'source': 'kiosk' if device_type == 'kiosk' else 'mobile',
            'email': partner.email if partner else order.get('email'),
            'mobile': partner.phone if partner else order.get('mobile'),
            'state': 'draft',
            'floating_order_name': floating_order_name,
            'general_customer_note': order.get('general_customer_note'),
            'nb_print': order.get('nb_print'),
            'pos_reference': pos_reference,
            'to_invoice': order.get('to_invoice'),
            'shipping_date': order.get('shipping_date'),
            'is_tipped': order.get('is_tipped'),
            'tip_amount': order.get('tip_amount'),
            'ticket_code': order.get('ticket_code'),
            'uuid': order.get('uuid'),
            'has_deleted_line': order.get('has_deleted_line'),
            'lines': lines,
            'payment_ids': payment_lines,
            'relations_uuid_mapping': order.get('relations_uuid_mapping', {}),
        }

    def recompute_prices(self):
        self.ensure_one()
        company = self.company_id

        for line in self.lines:
            if len(line.combo_line_ids):
                self._compute_combo_price(line)
            elif not line.combo_parent_id:
                # Lines without a combo parent are priced on their own. A line whose combo
                # parent doesn't belong to this order is never reached by _compute_combo_price,
                # so it is priced the same way instead of keeping its frontend price.
                self._compute_line_price(line)

        order_lines = self.lines
        base_lines = [line._prepare_base_line_for_taxes_computation() for line in order_lines]
        self.env['account.tax']._add_tax_details_in_base_lines(base_lines, company)
        self.env['account.tax']._round_base_lines_tax_details(base_lines, company)
        tax_totals = self.env['account.tax']._get_tax_totals_summary(
            base_lines=base_lines,
            currency=self.currency_id or company.currency_id,
            company=company,
        )
        self.amount_tax = tax_totals['tax_amount_currency']
        self.amount_total = tax_totals['total_amount_currency']

    def _compute_line_price(self, line):
        pricelist = self.pricelist_id
        selected_attributes = line.attribute_value_ids
        product = line.product_id.with_context(line.product_id._get_product_price_context(selected_attributes))
        price = pricelist._get_product_price(product, 1.0, currency=self.currency_id)
        line.price_unit = price
        line.tax_ids = line.product_id.taxes_id._filter_taxes_by_company(self.company_id)
        self._compute_line_subtotals(line)

    def _compute_line_subtotals(self, line):
        """
        Recompute the price_subtotal and price_subtotal_incl of a line based on its
        price_unit, quantity, and taxes.
        In self order the price_unit is always computed server-side, so this method
        is called after the price_unit is set.
        """
        product = line.product_id.with_context(line.product_id._get_product_price_context(line.attribute_value_ids))
        taxes = line.tax_ids_after_fiscal_position.compute_all(
            line.price_unit,
            self.currency_id,
            line.qty,
            product=product,
            partner=self.partner_id,
        )
        line.price_subtotal = taxes['total_excluded']
        line.price_subtotal_incl = taxes['total_included']

    def _compute_combo_price(self, parent_line):
        """
        This method is a python version of odoo/addons/point_of_sale/static/src/app/models/utils/compute_combo_items.js
        It is used to compute the price of combo items on the server side when an order is received from
        the POS frontend. In an accounting perspective, isn't correct but we still waiting the combo
        computation from accounting side.
        """
        child_lines = parent_line.combo_line_ids
        currency = parent_line.order_id.currency_id
        taxes = self.fiscal_position_id.map_tax(parent_line.product_id.taxes_id)
        parent_line.tax_ids = taxes
        parent_lst_price = self.pricelist_id._get_product_price(parent_line.product_id, parent_line.qty)
        child_line_free = []
        child_line_extra = []

        child_lines_by_combo = {}
        for line in child_lines:
            combo = line.combo_item_id.combo_id
            child_lines_by_combo.setdefault(combo, []).append(line)

        for combo, child_lines in child_lines_by_combo.items():
            free_count = 0
            max_free = combo.qty_free

            for line in child_lines:
                qty_per_line = line.qty / line.combo_parent_id.qty if line.combo_parent_id.qty else line.qty
                qty_free = max(0, max_free - free_count)
                free_qty = min(qty_per_line, qty_free)
                extra_qty = qty_per_line - free_qty

                if free_qty > 0:
                    child_line_free.append(line)
                    free_count += free_qty

                if extra_qty > 0:
                    child_line_extra.append(line)

        original_total = sum(
            line.combo_item_id.combo_id.base_price * (
                line.qty / line.combo_parent_id.qty
                if line.combo_parent_id.qty
                else line.qty
            ) for line in child_line_free if line.combo_item_id.combo_id.qty_free > 0
        )
        remaining_total = parent_lst_price

        for index, child in enumerate(child_line_free):
            combo_item = child.combo_item_id
            combo = combo_item.combo_id
            unit_devision_factor = original_total or 1
            price_unit = currency.round(combo.base_price * parent_lst_price / unit_devision_factor)
            remaining_total -= price_unit * (child.qty / child.combo_parent_id.qty if child.combo_parent_id.qty else child.qty)

            if index == len(child_line_free) - 1:
                price_unit += remaining_total
                remaining_total = 0

            selected_attributes = child.attribute_value_ids
            price_extra = sum(attr.price_extra for attr in selected_attributes)
            total_price = price_unit + price_extra + child.combo_item_id.extra_price
            child.price_unit = total_price

        extra_original_total = 0
        if remaining_total and child_line_extra:
            extra_original_total = sum(
                line.combo_item_id.combo_id.base_price * line.qty
                for line in child_line_extra
            ) or 1

        for index, child in enumerate(child_line_extra):
            combo_item = child.combo_item_id
            price_unit = currency.round(combo_item.combo_id.base_price)

            if extra_original_total:
                remaining_proportion = currency.round(
                    combo_item.combo_id.base_price * parent_lst_price / extra_original_total
                )
                price_unit += remaining_proportion
                remaining_total -= remaining_proportion * child.qty

                if index == len(child_line_extra) - 1:
                    price_unit += remaining_total / child.qty

            selected_attributes = child.attribute_value_ids
            price_extra = sum(attr.price_extra for attr in selected_attributes)
            total_price = price_unit + price_extra + child.combo_item_id.extra_price
            child.price_unit = total_price

        # The whole combo price is carried by the child lines, the parent line is always free.
        parent_line.price_unit = 0.0
        # Only the unit prices are computed above; the subtotals the order total is derived from
        # must be recomputed too, on the parent line as well as on every child line.
        combo_lines = parent_line | parent_line.combo_line_ids
        for line in combo_lines:
            self._compute_line_subtotals(line)
