<?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\Plugins\ViisonDHL\Classes\DhlExpressServiceOptions\DhlExpressInsuredServiceOption;
use Shopware\Plugins\ViisonDHL\Classes\DhlExpressServiceOptions\DhlExpressSaturdayDeliveryServiceOption;
use Shopware\Plugins\ViisonDHL\Components\ShippingDocumentTypeFactoryService;
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\LabelPersister;

/**
 * Class DHLExpressCommunication
 *
 * This class represents an DHL Express SOAP API call to the ExpressShipmentRequest method.
 * Therefor it overrides the constructor expecting the shipment information and provides a setter for
 * the receiver as well as the shipment weight.
 *
 * @package Shopware\Plugins\ViisonDHL\Classes
 */
class DHLExpressCommunication
{
    const WSDL_SERVICE_NAME = 'express';
    const WSDL_VERSION = '1.0';
    const WSDL_URL_PRODUCTION = 'https://wsbexpress.dhl.com/gbl/expressRateBook?WSDL';
    const WSDL_URL_TEST = 'https://wsbexpress.dhl.com/sndpt/expressRateBook?WSDL';
    const LOCAL_WSDL_DIR_NAME = 'express';
    const LOCAL_WSDL_FILE_NAME_PRODUCTION = 'expressRateBookProduction.wsdl';
    const LOCAL_WSDL_FILE_NAME_TEST = 'expressRateBookTest.wsdl';

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

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

    /**
     * @var string
     */
    private $wsdlUrl;

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

    /**
     * @param LocalWSDLHandler $wsdlHandler
     * @param string $wsdlUrl
     */
    private function __construct(LocalWSDLHandler $wsdlHandler, $wsdlUrl)
    {
        $this->util = Util::instance();
        $this->localWsdlHandler = $wsdlHandler;
        $this->documentTypeFactory = Shopware()->Container()->get('viison_dhl.shipping_document_type_factory');
        $this->wsdlUrl = $wsdlUrl;
    }

    /**
     * @return self
     */
    public static function createForProductionEndPoint()
    {
        return new self(self::createLocalWsdlHandler(self::LOCAL_WSDL_FILE_NAME_PRODUCTION), self::WSDL_URL_PRODUCTION);
    }

    /**
     * @return self
     */
    public static function createForTestingEndPoint()
    {
        return new self(self::createLocalWsdlHandler(self::LOCAL_WSDL_FILE_NAME_TEST), self::WSDL_URL_TEST);
    }

    /**
     * @param string $localWsdlFileName
     * @return LocalWSDLHandler
     */
    private static function createLocalWsdlHandler($localWsdlFileName)
    {
        return new LocalWSDLHandler(
            self::WSDL_SERVICE_NAME,
            $localWsdlFileName,
            self::LOCAL_WSDL_DIR_NAME,
            self::WSDL_VERSION
        );
    }

    /**
     * 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 free form label.
     * @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($shopId, $order, $sender, $product, $orderId, $shipmentWeight, $useItemWeights, $shippingDetails = null, $packageDimensions = null, $settings = null, $extraSettings = null, $exportDocumentItems = [])
    {
        $expressAccountNumber = $this->util->config($shopId, 'expressAccountNumber');
        $request = new ExpressShipmentRequest($order, $sender, $expressAccountNumber, $product, $shipmentWeight, $packageDimensions, $shopId);

        $gotCashOnDeliveryParam = !empty($settings) && array_key_exists('cashondelivery', $settings);
        if ($gotCashOnDeliveryParam && $settings['cashondelivery']) {
            throw new \Exception('Es ist nicht möglich, Nachnahme-Versandetiketten bei DHL Express zu erstellen.');
        }

        if (!empty($shippingDetails)) {
            // Set custom shipping values
            $request->setReceiver($shippingDetails);
        } else {
            // Use the shipping address of the order
            $request->setReceiver($order);
        }

        if ($orderId !== null) {
            $exportInfo = Shopware()->Container()->get('viison_dhl.export_document')->getExportDocumentInformation($orderId, $shopId, $exportDocumentItems);
            $exportInfo['termsOfTrade'] = $order['termsOfTrade'];
        } else {
            $exportInfo = [
                '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,
                'customsValue' => $extraSettings['insuredValue'],
            ];
        }

        /**
         * Note: PaymentInfo (incoterm) is a mandatory filed by DHL Express
         *       even for Domestic shipping.
         */
        if (!empty($exportInfo) && array_key_exists('termsOfTrade', $exportInfo) && (empty($exportInfo['termsOfTrade']) || $exportInfo['termsOfTrade'] === '-')) {
            $snippets = $this->util->getSnippetHandlerFromNamespace('exceptions/viison_dhl');
            throw new \Exception($this->util->addAdditionalTextToSnippet($snippets->get('express/incotermEmpty'), $this->util->getShopNameFromId($shopId)));
        }

        $totalWeight = (!$useItemWeights) ? $shipmentWeight : null;
        $request->setExportDocument($exportInfo, $totalWeight);

        $dispatchMethodLevelSettings = $this->util->getDispatchMethodLevelSettings($orderId);

