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

from odoo import _
from odoo.exceptions import UserError, ValidationError
from odoo.http import request, route

from odoo.addons.payment import utils as payment_utils
from odoo.addons.website_sale.controllers.main import WebsiteSale


class Delivery(WebsiteSale):
    _express_checkout_delivery_route = '/shop/express/shipping_address_change'

    @route('/shop/delivery_methods', type='json', auth='public', website=True)
    def shop_delivery_methods(self):
        """ Fetch available delivery methods and render them in the delivery form.

        :return: The rendered delivery form.
        :rtype: str
        """
        order_sudo = request.website.sale_get_order()
        values = {
            'delivery_methods': order_sudo._get_delivery_methods(),
            'selected_dm_id': order_sudo.carrier_id.id,
            'order': order_sudo,  # Needed for accessing default values for pickup points.
        }
        values |= self._get_additional_delivery_context()
        return request.env['ir.ui.view']._render_template('website_sale.delivery_form', values)

    def _get_additional_delivery_context(self):
        """ Hook to update values used for rendering the website_sale.delivery_form template. """
        return {}

    @route('/shop/set_delivery_method', type='json', auth='public', website=True)
    def shop_set_delivery_method(self, dm_id=None, **kwargs):
        """ Set the delivery method on the current order and return the order summary values.

        If the delivery method is already set, the order summary values are returned immediately.

        :param str dm_id: The delivery method to set, as a `delivery.carrier` id.
        :param dict kwargs: The keyword arguments forwarded to `_order_summary_values`.
        :return: The order summary values, if any.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        if not order_sudo:
            return {}

        dm_id = int(dm_id)
        if dm_id != order_sudo.carrier_id.id:
            for tx_sudo in order_sudo.transaction_ids:
                if tx_sudo.state not in ('draft', 'cancel', 'error'):
                    raise UserError(_(
                        "It seems that there is already a transaction for your order; you can't"
                        " change the delivery method anymore."
                    ))

            delivery_method_sudo = request.env['delivery.carrier'].sudo().browse(dm_id).exists()
            order_sudo._set_delivery_method(delivery_method_sudo)
        return self._order_summary_values(order_sudo, **kwargs)

    def _order_summary_values(self, order, **kwargs):
        """ Return the summary values of the order.

        :param sale.order order: The sales order whose summary values to return.
        :param dict kwargs: The keyword arguments. This parameter is not used here.
        :return: The order summary values.
        :rtype: dict
        """
        Monetary = request.env['ir.qweb.field.monetary']
        currency = order.currency_id
        return {
            'success': True,
            'is_free_delivery': not bool(order.amount_delivery),
            'amount_delivery': Monetary.value_to_html(
                order.amount_delivery, {'display_currency': currency}
            ),
            'amount_untaxed': Monetary.value_to_html(
                order.amount_untaxed, {'display_currency': currency}
            ),
            'amount_tax': Monetary.value_to_html(
                order.amount_tax, {'display_currency': currency}
            ),
            'amount_total': Monetary.value_to_html(
                order.amount_total, {'display_currency': currency}
            ),
        }

    @route('/shop/get_delivery_rate', type='json', auth='public', methods=['POST'], website=True)
    def shop_get_delivery_rate(self, dm_id):
        """ Return the delivery rate data for the given delivery method.

        :param str dm_id: The delivery method whose rate to get, as a `delivery.carrier` id.
        :return: The delivery rate data.
        :rtype: dict
        """
        order = request.website.sale_get_order()
        if not order:
            raise ValidationError(_("Your cart is empty."))

        if int(dm_id) not in order._get_delivery_methods().ids:
            raise UserError(_(
                "It seems that a delivery method is not compatible with your address. Please"
                " refresh the page and try again."
            ))

        Monetary = request.env['ir.qweb.field.monetary']
        delivery_method = request.env['delivery.carrier'].sudo().browse(int(dm_id)).exists()
        rate = Delivery._get_rate(delivery_method, order)
        if rate['success']:
            rate['amount_delivery'] = Monetary.value_to_html(
                rate['price'], {'display_currency': order.currency_id}
            )
            rate['is_free_delivery'] = not bool(rate['price'])
        else:
            rate['amount_delivery'] = Monetary.value_to_html(
                0.0, {'display_currency': order.currency_id}
            )
        return rate

    @route('/website_sale/set_pickup_location', type='json', auth='public', website=True)
    def website_sale_set_pickup_location(self, pickup_location_data):
        """ Fetch the order from the request and set the pickup location on the current order.

        :param str pickup_location_data: The JSON-formatted pickup location address.
        :return: None
        """
        order_sudo = request.website.sale_get_order()
        order_sudo._set_pickup_location(pickup_location_data)

    @route('/website_sale/get_pickup_locations', type='json', auth='public', website=True)
    def website_sale_get_pickup_locations(self, zip_code=None, **kwargs):
        """ Fetch the order from the request and return the pickup locations close to the zip code.

        Determine the country based on GeoIP or fallback on the order's delivery address' country.

        :param int zip_code: The zip code to look up to.
        :return: The close pickup locations data.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        country = order_sudo.partner_shipping_id.country_id
        return order_sudo._get_pickup_locations(zip_code, country, **kwargs)

    @route(_express_checkout_delivery_route, type='json', auth='public', website=True)
    def express_checkout_process_delivery_address(self, partial_delivery_address):
        """ Process the shipping address and return the available delivery methods.

        Depending on whether the partner is registered and logged in, a new partner is created or we
        use an existing partner that matches the partial delivery address received.

        :param dict partial_delivery_address: The delivery information sent by the express payment
                                              provider.
        :return: The available delivery methods, sorted by lowest price.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        if not order_sudo:
            return []

        self._include_country_and_state_in_address(partial_delivery_address)
        partial_delivery_address, _side_values = self._parse_form_data(partial_delivery_address)
        if order_sudo._is_anonymous_cart():
            # The partner_shipping_id and partner_invoice_id will be automatically computed when
            # changing the partner_id of the SO. This allows website_sale to avoid creating
            # duplicates.
            partial_delivery_address['name'] = _(
                'Anonymous express checkout partner for order %s',
                order_sudo.name,
            )
            new_partner_sudo = self._create_new_address(
                address_values=partial_delivery_address,
                address_type='delivery',
                use_delivery_as_billing=False,
                order_sudo=order_sudo,
            )
            # Pricelists are recomputed every time the partner is changed. We don't want to
            # recompute the price with another pricelist at this state since the customer has
            # already accepted the amount and validated the payment.
            with request.env.protecting([order_sudo._fields['pricelist_id']], order_sudo):
                order_sudo.partner_id = new_partner_sudo
        elif order_sudo.partner_shipping_id.name.endswith(order_sudo.name):
            order_sudo.partner_shipping_id.write(partial_delivery_address)
            # TODO VFE TODO VCR do we want to trigger cart recomputation here ?
            # order_sudo._update_address(
            #     order_sudo.partner_shipping_id.id, ['partner_shipping_id']
            # )
        elif not self._are_same_addresses(
            partial_delivery_address,
            order_sudo.partner_shipping_id,
        ):
            # Check if a child partner doesn't already exist with the same information. The phone
            # isn't always checked because it isn't sent in delivery information with Google Pay.
            child_partner_id = self._find_child_partner(
                order_sudo.partner_id.commercial_partner_id.id, partial_delivery_address
            )
            partial_delivery_address['name'] = _(
                'Anonymous express checkout partner for order %s',
                order_sudo.name,
            )
            order_sudo.partner_shipping_id = child_partner_id or self._create_new_address(
                address_values=partial_delivery_address,
                address_type='delivery',
                use_delivery_as_billing=False,
                order_sudo=order_sudo,
            )

        # Return the list of delivery methods available for the sales order.
        return sorted([{
            'id': dm.id,
            'name': dm.name,
            'description': dm.website_description,
            'minorAmount': payment_utils.to_minor_currency_units(price, order_sudo.currency_id),
        } for dm, price in Delivery._get_delivery_methods_express_checkout(order_sudo).items()
        ], key=lambda dm: dm['minorAmount'])

    @staticmethod
    def _get_delivery_methods_express_checkout(order_sudo):
        """ Return available delivery methods and their prices for the given order.

        :param sale.order order_sudo: The sudoed sales order.
        :rtype: dict
        :return: A dict with a `delivery.carrier` recordset as key, and a rate shipment price as
                 value.
        """
        res = {}
        for dm in order_sudo._get_delivery_methods():
            rate = Delivery._get_rate(dm, order_sudo, is_express_checkout_flow=True)
            if rate['success']:
                fname = f'{dm.delivery_type}_use_locations'
                if hasattr(dm, fname) and getattr(dm, fname):
                    continue  # Express checkout doesn't allow selecting locations.
                res[dm] = rate['price']
        return res

    @staticmethod
    def _get_rate(delivery_method, order, is_express_checkout_flow=False):
        """ Compute the delivery rate and apply the taxes if relevant.

        :param delivery.carrier delivery_method: The delivery method for which the rate must be
                                                 computed.
        :param sale.order order: The current sales order.
        :param boolean is_express_checkout_flow: Whether the flow is express checkout.
        :return: The delivery rate data.
        :rtype: dict
        """
        # Some delivery methods check if all the required fields are available before computing the
        # rate, even if those fields aren't required for the computation (although they are for
        # delivering the goods). If we only have partial information about the delivery address, but
        # still want to compute the rate, this context key will ensure that we only check the
        # required fields for a partial delivery address (city, zip, country_code, state_code).
        rate = delivery_method.rate_shipment(order.with_context(
            express_checkout_partial_delivery_address=is_express_checkout_flow
        ))
        if rate.get('success'):
            tax_ids = delivery_method.product_id.taxes_id.filtered(
                lambda t: t.company_id == order.company_id
            )
            if tax_ids:
                fpos = order.fiscal_position_id
                tax_ids = fpos.map_tax(tax_ids)
                taxes = tax_ids.compute_all(
                    rate['price'],
                    currency=order.currency_id,
                    quantity=1.0,
                    product=delivery_method.product_id,
                    partner=order.partner_shipping_id,
                )
                if (
                    not is_express_checkout_flow
                    and request.website.show_line_subtotals_tax_selection == 'tax_excluded'
                ):
                    rate['price'] = taxes['total_excluded']
                else:
                    rate['price'] = taxes['total_included']
        return rate
