<?php

namespace Botble\Shippo;

use Botble\Base\Facades\BaseHelper;
use Botble\Ecommerce\Enums\OrderStatusEnum;
use Botble\Ecommerce\Enums\ShippingStatusEnum;
use Botble\Ecommerce\Facades\EcommerceHelper;
use Botble\Ecommerce\Models\Shipment;
use Botble\Location\Facades\Location;
use Botble\Location\Models\City;
use Botble\Location\Models\State;
use Botble\Support\Services\Cache\Cache;
use Carbon\Carbon;
use Illuminate\Log\Logger;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Psr\Log\LoggerInterface;
use Shippo as GoShippo;
use Shippo_Error;
use Shippo_Rate;
use Shippo_Shipment;
use Shippo_Transaction;
use Throwable;

class Shippo
{
    protected ?string $liveApiToken;

    protected ?string $testApiToken;

    protected ?string $labelFileType;

    protected Cache $cache;

    protected array $packageTypes = [];

    protected array $serviceLevels = [];

    protected bool $sandbox = true;

    public const MAX_DESCRIPTION_LENGTH = 45;

    protected ?string $currency;

    protected array $statuses;

    protected array $contentTypes;

    protected LoggerInterface $logger;

    protected bool $useCache = true;

    protected bool $logging = true;

    protected bool $insurance;

    protected bool $signature;

    protected bool $validateAddress;

    protected string|array|null $distanceUnit;

    protected string|array|null $massUnit;

    protected string|array|null $defaultTariff;

    protected array $origin;

    public function __construct()
    {
        $this->liveApiToken = setting('shipping_shippo_production_key');
        $this->testApiToken = setting('shipping_shippo_test_key');
        $this->labelFileType = 'PDF';
        $this->sandbox = setting('shipping_shippo_sandbox', 1) == 1;

        if ($this->sandbox) {
            GoShippo::setApiKey($this->testApiToken);
        } else {
            GoShippo::setApiKey($this->liveApiToken);
        }

        $this->currency = get_application_currency()->title;

        $this->statuses = [
            'PRE_TRANSIT' => __('Shipping Label Created'),
            'TRANSIT' => __('In Transit'),
            'DELIVERED' => __('Delivered'),
            'RETURNED' => __('Returned to Sender'),
            'FAILURE' => __('Exception'),
            'UNKNOWN' => __('Shipping Label Created'),
        ];

        $this->contentTypes = [
            'MERCHANDISE' => __('Merchandise'),
            'DOCUMENTS' => __('Documents'),
            'GIFT' => __('Gift'),
            'RETURN_MERCHANDISE' => __('Returned Goods'),
            'HUMANITARIAN_DONATION' => __('Humanitarian Donation'),
            'OTHER' => __('Other'),
        ];

        $this->insurance = false;
        $this->signature = false;
        $this->validateAddress = false;
        $this->defaultTariff = '';
        $this->origin = $this->mergeAddress(EcommerceHelper::getOriginAddress());

        $this->distanceUnit = 'cm';
        $this->massUnit = 'g';

        $this->packageTypes = config('plugins.shippo.general.package_types', []);
        $this->serviceLevels = config('plugins.shippo.general.service_levels', []);

        $this->useCache = setting('shipping_shippo_cache_response', 1);

        $this->logging = setting('shipping_shippo_logging', 1);

        $this->cache = Cache::make(static::class);

        $this->logger = Log::channel('shippo');
    }

    public function getName(): string
    {
        return 'Shippo';
    }

    public function mergeAddress(array $address): array
    {
        return array_merge($address, [
            'street1' => Arr::get($address, 'address'),
            'street2' => Arr::get($address, 'address_2'),
            'zip' => Arr::get($address, 'zip_code'),
        ]);
    }

    public function validate(): array
    {
        $errors = [];

        $apiTokenName = trans('plugins/shippo::shippo.live_api_token');
        if ($this->sandbox) {
            $apiTokenName = trans('plugins/shippo::shippo.test_api_token');
        }

        if (! $this->getApiKey()) {
            $errors[] = trans('plugins/shippo::shippo.token_name_is_required', ['name' => $apiTokenName]);
        } elseif (! $this->validateActiveApiToken()) {
            $errors[] = trans('plugins/shippo::shippo.token_name_is_invalid', ['name' => $apiTokenName]);
        }

        return $errors;
    }

