<?php

namespace Hilco\Freight;

use Arr;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Hilco\Models\DeliveryTerm;
use Hilco\Models\RateShopAPIDetails;
use Hilco\Models\RateShopGroup;
use SimpleXMLElement;

class FreightHelper {
    private ?string $groupCode;

    /**
     * @param $groupCode
     */
    public function __construct (string $groupCode = null) {
        $this->groupCode = $groupCode;
    }

    /**
     * @param $serviceChargeCode
     * @param $currencyCode
     * @param $orderAmount
     * @return array|false|mixed|void
     * @throws GuzzleException
     * @throws Exception
     */
    public function getFreightServiceCharge ($serviceChargeCode, $currencyCode, $orderAmount) {
        $client = new Client([
            'base_uri' => config('m3-config.host').config('m3-config.environment').config('m3-config.endpoints.m3'),
            'headers' => [
                'Authorization' => 'Bearer '.m3()->activeToken(),
                'Accept' => 'application/json',
            ],
            'connect_timeout' => 5,
            'timeout' => 5,
        ]);
        $fieldsToReturn = 'LIMT,CHRG';
        $requestFields = [
            'CONO='.config('m3-config.cono'),
            'EXCD='.$serviceChargeCode,
            'CUCD='.$currencyCode
        ];
        $commandUri =
            config('m3-config.commands.freightServiceCharge').
            config('m3-config.options.noMetadata').
            config('m3-config.options.fieldsToReturn').$fieldsToReturn;
        $response = $client->get($commandUri, ['query' => http_build_query($requestFields)]);
        $responseBody = $response->getBody();
        $responseArray = json_decode($responseBody, true);
        if (empty($responseArray) || !Arr::get($responseArray, 'MIRecord', false)) {
            throw new Exception("M3 failed to return a valid response to LstServChrgMtrx: ".$responseBody);
        } else {
            return $this->parseServiceCharge($responseArray, $orderAmount);
        }
    }

    /**
     * @param $lstServChrgMtrxResponse
     * @param $orderAmount
     * @return array|false|mixed
     */
    private function parseServiceCharge ($lstServChrgMtrxResponse, $orderAmount) {
        $freightChargesReturnObj = false;
        // Parse service charges and convert to a sortable collection with each tier represented by key=>val
        $serviceChargeMatrix = collect();
        foreach ($lstServChrgMtrxResponse['MIRecord'] as $miRecordItem) {
            $nameValueFields = $miRecordItem['NameValue'];
            $nameValueMap = [];
            foreach ($nameValueFields as $nameValueField) {
                $nameValueMap[$nameValueField['Name']] = trim($nameValueField['Value']);
            }
            $serviceChargeMatrix->push($nameValueMap);
        }

        $serviceChargeMatrix = $serviceChargeMatrix->sortBy("LIMT");

        // Calculate the appropriate service charge based on the order amount and available tier limits
        $lowerLimit = -1;
        $upperLimit = false;
        foreach ($serviceChargeMatrix as $serviceChargeTier) {
            if ($upperLimit === false) {
                $upperLimit = $serviceChargeTier['LIMT'];
            }
            if ($orderAmount > $lowerLimit && $orderAmount <= $upperLimit) {
                $freightChargesReturnObj['LIMT'] = $serviceChargeTier['LIMT'];
                $freightChargesReturnObj['CHRG'] = $serviceChargeTier['CHRG'];
                break;
            }
            $lowerLimit = $upperLimit;
            $upperLimit = false;
        }

        // If return obj is still unset then the order amount exceeds all service charge limits, set to 0
        if ($freightChargesReturnObj === false) {
            $freightChargesReturnObj['LIMT'] = 'Limit Exceeded';
            $freightChargesReturnObj['CHRG'] = 0;
        }

        $serviceChargeName = '';
        $freightChargesReturnObj['CRD0'] = $serviceChargeName;

        return $freightChargesReturnObj;
    }

