<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonDHL\Classes;

use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonDHL\Classes\DhlServiceOptions\NoNeighbourDeliveryServiceOption;
use Shopware\Plugins\ViisonDHL\Classes\DhlServiceOptions\PreferredDeliveryServiceOption;
use Shopware\Plugins\ViisonShippingCommon\Classes\DailyClosingGenerator;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingDocument;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingLabelCreationResult;
use Shopware\Plugins\ViisonDHL\Util;
use Shopware\Plugins\ViisonShippingCommon\Classes\Types\LabelFile;
use Shopware\Plugins\ViisonShippingCommon\Components\CurrencyConverter;
use Shopware\Plugins\ViisonShippingCommon\Components\LabelPersister;

/**
 * Class DHLCommunication
 *
 * All main DHL communication is made via this class. That is, all requests from the RESTful interface
 * as well as from the backend modules are preprocessed and the DHL request itself is made by this class.
 * It provide three public methods for creating a new DHL label, fetching the label PDF URL of an existing label
 * and deleting an existing label from the DHL system.
 *
 * @package Shopware\Plugins\ViisonDHL\Classes
 */
class DHLCommunication extends DailyClosingGenerator
{
    /**
     * The DHL web service definition URL. It is used for both the test and the production environment.
     */
    private static $dhlWSDLUrl_v3_5_0 = 'https://cig.dhl.de/cig-wsdls/com/dpdhl/wsdl/geschaeftskundenversand-api/3.5.0/geschaeftskundenversand-api-3.5.0.wsdl';

    /**
     * The CIG authentication and end point to be used in the test environment.
     */
    private static $cigTestEndPoint = 'https://cig.dhl.de/services/sandbox/soap';

    /**
     * The CIG authentication and end point to be used in the production environment.
     */
    private static $cigProductionUser = 'pickware_dhl_api_3_0_1';
    private static $cigProductionPassword = 'dNbXgO62mYxmyJmH6HubKuHK7r6iK0';
    private static $cigProductionEndPoint = 'https://cig.dhl.de/services/production/soap';

    /**
     * @var Util $util
     */
    private $util;

    /**
     * @var LocalWSDLHandler $localWSDLHandler
     */
    private $localWSDLHandler;

    /**
     * @var ShippingDocumentTypeFactoryService
     */
    private $documentTypeFactory;

    public function __construct()
    {
        $this->util = Util::instance();
        $this->localWSDLHandler = new LocalWSDLHandler(
            'gkp',
            'geschaeftskundenversand-api-3.5.0.wsdl',
            'gkp',
            '3.5.0'
        );
        $this->documentTypeFactory = Shopware()->Container()->get('viison_dhl.shipping_document_type_factory');
    }