    protected function validateActiveApiToken(): bool
    {
        try {
            Shippo_Shipment::all([
                'object_created_lt' => Carbon::now()->toDateString(),
                'async' => false,
            ]);

            return true;
        } catch (Throwable $ex) {
            report($ex);

            $this->log([__LINE__, $ex->getMessage()]);

            return false;
        }
    }

    public function getRates(array $params, bool $suggest = true): array
    {
        $prepareParams = $this->getPrepareParams($params);

        $this->log([__LINE__, 'getRates: ' . BaseHelper::jsonEncodePrettify($prepareParams)]);

        $newResponse = $this->getCacheOrNewRates($prepareParams);

        if (! Arr::get($newResponse, 'shipment.rates', []) && $suggest && Arr::get($prepareParams, 'extra.COD', [])) {
            $suggestParams = $prepareParams;
            Arr::forget($suggestParams, 'extra.COD');

            $suggestResponse = $this->getCacheOrNewRates($suggestParams);
            if ($rates = Arr::get($suggestResponse, 'shipment.rates', [])) {
                foreach ($rates as &$rate) {
                    $rate['disabled'] = true;
                    $rate['error_message'] = __('Not available in COD payment option.');
                }

                Arr::set($newResponse, 'shipment.rates', $rates);
            }
        }

        return $newResponse;
    }

    public function getCacheOrNewRates(array $params): array
    {
        $cacheKey = $this->getCacheKey($params);
        $response = $this->getCacheValue($cacheKey);
        if (! $response) {
            if (! $params['address_from'] || ! $params['address_to']) {
                $this->log([__LINE__, 'Cannot detect address, ' . BaseHelper::jsonEncodePrettify($params)]);
            } else {
                $requestParams = $this->getRatesParams($params);
                $response = $this->createShipment($requestParams);
            }
        } else {
            $this->log([__LINE__, 'Found previously returned rates, so return them']);
        }

        if ($response) {
            $this->log([__LINE__, print_r($response, true)]);
        }

        return $this->getRatesResponse($response, $params);
    }

    public function createShipment(array $params)
    {
        $cacheKey = $this->getCacheKey($params);
        $response = $this->getCacheValue($cacheKey);
        if (! $response) {
            $params = array_merge([
                'async' => false,
                'mode' => $this->sandbox ? 'test' : 'production',
                'extra' => [
                    'is_return' => false,
                ],
            ], $params);

            try {
                $response = Shippo_Shipment::create($params)->__toArray(true);
                if (Arr::get($response, 'status') == 'SUCCESS') {
                    $rates = Arr::get($response, 'rates', []);

                    if ($rates) {
                        $rates = $this->ratesByCurrency($rates);

                        if (! $rates) {
                            // @phpstan-ignore-next-line
                            $ratesResponse = Shippo_Shipment::get_shipping_rates([
                                'id' => Arr::get($response, 'object_id'),
                                'currency' => $this->currency,
                            ])->__toArray(true);

                            $rates = $this->ratesByCurrency($ratesResponse['results']);
                        }

                        Arr::set($response, 'rates', $rates);
                    }

                    $this->log([__LINE__, 'Cache shipment for the future']);
                    $this->setCacheValue($cacheKey, $response);
                }
            } catch (Throwable $ex) {
                report($ex);
                $this->log([__LINE__, $ex->getMessage()]);
            }
        } else {
            $this->log([__LINE__, 'Found previously returned rates, so return them']);
        }

        return $response;
    }

    protected function ratesByCurrency(array $rates): array
    {
        $rates = collect($rates)
                ->filter(function ($rate) {
                    return in_array($this->currency, [$rate['currency'], $rate['currency_local']]);
                })
                ->toArray();

        $newRates = [];
        foreach ($rates as $key => $rate) {
            $newRates[$key] = $rate;

            if ($rate['currency'] == $this->currency) {
                $newRates[$key]['price'] = $rate['amount'];
            } elseif ($rate['currency_local'] == $this->currency) {
                $newRates[$key]['price'] = $rate['amount_local'];
            }
        }

        return $newRates;
    }