        $isInsuredValueSetInExtraSettings = isset($extraSettings['insuredValue']) && $extraSettings['insuredValue'];
        $isInsuredCurrencySetInExtraSettings = isset($extraSettings['insuredValueCurrency']) && $extraSettings['insuredValueCurrency'];
        if ($isInsuredValueSetInExtraSettings && $isInsuredCurrencySetInExtraSettings) {
            // Set the User input as priority, so he is able to override the dispatch settings in the single label creation.
            $request->addServiceOption(new DhlExpressInsuredServiceOption(
                (float) $extraSettings['insuredValue'],
                $extraSettings['insuredValueCurrency']
            ));
        } elseif ($orderId && isset($dispatchMethodLevelSettings['isInsured']) && $dispatchMethodLevelSettings['isInsured']) {
            $order = Shopware()->Models()->find('Shopware\\Models\\Order\\Order', $orderId);
            // If the option is coming from the dispatch settings take the order values.
            $request->addServiceOption(new DhlExpressInsuredServiceOption(
                $order->getInvoiceAmount(),
                $order->getShop()->getCurrency()->getCurrency()
            ));
        }

        $isSingleLabelFlow = isset($settings['issaturdaydelivery']);
        $isMobileOrBatchFlow = !$isSingleLabelFlow && isset($dispatchMethodLevelSettings['isSaturdayDelivery']) && $dispatchMethodLevelSettings['isSaturdayDelivery'];
        if (($isSingleLabelFlow && $settings['issaturdaydelivery']) || $isMobileOrBatchFlow) {
            $request->addServiceOption(new DhlExpressSaturdayDeliveryServiceOption());
        }

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

        // Save the content of the binary buffer in a new PDF file
        $newTrackingCode = $response->ShipmentIdentificationNumber;
        $documentFileName = $this->util->getDocumentFileName($newTrackingCode, ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL);
        $pdf = $response->LabelImage->GraphicImage;

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

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

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

        // Add the identifiers of all created documents
        $shippingLabelType = $this->documentTypeFactory->createLabelDocumentType();
        $result->addDocument(new ShippingDocument(
            ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL,
            $newTrackingCode,
            $shippingLabelType->getPageSizeName(),
            $shippingLabelType->getId()
        ));

        return $result;
    }

    /**
     * This action requests a deletion from DHL express 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)
    {
        // Deleting a label with DHL Express is only necessary if a pickup was booked with the original label request:
        // "If a scheduled pickup was not included in the original ShipmentRequest (i.e. DropOffType = REGULAR_PICKUP),
        // then it is not necessary to delete the shipment." (see 'DHL Express Global Web Services - Developer Guide V2.8.pdf', p. 41)
        // Since we only have implemented the REGULAR_PICKUP functionality, we do not need to delete shipments,
        // TODO implement this as soon as we support the DropOffType REQUEST_COURIER

        /*
        // Determine the id of the shop, in which the order associated with the tracking code was made
        $shopId = $this->util->originatingShopOfTrackingCode($trackingCode);

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

        $this->sendSoapRequest('deleteShipmentRequest', $shopId, $request);
        */
    }

    /**
     * Sends a SOAP request to the DHL express 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.
     * @return string
     * @throws \Exception
     */
    private function sendSoapRequest($method, $shopId, $request)
    {
        $options = [
            'soap_version' => SOAP_1_1,
            'cache_wsdl' => WSDL_CACHE_NONE,
            'trace' => true,
        ];

        $client = $this->localWsdlHandler->getSoapClient($this->wsdlUrl, $options);

        $userName = $this->util->config($shopId, 'expressUsername');
        $password = $this->util->config($shopId, 'expressPassword');

        $wsseHeader = new WsseAuthHeader($userName, $password);
        $client->__setSoapHeaders([$wsseHeader]);

        try {
            $response = $client->__soapCall($method, ['parameters' => $request->getBody()]);
            Shopware()->Container()->get('viison_dhl.soap_logger')->logLastRequest($client);
        } catch (\SoapFault $e) {
            Shopware()->Container()->get('viison_dhl.soap_logger')->logFailedRequest($client, $e);
            if (preg_match('/^.*401 Unauthorized.*\\r\\n/', $client->__getLastResponseHeaders())) {
                throw new \Exception('Ungültige Zugangsdaten: Die in der DHL Adapter Konfiguration angegebenen Zugangsdaten zu DHL Express sind nicht korrekt.');
            }
            if ($e->faultcode == 'HTTP' && $e->faultstring == 'Error Fetching http headers') {
                throw new \Exception('Der DHL Express Webservice ist derzeit nicht verfügbar. Bitte versuchen Sie es später erneut oder setzen Sie sich mit DHL in Verbindung.');
            }
            throw new \Exception(('DHL Express Webservice-Anfrage fehlgeschlagen: ' . $e->getMessage()), 0, $e);
        }

        $this->assertSuccessfulResonse($response);

        return $response;
    }

    /**
     * @param \stdClass $response
     * @throws DHLExpressException
     */
    private function assertSuccessfulResonse($response)
    {
        $notifications = $response->Notification;

        // Transform to array if only a single notification was part of the response
        if (!is_array($notifications)) {
            $notifications = [$notifications];
        }

        $errors = [];
        foreach ($notifications as $notification) {
            if ($notification->code !== 0) {
                $errors[$notification->code] = $notification->Message;
            }
        }

        if (count($errors) !== 0) {
            throw new DHLExpressException($errors);
        }
    }
}