    /**
     * Creates a new DHL label and, if required, an export document. In debug mode, the sent request as well as the
     * full SOAP response is written to the log file.
     *
     * @param int $orderId The id of the order where to add a label or zero to create a free form label.
     * @param float $shipmentWeight The weight to use as the shipment's weight.
     * @param bool $useItemWeights A flag indicating whether to use the weight defined in each item or to calculate the item weights from the total weight. This parameter is only relevant for shipments containing an export document (e.g. DHL Paket international).
     * @param array $shippingDetails An optional array containing the receivers address data.
     * @param array $packageDimensions An optional array containing the dimensions of the package, for which a new label shall be created.
     * @param array $settings An optional array containing the settings for the new label.
     * @param array $extraSettings An optional array containing DHL specific data like the export information / cash on delivery data when creating a label.
     *              In case of batch the extraSettings have the keys but empty values, for mobile the array is empty.
     * @param array $exportDocumentItems An optional array containing the order items and quantities, which will be contained in the shipment.
     * @return ShippingLabelCreationResult The created documents (label, export document etc.) and the new tracking code.
     * @throws \Exception
     */
    public function createLabel($orderId, $shipmentWeight, $useItemWeights, $shippingDetails = null, $packageDimensions = null, $settings = null, $extraSettings = null, $exportDocumentItems = [])
    {
        // Check DHL configuration
        $this->util->hasValidConfig($orderId);

        $shopId = $this->util->originatingShop($orderId);
        $activeDefaultShopId = Shopware()->Models()->getRepository('Shopware\\Models\\Shop\\Shop')->getActiveDefault()->getId();
        if ($orderId != 0) {
            $order = $this->util->getOrder($orderId);
            $this->util->checkIfLabelCreationIsPossible($order, $orderId);
            /** @var Order $orderFromDb */
            $orderFromDb = Shopware()->Models()->find('Shopware\\Models\\Order\\Order', $orderId);
            $isCustomerContactDataTransferAllowed = $this->isCustomerMailTransferAllowed($orderFromDb);
        } else {
            // Initialize a pseudo order when creating a free form label
            $order = [
                'orderDate' => date('Y-m-d'), // just use today
                'shippingCosts' => 0,// assume no shipping costs TODO it would be better to allow the user to enter this
            ];
            $gdprMailConfiguration = $this->util->config($activeDefaultShopId, 'gdprMailConfiguration');
            $isCustomerContactDataTransferAllowed = $gdprMailConfiguration !== ShippingProvider::GDPR_NEVER;
        }

        // Clear email because we are not allowed to transmit it
        if (!$isCustomerContactDataTransferAllowed) {
            // only set email/phone to empty string here if shippingDetails exists and isn't empty.
            // Else it breaks batch label creation and mobile.
            if ($shippingDetails !== null && count($shippingDetails) !== 0) {
                $shippingDetails['email'] = '';
            }
            $order['email'] = '';
        }

        // Clear phone depending on the sendPhoneConfiguration configuration of the adapter
        $sendPhone = $this->util->config(
            ($orderId != 0) ? $shopId : $activeDefaultShopId,
            'gdprPhoneConfiguration'
        );
        if ($sendPhone === ShippingProvider::GDPR_NEVER) {
            if ($shippingDetails !== null && count($shippingDetails) !== 0) {
                $shippingDetails['phone'] = '';
            }
            $order['phone'] = '';
        }

        // Store Cash On Delivery parameters in order if available
        if (!empty($extraSettings) && !empty($extraSettings['amount'])) {
            $order['amount'] = $extraSettings['amount'];
            $order['currency'] = $extraSettings['currency'];
        }

        $dispatchMethodLevelSettings = $this->util->getDispatchMethodLevelSettings($orderId);
        $createExportDocument = $dispatchMethodLevelSettings['exportDocumentRequired'];

        if (!empty($settings) && array_key_exists('product', $settings)) {
            // Use the specific DHL product that has been chosen as part of the settings
            $product = $this->util->getProductFromId($shopId, $settings['product']);
            if (empty($product)) {
                // Product not found
                $this->util->log('No product with id ' . $settings['product'] . 'found');
                throw new \Exception('Das gewählte DHL Produkt existiert nicht. Bitte lesen Sie gegebenenfalls in der DHL Adapter Dokumentation nach und überprüfen Sie die Versandart der Bestellung auf ein korrekt zugewiesenes DHL Produkt.');
            }
            $createExportDocument = $settings['createexportdocument'];
        } else {
            // Find DHL product using the shipment method of the order
            $product = $this->util->getProduct($orderId);
            if (empty($product)) {
                // No product found
                $this->util->log('No valid DHL product found for order with id ' . $orderId);
                throw new \Exception('Kein gültiges DHL Produkt gefunden. Bitte lesen Sie gegebenenfalls in der DHL Adapter Dokumentation nach und überprüfen Sie die Versandart der Bestellung auf ein korrekt zugewiesenes DHL Produkt.');
            }
        }

        if ($extraSettings['incoterm']) {
            $order['termsOfTrade'] = $extraSettings['incoterm'];
        } elseif ($dispatchMethodLevelSettings['incoterm']) {
            $order['termsOfTrade'] = $dispatchMethodLevelSettings['incoterm'];
        } else {
            $order['termsOfTrade'] = $this->util->config($shopId, 'exportIncoterm');
        }

        if (!$isCustomerContactDataTransferAllowed && $product['requiresEmail']) {
            // In case the customer or the shop admin did not allow to redirect any customer email or phone number to
            // DHL, the email is replaced by a valid but not working email address. That allows the product "DHL Austria
            // Connect" to be still used, even though is requires an email address.
            if ($shippingDetails !== null && count($shippingDetails) !== 0) {
                $shippingDetails['email'] = 'dummy@dummy.invalid';
            }
            $order['email'] = 'dummy@dummy.invalid';
        }

        if ($product['packageDimensionsRequired'] && $this->isPackageDimensionsEmpty($packageDimensions)) {
            $shopId = ($orderId !== 0) ? $this->util->originatingShop($orderId) : $activeDefaultShopId;
            $packageDimensions = $this->util->getDefaultPackageDimensionsForShopId($shopId);
        }

        if ($product['productCode'] == 'EXP') {
            // Express only shows one decimal place because of this the minimum ist 0,1kg
            if ($shipmentWeight < 0.1) {
                $shipmentWeight = 0.1;
                // The item weights cannot be used in the export document since the overall weight was adjusted
                $useItemWeights = false;
            }
            if ($this->util->config($shopId, 'useTestingWebservice')) {
                $expressCommunication = DHLExpressCommunication::createForTestingEndPoint();
            } else {
                $expressCommunication = DHLExpressCommunication::createForProductionEndPoint();
            }

            return $expressCommunication->createLabel(
                $shopId,
                $order,
                $this->getSender($shopId),
                $product,
                $orderId,
                $shipmentWeight,
                $useItemWeights,
                $shippingDetails,
                $packageDimensions,
                $settings,
                $extraSettings,
                $exportDocumentItems
            );
        } else {
            // The minimum weight for all DHL products is 0,01kg
            if ($shipmentWeight < 0.01) {
                $shipmentWeight = 0.01;
                // The item weights cannot be used in the export document since the overall weight was adjusted
                $useItemWeights = false;
            }
            $printOnlyIfCodeable = $extraSettings['ignorePrintOnlyIfCodeablePreset'] ? false : (bool) $this->util->config($shopId, 'printOnlyIfCodeable');

            // Create request object
            $request = new CreateShipmentOrderRequest($order, $this->getSender($shopId), $this->util->config($shopId, 'EKP'), $product, $product['partnerId'], $shipmentWeight, $packageDimensions, $printOnlyIfCodeable, $shopId);
            if (!empty($shippingDetails)) {
                // Set custom shipping values
                $request->setReceiver($shippingDetails, $orderId);
            } else {
                // Use the shipping address of the order
                $request->setReceiver($order, $orderId);
            }

            // Just disable temporally, later use $this->util->hasCashOnDeliveryPaymentMeans($orderId)
            $doesCashOnDeliveryLabelAlreadyExist = false;
            if ($orderFromDb) {
                $doesCashOnDeliveryLabelAlreadyExist = $orderFromDb->getAttribute()->getViisonCashOnDeliveryShipmentIdentifier() !== null;
            }

            $gotCashOnDeliveryParam = !empty($settings) && array_key_exists('cashondelivery', $settings);
            $useCashOnDeliveryService = ($gotCashOnDeliveryParam && $settings['cashondelivery']) || (!$gotCashOnDeliveryParam && !$doesCashOnDeliveryLabelAlreadyExist && $this->util->hasCashOnDeliveryPaymentMeans($orderId));
            if ($useCashOnDeliveryService) {
                // Add 'cash on delivery' to the request
                $request->setCashOnDelivery($order['amount'], $order['currency'], $this->getBankInformation($shopId, $orderId));
            }

            $gotPersonalHandoverParam = !empty($settings) && array_key_exists('personalhandover', $settings);
            if ($gotPersonalHandoverParam && $settings['personalhandover'] || (!$gotPersonalHandoverParam && $dispatchMethodLevelSettings && $dispatchMethodLevelSettings['personalHandover'])) {
                $request->setPersonalHandover();
            }
            $gotParcelOutletRoutingParam = !empty($settings) && array_key_exists('parceloutletrouting', $settings);
            $isParcelOutletRoutingOptionActive = $gotParcelOutletRoutingParam ? $settings['parceloutletrouting'] : ($dispatchMethodLevelSettings && $dispatchMethodLevelSettings['parcelOutletRouting']);
            if ($isParcelOutletRoutingOptionActive) {
                if (!$isCustomerContactDataTransferAllowed) {
                    throw new DHLException('Parcel outlet routing is not possible because the customer e-mail address is not allowed to be shared with DHL.', '7', 'parcelOutletRoutingEmailNotShared');
                }
                $request->enableParcelOutletRouting($order['email']);
            }

            $gotPostalDeliveryDutyPaidParam = !empty($settings) && array_key_exists('postalDeliveryDutyPaid', $settings);
            $isPostalDeliveryDutyPaidOptionActive = $gotPostalDeliveryDutyPaidParam ? $settings['postalDeliveryDutyPaid'] : ($dispatchMethodLevelSettings && $dispatchMethodLevelSettings['postalDeliveryDutyPaid']);
            if ($isPostalDeliveryDutyPaidOptionActive) {
                $request->enablePostalDeliveryDutyPaid();
            }

            $gotClosestDroppointDeliveryParam = !empty($settings) && array_key_exists('closestDroppointDelivery', $settings);
            $isClosestDroppointDeliveryOptionActive = $gotClosestDroppointDeliveryParam ? $settings['closestDroppointDelivery'] : ($dispatchMethodLevelSettings && $dispatchMethodLevelSettings['closestDroppointDelivery']);
            if ($isClosestDroppointDeliveryOptionActive) {
                if (!$isCustomerContactDataTransferAllowed) {
                    throw new DHLException('Closest Droppoint Delivery is not possible because the customer contact data is not allowed to be shared with DHL.', '7', 'closestDroppointDeliveryContactDataNotShared');
                }
                $request->enableClosestDroppointDelivery();
            }

            $gotSignedForByRecipientParam = !empty($settings) && array_key_exists('signedForByRecipient', $settings);
            $isSignedForByRecipientOptionActive = $gotSignedForByRecipientParam ? $settings['signedForByRecipient'] : ($dispatchMethodLevelSettings && $dispatchMethodLevelSettings['signedForByRecipient']);
            if ($isSignedForByRecipientOptionActive) {
                $request->enableSignedForByRecipient();
            }

            if ($extraSettings['endorsementType']) {
                $request->enableEndorsement($extraSettings['endorsementType']);
            } elseif ($dispatchMethodLevelSettings['endorsementType']) {
                $request->enableEndorsement($dispatchMethodLevelSettings['endorsementType']);
            }

            $defaultMinimumAge = null;
            if ($dispatchMethodLevelSettings) {
                $defaultMinimumAge = $dispatchMethodLevelSettings['visualAgeCheck'];
            }

            $gotMinimumAgeParam = !empty($settings) && array_key_exists('minimumage', $settings);
            if ($gotMinimumAgeParam && $settings['minimumage']) {
                $request->setMinimumAge($settings['minimumage']);
            } elseif (!$gotMinimumAgeParam && $defaultMinimumAge !== null) {
                $request->setMinimumAge($defaultMinimumAge);
            }

            $defaultIdentCheckAge = null;
            if ($dispatchMethodLevelSettings) {
                $defaultIdentCheckAge = $dispatchMethodLevelSettings['identCheckAge'];
            }

            $gotIdentCheckParam = !empty($settings) && array_key_exists('identCheckAge', $settings);
            if (!empty($shippingDetails)) {
                $identCheckData = [
                    'surname' => $shippingDetails['lastname'],
                    'givenname' => $shippingDetails['firstname'],
                ];
            } else {
                $identCheckData = [
                    'surname' => $order['lastname'],
                    'givenname' => $order['firstname'],
                ];
            }

            if ($gotIdentCheckParam && $settings['identCheckAge']) {
                $request->setIdentCheck($settings['identCheckAge'], $identCheckData);
            } elseif (!$gotIdentCheckParam && $defaultIdentCheckAge !== null) {
                $request->setIdentCheck($defaultIdentCheckAge, $identCheckData);
            }
            if (!empty($shippingDetails)) {
                // Update email address
                $order['email'] = $shippingDetails['email'];
                // First and last name required for dispatch email notifications
                $order['firstname'] = $shippingDetails['firstname'];
                $order['lastname'] = $shippingDetails['lastname'];
            }
            $dispatchNotificationSender = $this->util->config($shopId, 'dispatchNotificationSender');
            if ($this->util->config($shopId, 'sendDispatchNotification') && !empty($dispatchNotificationSender) && !empty($order['email'])) {
                // Add information for dispatch notification
                $request->setDispatchEmailNotification($order['email']);
            }
            if ($this->util->config($shopId, 'sendDeliveryNotification') && !empty($order['email'])) {
                // Add information for delivery notification
                $request->setDeliveryEmailNotification($order['email']);
            }
            if ($product['requiresEmail']) {
                // Some product require the email to be set for the delivery notification
                $request->setDeliveryEmailNotification($order['email']);
            }
            if ($createExportDocument) {
                if ($this->util->config($shopId, 'exportDescription') == ''
                    || in_array($order['termsOfTrade'], ['-', ''])
                    || $this->util->config($shopId, 'exportDocumentTypeDescription') == '') {
                    throw new \Exception('Bitte hinterlegen Sie in der Plugin-Konfiguration die Einstellungen zu Exportdokumenten (Inhaltsbeschreibung für Export, Incoterm, Allgemeine Export Beschreibung).');
                }

                if ($orderId != 0) {
                    $countryIso = empty($shippingDetails) ? $order['countryiso'] : $shippingDetails['countryiso'];
                    $languageId = null;
                    // Use English translation for export documents for shipments to non-german speaking countries
                    if (!in_array($countryIso, ['AT', 'CH', 'DE'])) {
                        $languageId = Shopware()->Models()->getRepository('Shopware\\Models\\Shop\\Locale')->findOneBy(['locale' => 'en_GB'])->getId();
                    }

                    // Include the export document
                    $exportInfo = Shopware()->Container()->get('viison_dhl.export_document')->getExportDocumentInformation($orderId, $shopId, $exportDocumentItems, $languageId);
                    $exportInfo['termsOfTrade'] = $order['termsOfTrade'];
                    $exportInfo['addresseesCustomsReference'] = $extraSettings['addresseesCustomsReference'];
                } else {
                    // When creating a free form label, use the export information given in $freeFormLabelExportInfo
                    $items = [[
                        'name' => $extraSettings['itemName'],
                        'quantity' => 1,
                        'price' => $extraSettings['amount'],
                        'currency' => $extraSettings['currency'],
                        'customsTariffNumber' => '',
                        'countryOfOriginCode' => null,
                    ],
                    ];
                    $exportInfo = [
                        'typeOfShipment' => $this->util->config($shopId, 'exportTypeOfShipment'),
                        'typeDescription' => $this->util->config($shopId, 'exportDocumentTypeDescription'),
                        'termsOfTrade' => $order['termsOfTrade'],
                        'description' => $this->util->config($shopId, 'exportDescription'),
                        'placeOfCommittal' => $this->util->config($shopId, 'exportPlaceOfCommittal'),
                        'invoice' => null,
                        'vatNumber' => Shopware()->Config()->taxNumber,
                        'totalNumberOfItems' => 1,
                        'items' => $items,
                        'currency' => $extraSettings['currency'],
                        'sendersCustomsReference' => $this->util->config($shopId, 'sendersCustomsReference'),
                        'addresseesCustomsReference' => $extraSettings['addresseesCustomsReference'],
                    ];
                }
                $totalWeight = (!$useItemWeights) ? $shipmentWeight : null;
                $request->setExportDocument($order, $exportInfo, $totalWeight);

                if ($exportInfo['currency'] !== 'EUR') {
                    // DHL does not support any other currency than EUR for export documents
                    // so we need to convert the positions to EUR.
                    /** @var CurrencyConverter $currencyConverter */
                    $currencyConverter = Shopware()->Container()->get('viison_shipping_common.currency_converter');
                    $currencyConversionFactor = $currencyConverter->getConversionFactor($exportInfo['currency'], 'EUR');
                    $request->multiplyMoneyValuesWithFactor($currencyConversionFactor);
                }

                $request->encodeExportDocumentPositionsAsSoapVars();
            }

            if ($product['product'] === 'V54EPAK') {
                if (!empty($extraSettings)) {
                    $frankatur = $extraSettings['frankatur'];
                } else {
                    $frankatur = $dispatchMethodLevelSettings['frankatur'];
                }

                $items = [
                    [
                        'name' => 'Placeholder',
                        'quantity' => 1,
                        'price' => 0,
                        'currency' => 'EUR',
                        'customsTariffNumber' => '',
                        'countryOfOriginCode' => null,
                    ],
                ];
                $exportInfo = [
                    'typeOfShipment' => $this->util->config($shopId, 'exportTypeOfShipment'),
                    'typeDescription' => $this->util->config($shopId, 'exportDocumentTypeDescription'),
                    'termsOfTrade' => $frankatur,
                    'description' => 'Placeholder',
                    'placeOfCommittal' => 'Placeholder',
                    'invoice' => null,
                    'vatNumber' => null,
                    'totalNumberOfItems' => 1,
                    'items' => $items,
                    'currency' => 'EUR',
                ];
                $totalWeight = (!$useItemWeights) ? $shipmentWeight : null;
                $request->setExportDocument($order, $exportInfo, $totalWeight);
                $request->encodeExportDocumentPositionsAsSoapVars();
            }

            /** @var \Shopware\Models\Attribute\Order $orderAttribute */
            $orderAttributes = $this->util->getOrderAttributeIfPackingStationPluginExist($orderId);
            $serviceOptions = [];
            // Only the single label flow has non empty values inside $extrasSettings in case of batch the extraSettings
            // have the keys but empty values and for mobile the array is empty.
            $isSingleLabelFlow = $extraSettings && count(array_filter($extraSettings));
            if ($isSingleLabelFlow) {
                // Single label flow.
                $serviceOptions[] = PreferredDeliveryServiceOption::createFromExtraSettings($extraSettings);
                $serviceOptions[] = NoNeighbourDeliveryServiceOption::createFromExtraSettings($extraSettings);
            } elseif ($orderAttributes) {
                // Batch label or mobile flow.
                $serviceOptions[] = PreferredDeliveryServiceOption::createFromOrderAttributes($orderAttributes);
            }
            foreach ($serviceOptions as $serviceOption) {
                $request->addServiceOption($serviceOption);
            }

            // Add additional product services
            $this->addAdditionalService($request, $product);

            // Track the start time of the communication to limit the time to retry any failed requests
            $startTime = time();
            $success = false;
            do {
                try {
                    // TODO Refactor all with new ShippingCommon!!!!! This logic shouldn't be part of this method!!!
                    $self = $this;
                    $response = $this->sendSoapRequest(
                        'createShipmentOrder',
                        $shopId,
                        $request,
                        function ($response, &$errorCode, $responseXML) use ($self) {
                            $errorMessage = '';
                            $statusText = $response->CreationState->LabelData->Status->statusText ?: $response->Status->statusText;
                            $statusMessages = $response->CreationState->LabelData->Status->statusMessage ?: $response->Status->statusText;

                            // A single statusMessage is represented as string by default
                            if (!is_array($statusMessages)) {
                                $statusMessages = [$statusMessages];
                            }

                            $statusMessagesAsString = implode(' ', array_filter($statusMessages));

                            // Check if the error message has already been translated to a custom message
                            $snippet = DHLApiMessageToSnippetTranslator::getSnippetNameForMessage($statusMessagesAsString);

                            if ($snippet) {
                                $errorCode = DHLApiMessageToSnippetTranslator::getTranslatedErrorCodeForSnippetIfExist(
                                    $snippet
                                );

                                throw new DHLException($statusMessagesAsString, $errorCode, $snippet);
                            }

                            if (in_array('Das angegebene Produkt ist für das Land nicht verfügbar.', $statusMessages, true)) {
                                $status = $response->status ?: $response->Status;
                                $statusCode = $status->statusCode ?: $response->CreationState->StatusCode;

                                return $statusCode . ': Das angegebene Produkt ist für das Land nicht verfügbar.';
                            }

                            // Packing station is not codable
                            if ($this->isPackingStationNonCodable($statusText, $statusMessages)) {
                                throw NonCodableAddressDHLException::packingStationNonCodable();
                            }

                            // Address is not codable
                            if ($this->isAddressNonCodable($statusText, $statusMessages)) {
                                throw NonCodableAddressDHLException::addressNonCodeable();
                            }

                            if ($statusText === 'Hard validation error occured.'
                                && in_array('Bitte geben Sie die Art der Sendung an.', $statusMessages, true)
                            ) {
                                return 'Exportdokument fehlt. Bei Sendungen in Nicht-EU Länder muss die Option "Exportdokument erstellen" gewählt werden.';
                            }

                            if (in_array('Bitte geben Sie die Beschreibung an.', $statusMessages, true)) {
                                $exportDocPositionInvalidMessage = 'Mindestens eine Bestellposition hat weder einen ' .
                                    'Artikelnamen noch eine Artikelnummer. Bitte ergänzen Sie diese, da eine ' .
                                    'Artikelbeschreibung für das Exportdokument zwingend erforderlich ist.';

                                return $exportDocPositionInvalidMessage;
                            }

                            $statusMessagesUniqueAsString = implode(' ', array_unique($statusMessages));

                            $status = $response->status ?: $response->Status;
                            $errorMessage .= ($status->statusCode ?: $response->CreationState->StatusCode) . ': ';
                            $creationMessage = $statusMessagesUniqueAsString;
                            if (!empty($creationMessage)) {
                                $errorMessage .= DHLCommunication::handleErrorMessage($creationMessage);
                            } else {
                                $errorMessage .= $status->statusMessage;
                            }

                            return $errorMessage;
                        }
                    );
                    $success = true;
                } catch (\Exception $e) {
                    // Only retry if the error was a connection error
                    if (mb_strpos($e->getMessage(), 'Could not connect to host') === false) {
                        throw $e;
                    }
                    // If the retries exceed 10 seconds, break the loop by rethrowing the excpetion.
                    // Retry the request by continuing the loop otherwise.
                    if (time() - $startTime > 10) {
                        throw $e;
                    }
                }
            } while (!$success);
        }

        // Save tracking code
        $newTrackingCode = $response->CreationState->shipmentNumber;
        $shippingUtil = new ShippingUtil();

        // Add the identifiers of all created documents
        $documents = [];

        $shippingLabelType = $this->documentTypeFactory->createLabelDocumentType();
        $documents[] = new ShippingDocument(
            ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL,
            $newTrackingCode,
            $shippingLabelType->getPageSizeName(),
            $shippingLabelType->getId()
        );

        $pdf = base64_decode($response->CreationState->LabelData->labelData);
        $documentFileName = $this->util->getDocumentFileName($newTrackingCode, ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL);

        $dhlLabelFile = new LabelFile($pdf, $documentFileName, $newTrackingCode);

        /** @var LabelPersister $dhlLabelPersister */
        $dhlLabelPersister = Shopware()->Container()->get('viison_dhl.label_persister');
        $dhlLabelPersister->persistLabelAndArchiveOldLabel($dhlLabelFile);

        $trackingCodes = $shippingUtil->saveTrackingCode(
            $orderId,
            ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL,
            $newTrackingCode,
            $shippingDetails,
            $product['productId'],
            $shipmentWeight
        );
        $result = new ShippingLabelCreationResult($newTrackingCode, $trackingCodes);
        $result->setIsCashOnDeliveryShipment($useCashOnDeliveryService);

        // For EU shipments there is no export document
        if ($createExportDocument && $response->CreationState->LabelData->exportLabelData) {
            $pdf = base64_decode($response->CreationState->LabelData->exportLabelData);
            $documentFileName = $this->util->getDocumentFileName($newTrackingCode, ShippingUtil::DOCUMENT_TYPE_EXPORT_DOCUMENT);

            $dhlExportLabelFile = new LabelFile($pdf, $documentFileName, $newTrackingCode);
            $dhlLabelPersister->persistLabelAndArchiveOldLabel($dhlExportLabelFile);

            // Add the document to the result
            $exportDocumentType = $this->documentTypeFactory->createExportDocumentType();
            $documents[] = new ShippingDocument(
                ShippingUtil::DOCUMENT_TYPE_EXPORT_DOCUMENT,
                $newTrackingCode,
                $exportDocumentType->getPageSizeName(),
                $exportDocumentType->getId()
            );

            // Save the export doc URL in the database
            Shopware()->Db()->query(
                'UPDATE s_order_viison_dhl
            SET exportDocumentUrl = ?
            WHERE trackingCode = ?',
                [
                    $this->util->createDocumentURL(
                        ShippingUtil::DOCUMENT_TYPE_EXPORT_DOCUMENT . ':' . $newTrackingCode
                    ),
                    $newTrackingCode,
                ]
            );
        }

        foreach ($documents as $document) {
            $result->addDocument($document);
        }

        // Throw a LabelCreated notify event that other plugins can listen to
        Shopware()->Events()->notify(
            'Shopware_ViisonShippingCommon_DispatchServiceProviderCommunication_CreateLabel_LabelCreated',
            [
                'subject' => $this,
                'orderId' => $orderId,
                'shipmentWeight' => $shipmentWeight,
                'useItemWeights' => $useItemWeights,
                'shippingDetails' => $shippingDetails,
                'packageDimensions' => $packageDimensions,
                'settings' => $settings,
                'extraSettings' => $extraSettings,
                'exportDocumentItems' => $exportDocumentItems,
                'product' => $product,
                'trackingCode' => $newTrackingCode,
                'plugin' => $this->util->getPluginInfo()->getPluginName(),
            ]
        );

        return $result;
    }