    public function log(array $logs): self
    {
        if ($this->logging) {
            /**
             * @var Logger $logger
             */
            $logger = $this->logger;
            $logger->debug($logs);
        }

        return $this;
    }

    public function getPrepareParams(array $inParams): array
    {
        $params['extra'] = Arr::get($inParams, 'extra', []);
        $params['address_from'] = $this->getRequestedOrigin($inParams);
        $params['address_from'] = $this->prepareAddress($params['address_from']);

        $params['address_to'] = [];
        if ($addressTo = Arr::get($inParams, 'address_to')) {
            $params['address_to'] = $this->prepareAddress($addressTo);
        }

        $params['parcels'] = $this->prepareParcelInfo($inParams);
        $params['items'] = Arr::get($inParams, 'items', []);

        return $params;
    }

    public function getCacheKey(array $params): string
    {
        $params['api'] = $this->getApiKey();

        $jsonData = json_encode($params);

        return md5($jsonData) . ($this->sandbox ? '_test' : '_production');
    }

    protected function getRatesParams(array $inParams): array
    {
        $params = [
            'async' => false,
            'mode' => $this->sandbox ? 'test' : 'production',
            'extra' => [
                'is_return' => false,
            ],
        ];

        if ($isReturn = Arr::get($inParams, 'extra.is_return')) {
            $params['extra']['is_return'] = (bool) $isReturn;
        }

        if ($orderId = Arr::get($inParams, 'extra.order_id')) {
            $params['extra']['reference_1'] = $orderId;
            $params['metadata'] = sprintf('Order %s', $orderId);
        }

        if ($orderToken = Arr::get($inParams, 'extra.order_token')) {
            $params['metadata'] = sprintf('Order Token %s', $orderToken);
        }

        if ($orderNumber = Arr::get($inParams, 'order_number')) {
            $params['extra']['reference_2'] = $orderNumber;
        }

        if ($this->isInsuranceRequested($inParams) && ! empty($inParams['value'])) {
            $params['extra']['insurance'] = [
                'amount' => $inParams['value'],
                'currency' => $this->currency,
            ];
        }

        if ($this->isSignatureRequested($inParams)) {
            $params['extra']['signature_confirmation'] = 'STANDARD';
        }

        if ($cod = Arr::get($inParams, 'extra.COD', [])) {
            $params['extra']['COD'] = $cod;
        }

        if ($addressFrom = Arr::get($inParams, 'address_from')) {
            $this->log([__LINE__, 'From Address: ' . BaseHelper::jsonEncodePrettify($addressFrom)]);
            $params['address_from'] = $this->getCachedAddress($addressFrom);
        }

        if ($addressTo = Arr::get($inParams, 'address_to')) {
            $this->log([__LINE__, 'To Address: ' . BaseHelper::jsonEncodePrettify($addressTo)]);
            $params['address_to'] = $this->getCachedAddress($addressTo);
        }

        $parcelsInfo = Arr::only($params, ['extra', 'metadata']) ?: [];
        $parcelsInfo['parcels'] = Arr::get($inParams, 'parcels') ?: [];

        $params['parcels'] = $this->getCachedParcelInfo($parcelsInfo);

        if (isset($inParams['address_from']['country'])
            && isset($inParams['address_to']['country'])
            && $inParams['address_from']['country'] != $inParams['address_to']['country']) {
            $params['customs_declaration'] = $this->getCachedCustomsInfo($inParams);
        }

        return $params;
    }

    protected function isInsuranceRequested(array $inParams): bool
    {
        $insurance = $this->insurance;
        if (isset($inParams['extra']['insurance'])) {
            $insurance = filter_var($inParams['extra']['insurance'], FILTER_VALIDATE_BOOLEAN);
        }

        return $insurance;
    }

    protected function isSignatureRequested(array $inParams): bool
    {
        $signature = $this->signature;
        if (isset($inParams['extra']['signature'])) {
            $signature = filter_var($inParams['extra']['signature'], FILTER_VALIDATE_BOOLEAN);
        }

        return $signature;
    }