    /**
     * @param $totalWeight
     * @param $totalVolume
     * @param $toCity
     * @param $toState
     * @param $toPostalCode
     * @param $toCountry
     * @param $fromPostalCode
     * @param $fromCountry
     * @return array
     * @throws GuzzleException
     * @throws Exception
     */
    public function getPWRates ($totalWeight, $totalVolume,
                                $toCity, $toState,
                                $toPostalCode, $toCountry,
                                $fromPostalCode, $fromCountry): array {
        if (is_null($this->groupCode)) return [];

        $xmlBody = $this->buildXMLBody(
            $totalWeight, $totalVolume,
            $toCity, $toState,
            $toPostalCode, $toCountry,
            $fromPostalCode, $fromCountry
        );

        $client = new Client([
            'base_uri' => config('processweaver.host'),
            'headers' => [
                'Accept' => 'application/xml',
                'Content-Type' => 'text/xml',
            ],
            'connect_timeout' => 5,
            'timeout' => 5,
            'body' => $xmlBody,
        ]);

        $response = $client->get(config('processweaver.commands.rateshop'));
        $responseBody = $response->getBody();
        $responseContents = $responseBody->getContents();
        $responseXML = simplexml_load_string($responseContents, "SimpleXMLElement", LIBXML_NOWARNING);
        if ($responseXML === false || !($responseXML instanceof SimpleXMLElement)) {
            throw new Exception("Process Weaver did not return a valid XML response: ".$responseContents);
        }
        return $this->parseXMLResponse($responseXML);
    }

    /**
     * @param $totalWeight, float value, total weight of items being shipped
     * @param $totalVolume, float value, "total" volume of items being shipped
     * @param $toCity, name of destination city
     * @param $toState, 2-character code of destination state
     * @param $toPostalCode, 5-digit destination zip code
     * @param $toCountry, 2-character code of destination country
     * @param $fromPostalCode, 5-digit origin zip code
     * @param $fromCountry, 2-character origin country
     * @return string, Process Weaver RateShop request body in XML
     */
    private function buildXMLBody ($totalWeight, $totalVolume,
                                   $toCity, $toState,
                                   $toPostalCode, $toCountry,
                                   $fromPostalCode, $fromCountry): string {
        if ($totalWeight < 0.01) {
            $totalWeight = 0.01;
        }
        $cubedVolume = ceil(pow($totalVolume * 1.1, 1/3));

        return <<<XML
<RateAPIRequest>
    <ECSSHIPINFO>
        <LOCATION_ID>1</LOCATION_ID>
        <SHIPFROM_DETAILS>
            <PostalCode>$fromPostalCode</PostalCode>
            <CountryCode>$fromCountry</CountryCode>
        </SHIPFROM_DETAILS>
        <SHIPTO_DETAILS>
            <Company></Company>
            <Contact></Contact>
            <StreetLines></StreetLines>
            <StreetLines1></StreetLines1>
            <StreetLines2></StreetLines2>
            <StreetLines3></StreetLines3>
            <City>$toCity</City>
            <StateOrProvinceCode>$toState</StateOrProvinceCode>
            <PostalCode>$toPostalCode</PostalCode>
            <CountryCode>$toCountry</CountryCode>
        </SHIPTO_DETAILS>
        <PACK_INFO>
            <PACKAGE>
                <WeightValue>$totalWeight</WeightValue>
                <WeightUnits>LBS</WeightUnits>
                <Length>$cubedVolume</Length>
                <Width>$cubedVolume</Width>
                <Height>$cubedVolume</Height>
                <DimensionUnit>IN</DimensionUnit>
            </PACKAGE>
        </PACK_INFO>
    </ECSSHIPINFO>
</RateAPIRequest>
XML;
    }

    /**
     * @param SimpleXMLElement $xml
     * @return array
     */
    private function parseXMLResponse (SimpleXMLElement $xml): array {
        $rates = [];
        if (!isset($xml->RateShop)) return $rates;
        foreach ($xml->RateShop->Rate as $Rate) {
            $rates[] = [
                'carrier_name' => (string) $Rate->Carrier,
                'api_code' => (string) $Rate->CarrierServiceCode,
                'carrier_desc' => (string) $Rate->Service,
                'rate' => floatval((string) $Rate->PublishedRate),
                'discounted_rate' => floatval((string) $Rate->DiscountedRate),
                'delivery_datetime' => (string) $Rate->EstmatedTime,
                'rate_type' => isset($Rate->RateType) ? (string) $Rate->RateType : null,
            ];
        }
        return $this->getValidRates($rates);
    }