    /**
     * @param string $statusText
     * @param array $statusMessages
     * @return bool
     */
    private function isPackingStationNonCodable($statusText, $statusMessages)
    {
        if ($statusText !== 'Hard validation error occured.') {
            return false;
        }

        return in_array('Die Packstationsnummer ist uns aktuell nicht bekannt.', $statusMessages, true)
            && (
                in_array('Die angegebene Straße kann nicht gefunden werden.', $statusMessages, true)
                || in_array('Der angegebene Ort passt nicht zur Postleitzahl.', $statusMessages, true)
            );
    }

    /**
     * @param string $statusText
     * @param array $statusMessages
     * @return bool
     */
    private function isAddressNonCodable($statusText, $statusMessages)
    {
        if ($statusText !== 'Hard validation error occured.') {
            return false;
        }

        return in_array('In der Sendung trat mindestens ein harter Fehler auf.', $statusMessages, true)
            && (count($statusMessages) === 2 || count($statusMessages) === 3)
            && (
                in_array('Die angegebene Straße kann nicht gefunden werden.', $statusMessages, true)
                || in_array('Der angegebene Ort passt nicht zur Postleitzahl.', $statusMessages, true)
                || in_array('Der Ort ist zu dieser PLZ nicht bekannt.', $statusMessages, true)
                || in_array('Die Postleitzahl konnte nicht gefunden werden.', $statusMessages, true)
            );
    }