    protected function getRequestedOrigin(array $inParams): array
    {
        $origin = Arr::get($inParams, 'origin');
        if (! $origin) {
            $origin = $this->origin;
        }

        return $this->mergeAddress($origin);
    }

    protected function getCachedParcelInfo(array $inParams)
    {
        $cacheKey = $this->getCacheKey($inParams);
        $parcelId = $this->getCacheValue($cacheKey);
        if (! empty($parcelId)) {
            $this->log([__LINE__, 'Found previous cached parcel ID: ' . $parcelId . ', so re-use it']);

            return $parcelId;
        }

        return $inParams['parcels'];
    }

    protected function prepareParcelInfo(array $inParams): array
    {
        $length = 0;
        $width = 0;
        $height = 0;

        foreach (Arr::get($inParams, 'items', []) as $item) {
            $_length = $item['length'] * $item['qty'];
            $_height = $item['height'] * $item['qty'];
            $length = max($length, $_length);
            $height = $height > $_height ? $length : $_height;
            $width += $item['wide'] * $item['qty'];
        }

        $width = ecommerce_convert_width_height($width);
        $length = ecommerce_convert_width_height($length);
        $height = ecommerce_convert_width_height($height);

        $weight = round(EcommerceHelper::validateOrderWeight(Arr::get($inParams, 'weight', 0)), 2);

        $weight = ecommerce_convert_weight($weight);

        $parcel = [
            'weight' => $weight ?: 200,
            'length' => round($length, 2) ?: 10,
            'width' => round($width, 2) ?: 10,
            'height' => round($height, 2) ?: 10,
            'distance_unit' => $this->distanceUnit,
            'mass_unit' => $this->massUnit,
        ];

        if (! empty($inParams['type']) && $inParams['type'] != 'parcel' && isset($this->packageTypes[$inParams['type']])) {
            $parcel['template'] = $inParams['type'];
        }

        return [$parcel];
    }

    public function getCacheValue($cacheKey)
    {
        if ($this->useCache) {
            return $this->cache->get($cacheKey);
        }

        return null;
    }

    public function setCacheValue($cacheKey, $value): bool
    {
        if ($cacheKey) {
            return $this->cache->put($cacheKey, $value);
        }

        return true;
    }

    protected function getCachedAddress($options)
    {
        $cacheKey = $this->getCacheKey($options);
        $addrId = $this->getCacheValue($cacheKey);
        if (! empty($addrId)) {
            $this->log([__LINE__, 'Found previous cached address ID: ' . $addrId . ', so re-use it']);

            return $addrId;
        }

        return $options;
    }

    protected function prepareAddress(array $options): array
    {
        $addr = $this->mergeAddress($options);

        $addr = $this->beforePrepareAddress($addr);

        $validator = Validator::make($addr, $this->getAddressFromValidationRules());

        if ($validator->fails()) {
            $this->log([__LINE__, 'Address is invalid ' . BaseHelper::jsonEncodePrettify($addr)]);

            $this->log([__LINE__, $validator->getMessageBag()->first()]);

            return [];
        }

        return $this->afterPrepareAddress($addr);
    }

    protected function beforePrepareAddress(array $addr): array
    {
        if (EcommerceHelper::loadCountriesStatesCitiesFromPluginLocation()) {
            $cityId = $addr['city'];
            if (! EcommerceHelper::useCityFieldAsTextField()) {
                if (! is_numeric($cityId)) {
                    $city = City::query()->where('name', $cityId)->first();
                    if ($city) {
                        $addr['city'] = $city->id;
                        $addr['state'] = $city->state->id;
                        $addr['country'] = $city->state->country->id;
                    }
                }
            } else {
                if (! is_numeric($addr['state'])) {
                    $state = State::query()->where('name', $addr['state'])->first();
                    if ($state) {
                        $addr['state'] = $state->id;
                        $addr['country'] = $state->country->id;
                    }
                }
            }
        }

        return $addr;
    }

