# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.

from zeep.exceptions import Fault
from datetime import datetime
from copy import deepcopy
from odoo.addons.delivery_fedex.models import fedex_request
from odoo.tools import remove_accents

STATECODE_REQUIRED_COUNTRIES = fedex_request.STATECODE_REQUIRED_COUNTRIES


def sanitize_name(name):
    if isinstance(name, str):
        return name.replace('[', '').replace(']', '')
    return 'Unknown'


class FedexRequest(fedex_request.FedexRequest):
    _transit_days = {
        'ONE_DAYS': 1,
        'ONE_DAY': 1,
        'TWO_DAYS': 2,
        'THREE_DAYS': 3,
        'FOUR_DAYS': 4,
        'FIVE_DAYS': 5,
        'SIX_DAYS': 6,
        'SEVEN_DAYS': 7,
        'EIGHT_DAYS': 8,
        'NINE_DAYS': 9,
        'TEN_DAYS': 10,
    }

    _service_transit_days = {
        'FEDEX_2_DAY': 2,
        'FEDEX_2_DAY_AM': 2,
        'FEDEX_3_DAY_FREIGHT': 3,
        'FIRST_OVERNIGHT': 1,
        'PRIORITY_OVERNIGHT': 1,
        'STANDARD_OVERNIGHT': 1,
    }

    def set_recipient(self, recipient_partner, attn=None, residential=False):
        """
        Adds ATTN: and sanitizes against known 'illegal' common characters in names.
        :param recipient_partner: default
        :param attn: NEW add to contact name as an ' ATTN: $attn'
        :param residential: NEW allow ground home delivery
        :return:
        """
        Contact = self.factory.Contact()
        if recipient_partner.is_company:
            Contact.PersonName = ''
            Contact.CompanyName = sanitize_name(remove_accents(recipient_partner.name))
        else:
            Contact.PersonName = sanitize_name(remove_accents(recipient_partner.name))
            Contact.CompanyName = sanitize_name(remove_accents(recipient_partner.parent_id.name)) or ''

        if attn:
            Contact.PersonName = Contact.PersonName + ' ATTN: ' + str(attn)

        Contact.PhoneNumber = recipient_partner.phone or ''

        Address = self.factory.Address()
        Address.StreetLines = [remove_accents(recipient_partner.street) or '', remove_accents(recipient_partner.street2) or '']
        Address.City = remove_accents(recipient_partner.city) or ''
        if recipient_partner.country_id.code in STATECODE_REQUIRED_COUNTRIES:
            Address.StateOrProvinceCode = recipient_partner.state_id.code or ''
        else:
            Address.StateOrProvinceCode = ''
        Address.PostalCode = recipient_partner.zip or ''
        Address.CountryCode = recipient_partner.country_id.code or ''

        if residential:
            Address.Residential = True

        self.RequestedShipment.Recipient = self.factory.Party()
        self.RequestedShipment.Recipient.Contact = Contact
        self.RequestedShipment.Recipient.Address = Address

    def add_package(self, weight_value, package_code=False, package_height=0, package_width=0, package_length=0, sequence_number=False, mode='shipping', reference=False, insurance=False, signature_required=False):
        # TODO remove in master and change the signature of a public method
        return self._add_package(weight_value=weight_value, package_code=package_code, package_height=package_height, package_width=package_width,
                                 package_length=package_length, sequence_number=sequence_number, mode=mode, po_number=False, dept_number=False, reference=reference, insurance=insurance, signature_required=signature_required)

    def _add_package(self, weight_value, package_code=False, package_height=0, package_width=0, package_length=0, sequence_number=False, mode='shipping', po_number=False, dept_number=False, reference=False, insurance=False, signature_required=False):
        package = self.factory.RequestedPackageLineItem()
        package_weight = self.factory.Weight()
        package_weight.Value = weight_value
        package_weight.Units = self.RequestedShipment.TotalWeight.Units

        if insurance:
            insured = self.factory.Money()
            insured.Amount = insurance
            # TODO at some point someone might need currency here
            insured.Currency = 'USD'
            package.InsuredValue = insured

        special_service = self.factory.PackageSpecialServicesRequested()
        signature_detail = self.factory.SignatureOptionDetail()
        signature_detail.OptionType = 'DIRECT' if signature_required else 'NO_SIGNATURE_REQUIRED'
        special_service.SignatureOptionDetail = signature_detail
        package.SpecialServicesRequested = special_service

        package.PhysicalPackaging = 'BOX'
        if package_code == 'YOUR_PACKAGING':
            package.Dimensions = self.factory.Dimensions()
            package.Dimensions.Height = package_height
            package.Dimensions.Width = package_width
            package.Dimensions.Length = package_length
            # TODO in master, add unit in product packaging and perform unit conversion
            package.Dimensions.Units = "IN" if self.RequestedShipment.TotalWeight.Units == 'LB' else 'CM'
        if po_number:
            po_reference = self.factory.CustomerReference()
            po_reference.CustomerReferenceType = 'P_O_NUMBER'
            po_reference.Value = po_number
            package.CustomerReferences.append(po_reference)
        if dept_number:
            dept_reference = self.factory.CustomerReference()
            dept_reference.CustomerReferenceType = 'DEPARTMENT_NUMBER'
            dept_reference.Value = dept_number
            package.CustomerReferences.append(dept_reference)
        if reference:
            customer_reference = self.factory.CustomerReference()
            customer_reference.CustomerReferenceType = 'CUSTOMER_REFERENCE'
            customer_reference.Value = reference
            package.CustomerReferences.append(customer_reference)

        package.Weight = package_weight
        if mode == 'rating':
            package.GroupPackageCount = 1
        if sequence_number:
            package.SequenceNumber = sequence_number
        else:
            self.hasOnePackage = True

        if mode == 'rating':
            self.RequestedShipment.RequestedPackageLineItems.append(package)
        else:
            self.RequestedShipment.RequestedPackageLineItems = package

    def shipping_charges_payment(self, shipping_charges_payment_account, third_party=False):
        """
        Allow 'shipping_charges_payment_account' to be considered 'third_party'
        :param shipping_charges_payment_account: default
        :param third_party: NEW add to indicate that the 'shipping_charges_payment_account' is third party.
        :return:
        """
        self.RequestedShipment.ShippingChargesPayment = self.factory.Payment()
        self.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' if not third_party else 'THIRD_PARTY'
        Payor = self.factory.Payor()
        Payor.ResponsibleParty = self.factory.Party()
        Payor.ResponsibleParty.AccountNumber = shipping_charges_payment_account
        self.RequestedShipment.ShippingChargesPayment.Payor = Payor

    def shipment_request(self, dropoff_type, service_type, packaging_type, overall_weight_unit, saturday_delivery, ship_timestamp=None):
        self.RequestedShipment = self.factory.RequestedShipment()
        self.RequestedShipment.SpecialServicesRequested = self.factory.ShipmentSpecialServicesRequested()
        self.RequestedShipment.ShipTimestamp = ship_timestamp or datetime.now()
        self.RequestedShipment.DropoffType = dropoff_type
        self.RequestedShipment.ServiceType = service_type
        self.RequestedShipment.PackagingType = packaging_type
        # Resuest estimation of duties and taxes for international shipping
        if service_type in ['INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY']:
            self.RequestedShipment.EdtRequestType = 'ALL'
        else:
            self.RequestedShipment.EdtRequestType = 'NONE'
        self.RequestedShipment.PackageCount = 0
        self.RequestedShipment.TotalWeight = self.factory.Weight()
        self.RequestedShipment.TotalWeight.Units = overall_weight_unit
        self.RequestedShipment.TotalWeight.Value = 0
        self.listCommodities = []
        if saturday_delivery:
            timestamp_day = self.RequestedShipment.ShipTimestamp.strftime("%A")
            if (service_type == 'FEDEX_2_DAY' and timestamp_day == 'Thursday') or (service_type in ['PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', 'INTERNATIONAL_PRIORITY'] and timestamp_day == 'Friday'):
                self.RequestedShipment.SpecialServicesRequested.SpecialServiceTypes.append('SATURDAY_DELIVERY')

    # Rating stuff

    def rate(self, date_planned=None, multi=False):
        """
        Response will contain 'transit_days' key with number of days.
        :param date_planned: Planned Outgoing shipment. Used to have FedEx tell us how long it will take for the package to arrive.
        :return:
        """
        if multi:
            multi_result = []
        if date_planned:
            self.RequestedShipment.ShipTimestamp = date_planned

        formatted_response = {'price': {}}
        del self.ClientDetail['Region']
        if self.hasCommodities:
            self.RequestedShipment.CustomsClearanceDetail.Commodities = self.listCommodities

        try:
            self.response = self.client.service.getRates(WebAuthenticationDetail=self.WebAuthenticationDetail,
                                                         ClientDetail=self.ClientDetail,
                                                         TransactionDetail=self.TransactionDetail,
                                                         Version=self.VersionId,
                                                         RequestedShipment=self.RequestedShipment,
                                                         ReturnTransitAndCommit=True)  # New ReturnTransitAndCommit for CommitDetails in response
            if (self.response.HighestSeverity != 'ERROR' and self.response.HighestSeverity != 'FAILURE'):
                if not getattr(self.response, "RateReplyDetails", False):
                    raise Exception("No rating found")
                if not multi:
                    for rating in self.response.RateReplyDetails[0].RatedShipmentDetails:
                        formatted_response['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = float(rating.ShipmentRateDetail.TotalNetFedExCharge.Amount)
                    if len(self.response.RateReplyDetails[0].RatedShipmentDetails) == 1:
                        if 'CurrencyExchangeRate' in self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail and self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail['CurrencyExchangeRate']:
                            formatted_response['price'][self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = float(self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount) / float(self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate)

                    # Hibou Delivery Planning
                    if hasattr(self.response.RateReplyDetails[0], 'DeliveryTimestamp') and self.response.RateReplyDetails[0].DeliveryTimestamp:
                        formatted_response['date_delivered'] = self.response.RateReplyDetails[0].DeliveryTimestamp
                        if hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'TransitTime'):
                            transit_days = self.response.RateReplyDetails[0].CommitDetails[0].TransitTime
                            transit_days = self._transit_days.get(transit_days, 0)
                            formatted_response['transit_days'] = transit_days
                    elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'CommitTimestamp'):
                        formatted_response['date_delivered'] = self.response.RateReplyDetails[0].CommitDetails[0].CommitTimestamp
                        formatted_response['transit_days'] = self._service_transit_days.get(self.response.RateReplyDetails[0].CommitDetails[0].ServiceType, 0)
                    elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'TransitTime'):
                        transit_days = self.response.RateReplyDetails[0].CommitDetails[0].TransitTime
                        transit_days = self._transit_days.get(transit_days, 0)
                        formatted_response['transit_days'] = transit_days
                else:
                    for rate_reply_detail in self.response.RateReplyDetails:
                        res = deepcopy(formatted_response)
                        res['service_code'] = rate_reply_detail.ServiceType
                        for rating in rate_reply_detail.RatedShipmentDetails:
                            res['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = float(rating.ShipmentRateDetail.TotalNetFedExCharge.Amount)
                        if len(rate_reply_detail.RatedShipmentDetails) == 1:
                            if 'CurrencyExchangeRate' in rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail and rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail['CurrencyExchangeRate']:
                                res['price'][rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = float(rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount) / float(rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate)
                        # Hibou Delivery Planning
                        if hasattr(rate_reply_detail, 'DeliveryTimestamp') and rate_reply_detail.DeliveryTimestamp:
                            res['date_delivered'] = rate_reply_detail.DeliveryTimestamp
                            res['transit_days'] = self._service_transit_days.get(rate_reply_detail.ServiceType, 0)
                            if not res['transit_days'] and hasattr(rate_reply_detail.CommitDetails[0], 'TransitTime'):
                                transit_days = rate_reply_detail.CommitDetails[0].TransitTime
                                transit_days = self._transit_days.get(transit_days, 0)
                                res['transit_days'] = transit_days
                        elif hasattr(rate_reply_detail, 'CommitDetails') and hasattr(rate_reply_detail.CommitDetails[0], 'CommitTimestamp'):
                            res['date_delivered'] = rate_reply_detail.CommitDetails[0].CommitTimestamp
                            res['transit_days'] = self._service_transit_days.get(rate_reply_detail.ServiceType, 0)
                        elif hasattr(rate_reply_detail, 'CommitDetails') and hasattr(rate_reply_detail.CommitDetails[0], 'TransitTime'):
                            transit_days = rate_reply_detail.CommitDetails[0].TransitTime
                            transit_days = self._transit_days.get(transit_days, 0)
                            res['transit_days'] = transit_days
                        multi_result.append(res)
            else:
                errors_message = '\n'.join([("%s: %s" % (n.Code, n.Message)) for n in self.response.Notifications if (n.Severity == 'ERROR' or n.Severity == 'FAILURE')])
                formatted_response['errors_message'] = errors_message

            if any([n.Severity == 'WARNING' for n in self.response.Notifications]):
                warnings_message = '\n'.join([("%s: %s" % (n.Code, n.Message)) for n in self.response.Notifications if n.Severity == 'WARNING'])
                formatted_response['warnings_message'] = warnings_message

        except Fault as fault:
            formatted_response['errors_message'] = fault
        except IOError:
            formatted_response['errors_message'] = "Fedex Server Not Found"
        except Exception as e:
            formatted_response['errors_message'] = e.args[0]

        if multi:
            return multi_result
        return formatted_response