    /**
     * This action requests a deletion from DHL of the shipment corresponding to
     * the given tracking code.
     *
     * @param string $trackingCode The DHL tracking code whose corresponding shipment should be deleted.
     * @throws \Exception, if the request fails and DHL errors are not ignored.
     * @return void
     */
    public function deleteLabel($trackingCode)
    {
        // Determine the id of the shop, in which the order associated with the tracking code was made
        $shopId = $this->util->originatingShopOfTrackingCode($trackingCode);

        $useExpressAccount = Util::instance()->isTrackingCodeExpressShipment($trackingCode);

        if ($useExpressAccount) {
            if ($this->util->config($shopId, 'useTestingWebservice')) {
                $expressCommunication = DHLExpressCommunication::createForTestingEndPoint();
            } else {
                $expressCommunication = DHLExpressCommunication::createForProductionEndPoint();
            }

            $expressCommunication->deleteLabel($trackingCode);

            return;
        }

        // Create request object
        $request = new DeleteShipmentOrderRequest($trackingCode);

        $this->sendSoapRequest('deleteShipmentOrder', $shopId, $request, function ($response) {
            $errorMessage = '';
            $deletionMessage = $response->DeletionState->Status->StatusMessage;
            if (!empty($deletionMessage)) {
                $errorMessage = $response->DeletionState->Status->StatusCode . ': ';
                $errorMessage .= DHLCommunication::handleErrorMessage($deletionMessage);
            }

            return $errorMessage;
        });
    }