    protected function afterPrepareAddress(array $addr): array
    {
        if (EcommerceHelper::loadCountriesStatesCitiesFromPluginLocation()) {
            $cityId = $addr['city'];
            if (! EcommerceHelper::useCityFieldAsTextField()) {
                $city = Location::getCityById($cityId);
                if ($city) {
                    $addr['city'] = $city->name;
                    $addr['state'] = $city->state->abbreviation ?: $city->state->name;
                    $addr['country'] = $city->state->country->code;
                }
            } else {
                $state = State::query()->find($addr['state']);

                if ($state) {
                    $addr['state'] = $state->abbreviation ?: $state->name;
                    $addr['country'] = $state->country->code;
                }
            }
        }

        return $addr;
    }

    protected function getAddressFromValidationRules(): array
    {
        return EcommerceHelper::getCustomerAddressValidationRules();
    }

    protected function getValidationErrors($addressField, $addressType): array
    {
        if (empty($addressField['validation_results']) || ! empty($addressField['validation_results']['is_valid'])) {
            return [];
        }

        $this->log([__LINE__, 'Address is invalid: ' . print_r($addressField['validation_results'], true)]);

        $validationErrors = [];

        foreach ($addressField['validation_results']['messages'] as $error) {
            $errorMessage = $this->getErrorMessage($error);
            $validationErrors[$addressType][] = $errorMessage;
        }

        return $validationErrors;
    }

    protected function getErrorMessage($error)
    {
        if (isset($error['object_id']) || isset($error['results']) || isset($error['tracking_number'])) {
            return '';
        }

        if (isset($error['__all__'])) {
            $error = $error['__all__'];
        }

        if (is_string($error)) {
            return $error;
        }

        if (isset($error['text'])) {
            return $error['text'];
        }

        $message = '';
        if (is_array($error)) {
            foreach ($error as $key => $val) {
                if (! empty($message)) {
                    $message .= "\n";
                }

                if (! is_numeric($key)) {
                    $message .= $key . ' -> ';
                }

                $message .= $this->getErrorMessage($val);
            }
        }

        return trim($message);
    }

    protected function getCachedCustomsInfo(array $inParams)
    {
        $customsInfo = $this->prepareCustomsInfo($inParams);

        $cacheKey = $this->getCacheKey($customsInfo);
        $customsInfoId = $this->getCacheValue($cacheKey);
        if (! empty($customsInfoId)) {
            $this->log([__LINE__, 'Found previous cached customs info ID: ' . $customsInfoId . ', so re-use it']);

            return $customsInfoId;
        }

        return $customsInfo;
    }

    protected function prepareCustomsInfo(array $inParams): array
    {
        $customsInfo = [
            'certify' => true,
            'non_delivery_option' => 'RETURN',
            'certify_signer' => trim(Arr::get($inParams, 'address_from.name') ?: Arr::get($inParams, 'address_from.company')) ?: 'Shipper',
            'contents_type' => 'MERCHANDISE',
        ];

        if (! empty($inParams['order_number'])) {
            $customsInfo['invoice'] = $inParams['order_number'];
        }

        if (! empty($inParams['contents']) && ! empty($this->contentTypes[$inParams['contents']])) {
            $customsInfo['contents_type'] = $inParams['contents'];
        }

        if (isset($inParams['description'])) {
            $customsInfo['contents_explanation'] = $inParams['description'];
        }

        $defaultOriginCountry = '';
        if (isset($inParams['address_from']['country'])) {
            $defaultOriginCountry = strtoupper($inParams['address_from']['country']);
        }

        if (! empty($inParams['items']) && is_array($inParams['items'])) {
            $customsInfo['items'] = $this->prepareCustomsItems($inParams['items'], $defaultOriginCountry);
        }

        $this->log([__LINE__, 'Customs Info: ' . print_r($customsInfo, true)]);

        return $customsInfo;
    }

    protected function prepareCustomsItems(array $itemsInParcel, $defaultOriginCountry): array
    {
        $customsItems = [];

        foreach ($itemsInParcel as $itemInParcel) {
            if (empty($itemInParcel['country'])) {
                $itemInParcel['country'] = $defaultOriginCountry;
            }

            $customsItem = $this->prepareCustomsItem($itemInParcel);
            if (! empty($customsItem)) {
                $customsItems[] = $customsItem;
            }
        }

        return $customsItems;
    }

