# © 2021 Hibou Corp.

import zlib
from datetime import date, datetime
from base64 import b64decode

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

import logging
_logger = logging.getLogger(__name__)


class ProductPackaging(models.Model):
    _inherit = 'product.packaging'

    amazon_sp_mfn_allowed_services = fields.Text(
        string='Amazon SP MFN Allowed Methods',
        help='Comma separated list. e.g. "FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB"')


class ProviderAmazonSP(models.Model):
    _inherit = 'delivery.carrier'

    delivery_type = fields.Selection(selection_add=[
        # ('amazon_sp', 'Amazon Selling Partner'),  # TODO buy shipping for regular orders?
        ('amazon_sp_mfn', 'Amazon SP Merchant Fulfillment')
    ])

    # Fields when uploading shipping to Amazon
    amazon_sp_carrier_code = fields.Char(string='Amazon Carrier Code',
                                         help='Specific carrier code, will default to "Other".')
    amazon_sp_carrier_name = fields.Char(string='Amazon Carrier Name',
                                         help='Specific carrier name, will default to regular name.')
    amazon_sp_shipping_method = fields.Char(string='Amazon Shipping Method',
                                            help='Specific shipping method, will default to "Standard"')
    # Fields when purchasing shipping from Amazon
    amazon_sp_mfn_allowed_services = fields.Text(
        string='Allowed Methods',
        help='Comma separated list. e.g. "FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB"',
        default='FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB')
    amazon_sp_mfn_label_formats = fields.Text(
        string='Allowed Label Formats',
        help='Comma separated list. e.g. "ZPL203,PNG"',
        default='ZPL203,PNG')

    def send_shipping(self, pickings):
        pickings = pickings.with_context(amz_pii_decrypt=1)
        self = self.with_context(amz_pii_decrypt=1)
        return super(ProviderAmazonSP, self).send_shipping(pickings)

    def is_amazon(self, order=None, picking=None):
        # Override from `delivery_hibou` to be used in stamps etc....
        if picking and picking.sale_id:
            so = picking.sale_id
            if so.amazon_bind_ids:
                return True
        if order and order.amazon_bind_ids:
            return True
        return super().is_amazon(order=order, picking=picking)

    def _amazon_sp_mfn_get_order_details(self, order):
        company = self.get_shipper_company(order=order)
        wh_partner = self.get_shipper_warehouse(order=order)

        if not order.amazon_bind_ids:
            raise ValidationError('Amazon shipping is not available for this order.')

        amazon_order_id = order.amazon_bind_ids[0].external_id
        from_ = dict(
            Name=company.name,
            AddressLine1=wh_partner.street,
            AddressLine2=wh_partner.street2 or '',
            City=wh_partner.city,
            StateOrProvinceCode=wh_partner.state_id.code,
            PostalCode=wh_partner.zip,
            CountryCode=wh_partner.country_id.code,
            Email=company.email or '',
            Phone=company.phone or '',
        )
        return amazon_order_id, from_

    def _amazon_sp_mfn_get_items_for_order(self, order):
        items = order.order_line.filtered(lambda l: l.amazon_bind_ids)
        return items.mapped(lambda l: (l.amazon_bind_ids[0].external_id, str(int(l.product_qty))))

    def _amazon_sp_mfn_get_items_for_package(self, package, order):
        items = []
        if not package.quant_ids:
            for move_line in package.current_picking_move_line_ids:
                line = order.order_line.filtered(lambda l: l.product_id.id == move_line.product_id.id and l.amazon_bind_ids)
                if line:
                    items.append((line[0].amazon_bind_ids[0].external_id, int(move_line.qty_done), {
                        'Unit': 'g',
                        'Value': line.product_id.weight * move_line.qty_done * 1000,
                    }, line.name))
        else:
            for quant in package.quant_ids:
                line = order.order_line.filtered(lambda l: l.product_id.id == quant.product_id.id and l.amazon_bind_ids)
                if line:
                    items.append((line[0].amazon_bind_ids[0].external_id, int(quant.quantity), {
                        'Unit': 'g',
                        'Value': line.product_id.weight * quant.quantity * 1000,
                    }, line.name))
        return items

    def _amazon_sp_mfn_convert_weight(self, weight):
        return int(weight * 1000), 'g'

    def _amazon_sp_mfn_pick_service(self, api_services, package=None):
        allowed_services = self.amazon_sp_mfn_allowed_services.split(',')
        if package and package.packaging_id.amazon_sp_mfn_allowed_services:
            allowed_services = package.packaging_id.amazon_sp_mfn_allowed_services.split(',')

        allowed_label_formats = self.amazon_sp_mfn_label_formats.split(',')
        services = []
        api_service_list = api_services['ShippingServiceList']
        if not isinstance(api_service_list, list):
            api_service_list = [api_service_list]
        for s in api_service_list:
            if s['ShippingServiceId'] in allowed_services:
                s_available_formats = s['AvailableLabelFormats']
                for l in allowed_label_formats:
                    if l in s_available_formats:
                        services.append({
                            'service_id': s['ShippingServiceId'],
                            'amount': float(s['Rate']['Amount']),
                            'label_format': l
                        })
                        break
        if services:
            return sorted(services, key=lambda s: s['amount'])[0]

        error = 'Cannot find applicable service. API Services: ' + \
                ','.join([s['ShippingServiceId'] for s in api_services['ShippingServiceList']]) + \
                ' Allowed Services: ' + self.amazon_sp_mfn_allowed_services
        raise ValidationError(error)

    def amazon_sp_mfn_send_shipping(self, pickings):
        res = []
        date_planned = datetime.now().replace(microsecond=0).isoformat()

        for picking in pickings:
            shipments = []

            picking_packages = picking.package_ids
            package_carriers = picking_packages.mapped('carrier_id')
            if package_carriers:
                # only ship ours
                picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)

            if not picking_packages:
                continue

            order = picking.sale_id.sudo()  # for having access to amazon bindings and backend

            # API comes from the binding backend
            if order.amazon_bind_ids:
                amazon_order = order.amazon_bind_ids[0]
                api_wrapped = amazon_order.backend_id.get_wrapped_api()
                # must_arrive_by_date not used, and `amazon_order.requested_date` can be False
                # so if it is to be used, we must decide what to do if there is no date.
                # must_arrive_by_date = fields.Datetime.from_string(amazon_order.requested_date).isoformat()
                api = api_wrapped.merchant_fulfillment()

            if not api:
                raise UserError('%s cannot be shipped by Amazon SP without a Sale Order or backend to use.' % picking)

            amazon_order_id, from_ = self._amazon_sp_mfn_get_order_details(order)
            for package in picking_packages:
                dimensions = {
                    'Length': package.packaging_id.length or 0.1,
                    'Width': package.packaging_id.width or 0.1,
                    'Height': package.packaging_id.height or 0.1,
                    'Unit': 'inches',
                }
                weight, weight_unit = self._amazon_sp_mfn_convert_weight(package.shipping_weight)
                items = self._amazon_sp_mfn_get_items_for_package(package, order)
                # Declared value
                inventory_value = self.get_inventory_value(picking=picking, package=package)
                sig_req = self.get_signature_required(picking=picking, package=package)

                ShipmentRequestDetails = {
                    'AmazonOrderId': amazon_order_id,
                    'ShipFromAddress': from_,
                    'Weight': {'Unit': weight_unit, 'Value': weight},
                    'SellerOrderId': order.name,
                    # The format of these dates cannot be determined, attempts:
                    # 2021-04-27 08:00:00
                    # 2021-04-27T08:00:00
                    # 2021-04-27T08:00:00Z
                    # 2021-04-27T08:00:00+00:00
                    # 'ShipDate': date_planned,
                    # 'MustArriveByDate': must_arrive_by_date,
                    'ShippingServiceOptions': {
                        'DeliveryExperience': 'DeliveryConfirmationWithSignature' if sig_req else 'NoTracking',
                        # CarrierWillPickUp is required
                        'CarrierWillPickUp': False,  # Note: Scheduled carrier pickup is available only using Dynamex (US), DPD (UK), and Royal Mail (UK).
                        'DeclaredValue': {
                            'Amount': inventory_value,
                            'CurrencyCode': 'USD'
                        },
                        # Conflicts at time of shipping for the above
                        # 'CarrierWillPickUpOption': 'NoPreference',
                        'LabelFormat': 'ZPL203'
                    },
                    'ItemList': [{
                        'OrderItemId': i[0],
                        'Quantity': i[1],
                        'ItemWeight': i[2],
                        'ItemDescription': i[3],
                    } for i in items],
                    'PackageDimensions': dimensions,
                }

                try:
                    # api_services = api.get_eligible_shipment_services(ShipmentRequestDetails, ShippingOfferingFilter={
                    #     'IncludePackingSlipWithLabel': False,
                    #     'IncludeComplexShippingOptions': False,
                    #     'CarrierWillPickUp': 'CarrierWillPickUp',
                    #     'DeliveryExperience': 'NoTracking',
                    # })
                    api_services = api.get_eligible_shipment_services(ShipmentRequestDetails)
                except api_wrapped.SellingApiForbiddenException:
                    raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
                except api_wrapped.SellingApiException as e:
                    raise UserError('API Exception: ' + str(e.message))

                api_services = api_services.payload
                service = self._amazon_sp_mfn_pick_service(api_services, package=package)

                try:
                    shipment = api.create_shipment(ShipmentRequestDetails, service['service_id']).payload
                except api_wrapped.SellingApiForbiddenException:
                    raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
                except api_wrapped.SellingApiException as e:
                    raise UserError('API Exception: ' + str(e.message))

                shipments.append((shipment, service))

            carrier_price = 0.0
            tracking_numbers =[]
            for shipment, service in shipments:
                tracking_number = shipment['TrackingId']
                carrier_name = shipment['ShippingService']['CarrierName']
                label_data = shipment['Label']['FileContents']['Contents']

                # So far, this is b64encoded and gzipped
                try:
                    label_decoded = b64decode(label_data)
                    try:
                        label_decoded = zlib.decompress(label_decoded)
                    except:
                        label_decoded = zlib.decompress(label_decoded, zlib.MAX_WBITS | 16)
                    label_data = label_decoded
                except:
                    # Oh well...
                    pass

                body = 'Shipment created into Amazon MFN<br/> <b>Tracking Number : <br/>' + tracking_number + '</b>'
                picking.message_post(body=body, attachments=[('Label%s-%s.%s' % (carrier_name, tracking_number, service['label_format']), label_data)])
                carrier_price += float(shipment['ShippingService']['Rate']['Amount'])
                tracking_numbers.append(tracking_number)
            shipping_data = {'exact_price': carrier_price, 'tracking_number': ','.join(tracking_numbers)}
            res = res + [shipping_data]

        return res

    def amazon_sp_mfn_rate_shipment_multi(self, order=None, picking=None, packages=None):
        if not packages:
            return self._amazon_sp_mfn_rate_shipment_multi_package(order=order, picking=picking)
        else:
            rates = []
            for package in packages:
                rates += self._amazon_sp_mfn_rate_shipment_multi_package(order=order, picking=picking, package=package)
            return rates

    def _amazon_sp_mfn_rate_shipment_multi_package(self, order=None, picking=None, package=None):
        res = []
        self.ensure_one()
        date_planned = fields.Datetime.now()
        if self.env.context.get('date_planned'):
            date_planned = self.env.context.get('date_planned')

        if order or not picking:
            raise UserError('Amazon SP MFN is intended to be used on imported orders.')
        if package:
            packages = package
        else:
            packages = picking.package_ids

        if not packages:
            raise UserError('Amazon SP MFN can only be used with packed items.')

        # to use current inventory in package
        packages = packages.with_context(picking_id=picking.id)

        order = picking.sale_id.sudo()
        api = None
        if order.amazon_bind_ids:
            amazon_order = order.amazon_bind_ids[0]
            api_wrapped = amazon_order.backend_id.get_wrapped_api()
            # must_arrive_by_date not used, and `amazon_order.requested_date` can be False
            # so if it is to be used, we must decide what to do if there is no date.
            # must_arrive_by_date = fields.Datetime.from_string(amazon_order.requested_date).isoformat()
            api = api_wrapped.merchant_fulfillment()

        if not api:
            raise UserError('%s cannot be shipped by Amazon SP without a Sale Order or backend to use.' % picking)

        amazon_order_id, from_ = self._amazon_sp_mfn_get_order_details(order)
        for package in packages:
            dimensions = {
                'Length': package.packaging_id.length or 0.1,
                'Width': package.packaging_id.width or 0.1,
                'Height': package.packaging_id.height or 0.1,
                'Unit': 'inches',
            }
            weight, weight_unit = self._amazon_sp_mfn_convert_weight(package.shipping_weight)
            items = self._amazon_sp_mfn_get_items_for_package(package, order)
            # Declared value
            inventory_value = self.get_insurance_value(picking=picking, package=package)
            sig_req = self.get_signature_required(picking=picking, package=packages)


            ShipmentRequestDetails = {
                'AmazonOrderId': amazon_order_id,
                'ShipFromAddress': from_,
                'Weight': {'Unit': weight_unit, 'Value': weight},
                'SellerOrderId': order.name,
                # The format of these dates cannot be determined, attempts:
                # 2021-04-27 08:00:00
                # 2021-04-27T08:00:00
                # 2021-04-27T08:00:00Z
                # 2021-04-27T08:00:00+00:00
                # 'ShipDate': date_planned,
                # 'MustArriveByDate': must_arrive_by_date,
                'ShippingServiceOptions': {
                    'DeliveryExperience': 'DeliveryConfirmationWithSignature' if sig_req else 'NoTracking',
                    # CarrierWillPickUp is required
                    'CarrierWillPickUp': False,
                    # Note: Scheduled carrier pickup is available only using Dynamex (US), DPD (UK), and Royal Mail (UK).
                    'DeclaredValue': {
                        'Amount': inventory_value,
                        'CurrencyCode': 'USD'
                    },
                    # Conflicts at time of shipping for the above
                    # 'CarrierWillPickUpOption': 'NoPreference',
                    'LabelFormat': 'ZPL203'
                },
                'ItemList': [{
                    'OrderItemId': i[0],
                    'Quantity': i[1],
                    'ItemWeight': i[2],
                    'ItemDescription': i[3],
                } for i in items],
                'PackageDimensions': dimensions,
            }

            try:
                # api_services = api.get_eligible_shipment_services(ShipmentRequestDetails, ShippingOfferingFilter={
                #     'IncludePackingSlipWithLabel': False,
                #     'IncludeComplexShippingOptions': False,
                #     'CarrierWillPickUp': 'CarrierWillPickUp',
                #     'DeliveryExperience': 'NoTracking',
                # })
                api_services = api.get_eligible_shipment_services(ShipmentRequestDetails)
            except api_wrapped.SellingApiForbiddenException:
                raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
            except api_wrapped.SellingApiException as e:
                raise UserError('API Exception: ' + str(e.message))

            api_services = api_services.payload
            # project into distinct carrier
            allowed_services = self.amazon_sp_mfn_allowed_services.split(',')
            if package and package.packaging_id.amazon_sp_mfn_allowed_services:
                allowed_services = package.packaging_id.amazon_sp_mfn_allowed_services.split(',')

            api_service_list = api_services['ShippingServiceList']
            if not isinstance(api_service_list, list):
                api_service_list = [api_service_list]

            for s in filter(lambda s: s['ShippingServiceId'] in allowed_services, api_service_list):
                _logger.warning('ShippingService: ' + str(s))
                service_code = s['ShippingServiceId']
                carrier = self.amazon_sp_mfn_find_delivery_carrier_for_service(service_code)
                if carrier:
                    res.append({
                        'carrier': carrier,
                        'package': package or self.env['stock.quant.package'].browse(),
                        'success': True,
                        'price': s['Rate']['Amount'],
                        'error_message': False,
                        'warning_message': False,
                        # 'transit_days': transit_days,
                        'date_delivered': s['LatestEstimatedDeliveryDate'] if s['LatestEstimatedDeliveryDate'] else s['EarliestEstimatedDeliveryDate'],
                        'date_planned': date_planned,
                        'service_code': service_code,
                    })
            if not res:
                res.append({
                    'success': False,
                    'price': 0.0,
                    'error_message': 'No valid rates returned from AmazonSP-MFN',
                    'warning_message': False
                })
            return res

    def amazon_sp_mfn_find_delivery_carrier_for_service(self, service_code):
        if self.amazon_sp_mfn_allowed_services == service_code:
            return self
        carrier = self.search([('amazon_sp_mfn_allowed_services', '=', service_code),
                               ('delivery_type', '=', 'amazon_sp_mfn')
                               ], limit=1)
        return carrier