    /**
     * Requests the manifest document for the date range defined by the given parameters and
     * and returns decoded raw data of the PDF file.
     *
     * @param int $shopId The id of the shop, whose configuration will be used to select the DHL login data.
     * @param string $fromDate The first day of the date range, whose shipments will be included in manifest.
     * @param string $toDate The last day of the date range, whose shipments will be included in manifest.
     * @return string The raw data of the manifest PDF file.
     * @throws \Exception If DHL returned an error status code.
     */
    public function getDailyClosing($shopId, $fromDate, $toDate)
    {
        // The DHL web service does not support a date range, but only a single date
        $date = $fromDate;

        // Create request object
        $request = new GetManifestRequest($date);

        // If a daily closing document for the current day is requested, the shipments have to be manifested first so
        // that they are included in the daily closing document.
        $today = new \DateTime();
        if ($date == $today->format('Y-m-d')) {
            $orderTable = new \Zend_Db_Table($this->util->getPluginInfo()->getOrderTableName());
            $orders = $orderTable->fetchAll($orderTable->select()->from($orderTable, 'trackingCode')->where('DATE(created) = CURDATE()'))->toArray();
            $orders = array_map(function ($order) {
                return $order['trackingCode'];
            }, $orders);
            $chunks = array_chunk($orders, 30);
            foreach ($chunks as $chunk) {
                $doManifestRequest = new DoManifestRequest($chunk);
                $this->sendSoapRequest('doManifest', $shopId, $doManifestRequest, function ($response) {
                    $manifestState = $response->ManifestState;
                    if (!is_array($manifestState)) {
                        $manifestState = [$manifestState];
                    }
                    // Filter out all successful and 'Unknown shipment number.' entries since these are also returned if the shipment has already been manifested
                    $manifestState = array_filter($manifestState, function ($manifestState) {
                        return $manifestState->Status->statusCode != 0 && $manifestState->Status->statusText != 'Unknown shipment number.';
                    });
                    // Mark the request as successful if no real error occurred
                    if (empty($manifestState)) {
                        return false;
                    }

                    return implode(', ', array_map(function ($manifestState) {
                        return $manifestState->Status->statusText . ': ' . $manifestState->Status->statusMessage;
                    }, $manifestState));
                });
            }

            // doManifest requests take some time to be processed, so we wait here for a fixed amount of time till we
            // hope they are done.
            if (count($chunks) > 0) {
                sleep(20);
            }
        }

        $response = $this->sendSoapRequest('getManifest', $shopId, $request);

        return $response->manifestData;
    }