    /**
     * @param $rates
     * @return array
     */
    private function getValidRates ($rates): array {
        $ratesKeyedByCarrier = $this->keyRatesByCarrier($rates);
        $apiDetailsByCarrier = $this->getRateShopDetailsKeyedByCarrier();
        $validRates = [];
        foreach ($apiDetailsByCarrier as $apiCarrier => $detailsByResidential) {
            if (in_array($apiCarrier, array_keys($ratesKeyedByCarrier))) {
                foreach ($detailsByResidential as $isResidentialIndex => $rateShopApiDetails) {
                    foreach ($rateShopApiDetails as $rateShopApiDetail) {
                        $apiCode = $rateShopApiDetail->api_code;
                        if (in_array($apiCode, array_keys($ratesKeyedByCarrier[$apiCarrier]))) {
                            $deliveryMethodCode = $rateShopApiDetail->delivery_method;
                            $pwRate = $ratesKeyedByCarrier[$apiCarrier][$apiCode];
                            $pwRate['delivery_method'] = $deliveryMethodCode;

                            // Canadian shipping special-case: use Hilco discounted rate (plus 30%) instead of base
                            if (in_array($this->groupCode, [RateShopGroup::CA_GROUP_CODE,RateShopGroup::PUROLATOR_GROUP_CODE]) &&
                                in_array($deliveryMethodCode, ["F60","F70","IWG","IWO","PRG","PRX"]) &&
                                in_array($apiCarrier, ['FedEx','ICSCOURIER','PUROLATORINTSHIP'])) {
                                // NOTE: Hilco asked us to do this years ago, but in all seriousness, Process
                                // Weaver ought to be able to do that calculation before returning the published
                                // rate value to us.
                                $pwRate['rate'] = $pwRate['discounted_rate'] +
                                    ($pwRate['discounted_rate'] * round(30/100, 2));
                            }

                            // Just set D00/F00 so that we don't have to worry about checking for it later
                            if (!is_null($pwRate['rate'])) {
                                if ($pwRate['rate'] > 0) {
                                    $pwRate['delivery_term'] = DeliveryTerm::PREDEFINED_TERM;
                                } else if ($pwRate->publishedRate == 0) {
                                    // this is extremely unlikely to happen but just in case...
                                    $pwRate['delivery_term'] = DeliveryTerm::FREE_SHIPPING;
                                }
                            }

                            $validRates[$deliveryMethodCode] = $pwRate;
                        }
                    }
                }
            }
        }
        return $validRates;
    }

    /**
     * @param $rates
     * @return array
     */
    private function keyRatesByCarrier ($rates): array {
        $keyedByCarrier = [];
        $hilcoHandlingFee = config('processweaver.handling_fee');
        foreach ($rates as $pwRate) {
            if (isset($pwRate['rate_type']) && $pwRate['rate_type'] !== "CONTRACT_RATES") continue;
            $pwRate['rate'] += $hilcoHandlingFee;
            $pwRate['discounted_rate'] += $hilcoHandlingFee;
            $keyedByCarrier[$pwRate['carrier_name']][$pwRate['api_code']] = $pwRate;
        }
        return $keyedByCarrier;
    }

    /**
     * @return array
     */
    private function getRateShopDetailsKeyedByCarrier(): array {
        $detailsKeyedByCarrier = [];
        $groupCode = $this->groupCode;
        $rateShopAPIDetails = RateShopAPIDetails::whereExists(function ($where) use ($groupCode) {
            return $where->select()
                ->from("RateShopGroups")
                ->whereRaw("RateShopGroups.delivery_method = RateShopAPIDetails.delivery_method")
                ->where("RateShopGroups.group_code", '=', $groupCode)
                ->whereRaw("RateShopGroups.deleted_at = 0");
        })->whereNotNull('api_carrier')->whereNotNull('api_code')
            ->whereRaw('api_carrier != ""')->whereRaw('api_code != ""')
            ->groupBy("deliverymethod_id", "transit_code")
            ->get();
        foreach ($rateShopAPIDetails as $rateShopAPIDetail) {
            $carrierName = $rateShopAPIDetail->api_carrier;
            $index = $rateShopAPIDetail->residential ? 1 : 0;
            $detailsKeyedByCarrier[$carrierName][$index][] = $rateShopAPIDetail;
        }
        return $detailsKeyedByCarrier;
    }
}