    protected function prepareCustomsItem($itemInParcel): array
    {
        if (empty($itemInParcel['name']) ||
            ! isset($itemInParcel['weight']) ||
            empty($itemInParcel['qty']) ||
            ! isset($itemInParcel['price'])) {
            $this->log([__LINE__, 'Item is invalid, so skip it ' . print_r($itemInParcel, true)]);

            return [];
        }

        $value = $itemInParcel['price'] * $itemInParcel['qty'];

        $tariff = $this->defaultTariff;
        if (! empty($itemInParcel['tariff'])) {
            $tariff = $itemInParcel['tariff'];
        }

        $description = preg_replace('/[^\w\d\s]/', '?', utf8_decode($itemInParcel['name']));

        return [
            'description' => Str::limit($description, self::MAX_DESCRIPTION_LENGTH),
            'quantity' => $itemInParcel['qty'],
            'value_amount' => round($value, 3),
            'value_currency' => $this->currency,
            'net_weight' => round(EcommerceHelper::validateOrderWeight($itemInParcel['weight']), 3),
            'mass_unit' => $this->massUnit,
            'origin_country' => $itemInParcel['country'],
            'tariff_number' => $tariff,
        ];
    }

    protected function getShipmentResponse($response, array $params): array
    {
        if (empty($response['object_id'])) {
            $this->log([__LINE__, 'Shipment ID has not been found']);

            return [];
        }

        if ($addressFrom = Arr::get($response, 'address_from')) {
            $validationErrors = $this->getValidationErrors($addressFrom, 'origin');
            if (! empty($validationErrors)) {
                $response['address_from']['object_id'] = null;
            }
        }

        if ($addressTo = Arr::get($response, 'address_to')) {
            if ($this->validateAddress && empty($addressTo['is_complete'])) {
                $validationErrors['destination'][] = __('Address appears to be incomplete');

                $this->log([__LINE__, 'Address is incomplete']);
            }

            $validationErrors = $this->getValidationErrors($addressTo, 'destination');
            if (! empty($validationErrors)) {
                $response['address_to']['object_id'] = null;
            }
        }

        $shipmentId = $this->getShipmentId($response, $params);

        $rates = [];
        foreach (Arr::get($response, 'rates', []) as $rate) {
            $serviceId = $rate['servicelevel']['token'];
            $serviceName = $rate['servicelevel']['name'];

            if (Arr::has($this->serviceLevels, $serviceId)) {
                $serviceName = $this->serviceLevels[$serviceId];
            }

            $days = Arr::get($rate, 'days', Arr::get($rate, 'estimated_days', 0));
            if ($days) {
                $description = trans('plugins/shippo::shippo.estimated_days', ['day' => $days]);
            } else {
                $description = Arr::get($rate, 'duration_terms', '');
            }

            $rates[$serviceId] = [
                'id' => $rate['object_id'],
                'service' => $serviceId,
                'carrier' => $rate['provider'],
                'name' => $serviceName,
                'delivery_days' => $days,
                'description' => $description,
                'price' => Arr::get($rate, 'price'),
                'image' => $rate['provider_image_75'],
                'company_name' => $serviceName,
                'shipment_id' => $shipmentId,
            ];
        }

        $newResponse = [
            'shipment' => [
                'id' => $shipmentId,
                'rates' => $this->sortRates($rates),
            ],
        ];

        if (! empty($validationErrors)) {
            $newResponse['validation_errors'] = $validationErrors;
        }

        return $newResponse;
    }

    public function sortRates(array $rates): array
    {
        uasort($rates, function ($rate1, $rate2) {
            return $rate1['price'] > $rate2['price'] ? 1 : -1;
        });

        return $rates;
    }

    protected function getRatesResponse($response, array $params): array
    {
        $newResponse = $this->getShipmentResponse($response, $params);

        if (Arr::get($newResponse, 'shipment.id')) {
            $this->setShipmentCacheValues($response, $params);
        }

        return $newResponse;
    }