    /**
     * Add additional dhl service (Optional once)
     *
     * @param CreateShipmentOrderRequest $request Request meant to be send to DHL.
     * @param array $product Product information.
     */
    private function addAdditionalService(CreateShipmentOrderRequest $request, $product)
    {
        if ($product['premiumOption']) {
            $request->addService('Premium');
        }

        if ($product['bulkfreightOption']) {
            $request->addService('BulkyGoods');
        }

        if (!empty($product['insuranceAmount'])) {
            $request->addService('AdditionalInsurance', [
                'insuranceAmount' => (int) $product['insuranceAmount'],
            ]);
        }
    }

    private function isPackageDimensionsEmpty($packageDimensions)
    {
        return $packageDimensions['length'] === null
            || $packageDimensions['width'] === null
            || $packageDimensions['height'] === null;
    }

    /**
     * Gathers all information about the shipment sender. That is, the values
     * preconfigured in this plugin's configuration.
     *
     * @param int $shopId The id of the shop from whose configuration the address shall be taken.
     * @return array An array containing all information set in this plugin's settings.
     */
    private function getSender($shopId)
    {
        $sender = [
            'companyName' => $this->util->config($shopId, 'senderName'),
            'companyName2' => $this->util->config($shopId, 'senderName2'),
            'streetName' => $this->util->config($shopId, 'streetName'),
            'streetNumber' => $this->util->config($shopId, 'streetNumber'),
            'city' => $this->util->config($shopId, 'city'),
            'zip' => $this->util->config($shopId, 'zipCode'),
            'countryISO' => $this->util->config($shopId, 'countryiso'),
            'contactPerson' => $this->util->config($shopId, 'contactPerson'),
            'email' => $this->util->config($shopId, 'email'),
            'phoneNumber' => $this->util->config($shopId, 'phoneNumber'),
        ];

        return $sender;
    }

    /**
     * Gathers all bank information configured for the shop with the given id
     * as well as the reason for payment based on the order data.
     *
     * @param int $shopId The id of the shop from whose configuration the address shall be taken.
     * @param int $orderId (optional) The id of the order whose data shall be used for the 'reason for payment'(note).
     * @return array An array containing the reason for payment and all bank information set in this plugin's settings.
     */
    private function getBankInformation($shopId, $orderId = null)
    {
        $bankAccountOwner = $this->util->config($shopId, 'bankAccountOwner');
        $bankName = $this->util->config($shopId, 'bankName');
        $iban = $this->util->config($shopId, 'bankIBAN');

        if (!$bankAccountOwner || !$bankName || !$iban) {
            // No valid configuration found
            $missingFieldsArray = [];

            array_push($missingFieldsArray, empty($bankAccountOwner) ? "'Kontoinhaber'" : '', empty($bankName) ? "'Name der Bank'" : '', empty($iban) ? "'IBAN'" : '');
            $errorMessage = !empty($missingFieldsArray) ? implode(', ', array_filter($missingFieldsArray)) : '';

            $this->util->log('Failed to use the service CashOnDelivery, because of missing params inside the DHL Configuration');
            throw new \Exception('Kontodaten für die Option "Nachnahme" sind nicht hinterlegt. Bitte füllen Sie die Felder: ' . $errorMessage . ' in Kunden -> DHL -> Konfiguration aus.');
        }

        // Gather basic information
        $bankInformation = [
            'accountOwner' => $bankAccountOwner,
            'bankName' => $bankName,
        ];

        // Check IBAN
        if (!empty($iban)) {
            // Add IBAN
            $bankInformation['iban'] = $iban;
        }

        if ($orderId === null) {
            return $bankInformation;
        }

        // Add reason for payment
        $orderInformation = Shopware()->Db()->fetchRow(
            'SELECT ordernumber, transactionID
             FROM s_order
             WHERE id = ?',
            [
                $orderId
            ]
        );
        if (empty($orderInformation)) {
            return $bankInformation;
        }

        $bankInformation['note1'] = 'Bestell-Nr. ' . $orderInformation['ordernumber'];

        if (!empty($orderInformation['transactionID'])) {
            $bankInformation['note2'] = 'Transaktions-ID: ' . $orderInformation['transactionID'];
        }

        return $bankInformation;
    }

    /**
     * Constructs and returns the two necessary SOAP headers.
     *
     * @param int $shopId The id of the shop from whose configuration the username and password shall be taken.
     * @return array An array containing the constructed SOAP headers.
     */
    private function createSoapHeaders($shopId)
    {
        if ($this->util->config($shopId, 'useTestingWebservice')) {
            $username = '2222222222_01';
            $password = 'pass';
        } else {
            // NOTE: DHL GKP API takes only a Username in lowercase format
            $username = mb_strtolower($this->util->config($shopId, 'username'));
            $password = $this->util->config($shopId, 'password');
        }

        $auth = [
            'user' => $username,
            'signature' => $password,
            'type' => 0,
        ];
        $authHeader = new \SoapHeader('http://dhl.de/webservice/cisbase', 'Authentification', $auth, false);

        return [
            $authHeader
        ];
    }

    /**
     * Returns the additional options used for SOAP requests. That is, the SOAP version
     * and the trace parameter based on the active mode (production/test).
     *
     * @param integer $shopId
     * @return array The SOAP options based on the configuration.
     */
    private function getOptions($shopId)
    {
        $options = [
            'soap_version' => SOAP_1_1,
            'cache_wsdl' => WSDL_CACHE_NONE,
        ];

        if ($this->util->config($shopId, 'useTestingWebservice')) {
            // Use the DHL developer portal login of the config
            $extraOptions = [
                'login' => $this->util->config($shopId, 'username'),
                'password' => $this->util->config($shopId, 'password'),
                'location' => self::$cigTestEndPoint,
                'trace' => true,
            ];
        } else {
            // Production settings
            $extraOptions = [
                'login' => self::$cigProductionUser,
                'password' => self::$cigProductionPassword,
                'location' => self::$cigProductionEndPoint,
                'trace' => true,
            ];
        }

        return array_merge($options, $extraOptions);
    }

    /**
     * Analyzes the given error message and combines all its parts, if it is an array.
     * Further more duplicate occurrences of 'Your order could not be processed' will be reduced.
     *
     * @param string|array $message The error message object.
     * @return string A string containing all error messages combined.
     */
    public static function handleErrorMessage($message)
    {
        if (is_array($message) && count($message) > 0) {
            $message = array_map(function ($element) {
                return trim(preg_replace('/(Your order could not be processed(\\s)?)+/', '$1', $element));
            }, $message);

            return implode(' -- ', $message);
        }
        if (is_array($message)) {
            return 'No message.';
        }

        return $message;
    }