    protected function setShipmentCacheValues($response, array $params): void
    {
        if (($addressFrom = Arr::get($response, 'address_from')) && $this->isResponseObjectValid($addressFrom)) {
            $addrId = $addressFrom['object_id'];
            $this->log([__LINE__, 'Cache from address ID: ' . $addrId]);

            $cacheKey = $this->getCacheKey(Arr::get($params, 'address_from'));
            $this->setCacheValue($cacheKey, $addrId);
        }

        if (($addressTo = Arr::get($response, 'address_to')) && $this->isResponseObjectValid($addressTo)) {
            $addrId = $addressTo['object_id'];
            $this->log([__LINE__, 'Cache to address ID: ' . $addrId]);

            $cacheKey = $this->getCacheKey(Arr::get($params, 'address_to'));
            $this->setCacheValue($cacheKey, $addrId);
        }

        if (($parcel = Arr::get($response, 'parcels.0')) && $this->isResponseObjectValid($parcel)) {
            $parcelId = $parcel['object_id'];
            $this->log([__LINE__, 'Cache parcel ID: ' . $parcelId]);

            $cacheKey = $this->getCacheKey(Arr::get($params, 'parcels.0'));

            $this->setCacheValue($cacheKey, $parcelId);
        }
    }

    protected function isResponseObjectValid($object): bool
    {
        $isValid = false;

        if (! empty($object['object_id']) && ! empty($object['object_state']) && $object['object_state'] == 'VALID') {
            $isValid = true;
        }

        return $isValid;
    }

    protected function getShipmentId($response, array $params = []): string
    {
        $shipmentId = '';

        // 1. shipment id
        if (! empty($params['shipment_id'])) {
            $shipmentId = $params['shipment_id'];
        } elseif (! empty($response['shipment_id'])) {
            $shipmentId = $response['shipment_id'];
        } elseif (! empty($response['object_id'])) {
            $shipmentId = $response['object_id'];
        }

        return $shipmentId;
    }

    protected function getApiKey()
    {
        return $this->sandbox ? $this->testApiToken : $this->liveApiToken;
    }

    public function canCreateTransaction(Shipment $shipment): bool
    {
        $order = $shipment->order;
        if (
            $order
            && $order->id
            && $order->shipping_method->getValue() == SHIPPO_SHIPPING_METHOD_NAME
            && $order->status != OrderStatusEnum::CANCELED
            && ! in_array($shipment->status->getValue(), [
                ShippingStatusEnum::CANCELED,
                ShippingStatusEnum::DELIVERING,
                ShippingStatusEnum::DELIVERED,
                ShippingStatusEnum::NOT_DELIVERED,
            ])) {
            return true;
        }

        return false;
    }

    public function createTransaction(string $rateId): array
    {
        try {
            $transaction = Shippo_Transaction::create([
                'rate' => $rateId,
                'async' => false,
            ])->__toArray(true);

            $this->log([__LINE__, $transaction]);

            return $transaction;
        } catch (Throwable $ex) {
            report($ex);

            $this->log([__LINE__, $ex->getMessage()]);

            return [];
        }
    }

    public function retrieveRate(string $rateId)
    {
        $cacheKey = $this->getCacheKey(['function' => __FUNCTION__, 'rate_id' => $rateId]);
        $response = $this->getCacheValue($cacheKey);
        if (! $response) {
            // @phpstan-ignore-next-line
            $response = Shippo_Rate::retrieve($rateId)->__toArray(true);
            $this->setCacheValue($cacheKey, $response);
        }

        return $response;
    }

    public function retrieveShipment(string $shipmentId)
    {
        $cacheKey = $this->getCacheKey(['function' => __FUNCTION__, 'shipment_id' => $shipmentId]);
        $response = $this->getCacheValue($cacheKey);
        if (! $response) {
            try {
                // @phpstan-ignore-next-line
                $response = Shippo_Shipment::retrieve($shipmentId)->__toArray(true);
                $this->setCacheValue($cacheKey, $response);
            } catch (Shippo_Error $ex) {
                $response['status'] = 'ERROR';
                $response['message'] = $ex->getMessage();

                $this->log([__LINE__, $ex->getMessage()]);
            } catch (Throwable $ex) {
                $this->log([__LINE__, $ex->getMessage()]);
            }
        }

        return $response;
    }

    public function getRoutePrefixByFactor(): string
    {
        if (is_plugin_active('marketplace') && ! is_in_admin(true)) {
            return 'marketplace.vendor.orders.';
        }

        return 'ecommerce.shipments.';
    }
}