    /**
     * Sends a SOAP request to the DHL webservice. If an error occurs, whose origin is well known,
     * a meaningful error message is added to the \Exception. This function also tries to handle SOAP timeouts
     * gracefully by relying on the appropriate faultcode and faultstring of the SoapFault object. Note that
     * this only works if the default_socket_timeout is set to a smaller value than the max_execution_time PHP
     * ini value (this is taken care of in the constructor of the bootstrap class).
     *
     * @param string $method The SOAP method to be called.
     * @param int $shopId The (sub-)shop ID,
     * @param SoapRequest $request The request data to be sent.
     * @param callable|null $getCustomErrorMessage A function that gathers request specific error information from the response that is to be displayed to the user. If the function returns false, the error gets ignored an no \Exception is thrown.
     * @return string
     * @throws \Exception
     */
    private function sendSoapRequest($method, $shopId, $request, $getCustomErrorMessage = null)
    {
        $status = null;

        try {
            $soapVersion = [
                'majorRelease' => 3,
                'minorRelease' => 5,
            ];

            $client = $this->localWSDLHandler->getSoapClient(self::$dhlWSDLUrl_v3_5_0, $this->getOptions($shopId), $this->createSoapHeaders($shopId));

            // Send request
            $response = $client->__soapCall($method, ['parameters' => $request->getBody($soapVersion)]);
            $responseXML = $client->__getLastResponse();
            Shopware()->Container()->get('viison_dhl.soap_logger')->logLastRequest($client);
        } catch (\SoapFault $e) {
            Shopware()->Container()->get('viison_dhl.soap_logger')->logFailedRequest($client, $e);
            // Ignore error caused by faulty DHL webservice implementation
            if ($e->getMessage() == "SOAP-ERROR: Encoding: Element 'minorRelease' has fixed value '1' (value '0' is not allowed)") {
                $fullResponseDOM = new DOMDocument('1.0', 'utf-8');
                $fullResponseDOM->loadXML($client->__getLastResponse());
                $fullResponseArray = self::xmlToArray($fullResponseDOM);
                $bodyArray = $fullResponseArray['Envelope']['Body'];
                foreach ($bodyArray as $key => $value) {
                    if (preg_match('/.*Response.*/', $key)) {
                        $response = json_decode(json_encode($value), false);
                    }
                }
                $responseXML = $client->__getLastResponse();
            } else {
                $this->util->log($method.' failed');
                $this->util->log($e->getMessage());

                $snippet = DHLApiMessageToSnippetTranslator::getSnippetNameForMessage($e->getMessage());
                if ($snippet !== null) {
                    throw new DHLException($e->getMessage(), $e->getCode(), $snippet);
                }

                throw new \Exception(('DHL Webservice-Anfrage fehlgeschlagen: ' . $e->getMessage()), 0, $e);
            }
        }

        if ($response === null && $method == 'GetExportDoc') {
            $fullResponseDOM = new DOMDocument('1.0', 'utf-8');
            if ($fullResponseDOM->loadXML($client->__getLastResponse())) {
                $xp = new DOMXPath($fullResponseDOM);
                $exportDocURL = $xp->query('//ExportDocURL')->item(0)->nodeValue;
                $response = [
                    'ExportDocData' => [
                        'ExportDocURL' => $exportDocURL,
                    ],
                ];

                return json_decode(json_encode($response));
            }
        }

        // TODO NOTE for the future, move error handling to a seperate error message class for Adapters
        // TODO so it can be overriden easly and be cleaner
        // For most methods, the status element is lower case, but in some, it is upper case
        $status = $response->status ? $response->status : $response->Status;

        // Check response. On some PHP 5.6 installations, the status code is a string instead of an integer
        if ($status->statusCode !== 0 && $status->statusCode !== '0') {
            // Log JSON encoded response
            $logMessage = json_encode($response, JSON_UNESCAPED_UNICODE);
            $this->util->log($method.' failed');
            $this->util->log($logMessage);

            // Build the error message
            $errorMessage = $status->statusCode . ': ' . $status->statusMessage;
            $errorCode = 0;
            if ($getCustomErrorMessage !== null) {
                $errorMessage = call_user_func_array($getCustomErrorMessage, [$response, &$errorCode, $responseXML]);
                if ($errorMessage === false) {
                    return $response;
                }
            }

            $snippet = DHLApiMessageToSnippetTranslator::getSnippetNameForMessage($status->statusMessage);
            if ($snippet !== null) {
                throw new DHLException($status->statusMessage, $status->statusCode, $snippet, [$errorMessage]);
            }

            throw new \Exception($errorMessage, $errorCode);
        }

        return $response;
    }

    private static function xmlToArray($root)
    {
        $result = [];

        if ($root->hasAttributes()) {
            $attrs = $root->attributes;
            foreach ($attrs as $attr) {
                $result['@attributes'][$attr->name] = $attr->value;
            }
        }

        if ($root->hasChildNodes()) {
            $children = $root->childNodes;
            if ($children->length == 1) {
                $child = $children->item(0);
                if ($child->nodeType == XML_TEXT_NODE) {
                    $nodeValue = $child->nodeValue;
                    // Check if $value is an int (potentially in a string) and transform it to a real int
                    if (filter_var($nodeValue, FILTER_VALIDATE_INT) !== false) {
                        $nodeValue = (int) $nodeValue;
                    }
                    $result['_value'] = $nodeValue;

                    return count($result) == 1 ? $result['_value'] : $result;
                }
            }
            $groups = [];
            foreach ($children as $child) {
                // Remove namespace from tag names
                $nodeName = array_pop(preg_split('/:/', $child->nodeName));

                if (!isset($result[$nodeName])) {
                    $result[$nodeName] = self::xmlToArray($child);
                } else {
                    if (!isset($groups[$nodeName])) {
                        $result[$nodeName] = [$result[$nodeName]];
                        $groups[$nodeName] = 1;
                    }
                    $result[$nodeName][] = self::xmlToArray($child);
                }
            }
        }

        return $result;
    }

    /**
     * Decides whether the customer data is allowed to be transferred to DHL.
     *
     * @param Order $order
     * @return bool
     */
    private function isCustomerMailTransferAllowed(Order $order)
    {
        switch ($this->util->config($order->getShop()->getId(), 'gdprMailConfiguration')) {
            case ShippingProvider::GDPR_ALWAYS:
                return true;
            case ShippingProvider::GDPR_NEVER:
                return false;
            case ShippingProvider::GDPR_CUSTOMER_CHOICE:
                $orderAttributes = $order->getAttribute();

                return $orderAttributes && $orderAttributes->getViisonTransferCustomerContactDataAllowed();
            default:
                return false;
        }
    }
}
