<?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\SoapRequestBodyMutator;
use Shopware\Plugins\ViisonDHL\Util;

/**
 * Class ExpressShipmentRequest
 *
 * This class represents an DHL SOAP API call to the CreateShipmentOrder 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 ExpressShipmentRequest
{
    /**
     * Maximum field lengths according to DHL documentation.
     */
    const MAX_PERSON_NAME_LENGTH = 45;
    const MAX_COMPANY_NAME_LENGTH = 35;
    const MAX_ADDRESS_NAME_LENGTH = 35;
    const MAX_STREET_NAME_LENGTH = 35;
    const MAX_STREET_NUMBER_LENGTH = 15;
    const MAX_ADDITIONAL_ADDRESS_LINE_LENGTH = 35;
    const MAX_ZIP_LENGTH = 12;
    const MAX_CITY_LENGTH = 35;
    const MAX_PHONE_NUMBER_LENGTH = 20;
    const MAX_EMAIL_LENGTH = 50;
    const MAX_WEIGHT_LENGTH = 22;
    const MAX_LENGTH_LENGTH = 22;
    const MAX_WIDTH_LENGTH = 22;
    const MAX_HEIGHT_LENGTH = 22;

    /**
     * The shipment info contained in the request body.
     */
    private $shipmentInfo;

    /**
     * The shipper information contained in the request body.
     */
    private $shipper;

    /**
     * The international detail information.
     */
    private $exportDocument;

    /**
     * The receiver information contained in the request body.
     */
    private $recipient;

    /**
     * The shop id, for which the label should be created.
     */
    private $shopId;

    /**
     * @var array $product The product type of this shipment.
     */
    private $product;

    /**
     * @var string $termsOfTrade The terms of trade state which charges the sender and receiver have to pay.
     */
    private $termsOfTrade;

    /**
     * @var SoapRequestBodyMutator[]
     */
    private $soapBodyRequestMutators = [];

    /**
     * The main constructor creating all necessary parts for the request body.
     *
     * @param array $order An array containing the main order information.
     * @param array $sender An array containing information about the shipper.
     * @param string $EKP The DHL EKP of this shipment.
     * @param array $product The product type of this shipment.
     * @param float $weight The weight of this shipment.
     * @param array $packageDimensions The length, width and height of the package.
     * @param int $shopId
     */
    public function __construct($order, $sender, $accountNumber, $product, $weight, $packageDimensions, $shopId)
    {
        $this->shopId = $shopId;
        $this->product = $product;
        $this->shipmentInfo = $this->createShipmentInfo($order, $accountNumber, $product);
        $this->shipper = $this->createShipper($sender, $order);
        $this->weight = $weight;
        $this->packageDimensions = $packageDimensions;
        $this->order = $order;
    }

    /**
     * Creates the main request body containing all information like shipper and receiver.
     *
     * Product LEGEND:
     *
     *    ProductName                   ServiceType                  Content
     * DOMESTIC EXPRESS                     N                       DOCUMENTS
     * DOMESTIC EXPRESS 9:00                I                       DOCUMENTS
     * DOMESTIC EXPRESS 10:30               O                       DOCUMENTS
     * DOMESTIC EXPRESS 12:00               1                       DOCUMENTS
     *
     * EXPRESS ENVELOPE                     X                       DOCUMENTS
     *
     * EXPRESS WORLDWIDE (EU)               U                       DOCUMENTS
     *
     * EXPRESS WORLDWIDE (Non-Doc)          P                       NON_DOCUMENTS
     * EXPRESS WORLDWIDE 9:00 (Non-Doc)     E                       NON_DOCUMENTS
     * EXPRESS WORLDWIDE 10:30 (Non-Doc)    M                       NON_DOCUMENTS
     * EXPRESS WORLDWIDE 12:00 (Non-Doc)    Y                       NON_DOCUMENTS
     *
     * EXPRESS WORLDWIDE (Doc)              D                       DOCUMENTS
     * EXPRESS WORLDWIDE 9:00 (Doc)         K                       DOCUMENTS
     * EXPRESS WORLDWIDE 10:30 (Doc)        L                       DOCUMENTS
     * EXPRESS WORLDWIDE 12:00 (Doc)        T                       DOCUMENTS
     *
     * ECONOMY SELECT (EU)                  W                       DOCUMENTS
     * ECONOMY SELECT (NON-EU)              H                       NON_DOCUMENTS
     *
     * @return array The request body containing all shipment information.
     * @throws Exception
     */
    public function getBody()
    {
        $shipTimestamp = new \DateTime();
        $shipTimestamp->modify('+15 minutes');

        $content = ($this->product['isDutiable'] === true) ? 'NON_DOCUMENTS' : 'DOCUMENTS';
        $serviceType = null;

        $this->shipmentInfo['ServiceType'] = $this->product['product'];
        if ($this->shipmentInfo['ServiceType'] === null) {
            throw new \Exception(sprintf('Interner Fehler: für die dateTimeOption \'%s\' konnte kein DHL Express Produkt ermittelt werden.', $dateTimeOption));
        }

        $orderNumber = $this->order['orderNumber'];
        if (empty($orderNumber)) {
            $orderNumber = '-';
        }

        $body = [
            'RequestedShipment' => [
                'ShipmentInfo' => $this->shipmentInfo,
                'ShipTimestamp' => $shipTimestamp->format('Y-m-d\\TH:i:s\\G\\M\\TP'),
                'PaymentInfo' => $this->termsOfTrade,
                'Ship' => [
                    'Shipper' => $this->shipper,
                    'Recipient' => $this->recipient,
                ],
                'InternationalDetail' => [
                    'Content' => $content,
                ],
                'Packages' => [
                    'RequestedPackages' => [
                        'number' => 1,
                        'Weight' => !empty($this->weight) ? round($this->weight, 3) : 0,
                        'Dimensions' => [
                            'Length' => $this->packageDimensions['length'],
                            'Width' => $this->packageDimensions['width'],
                            'Height' => $this->packageDimensions['height'],
                        ],
                        'CustomerReferences' => $orderNumber,
                    ],
                ],
            ],
        ];

        foreach ($this->soapBodyRequestMutators as $soapBodyRequestMutator) {
            $body = $soapBodyRequestMutator->mutate($body);
        }

        if (!empty($this->exportDocument)) {
            // Add export document
            $body['RequestedShipment']['InternationalDetail']['Commodities'] = $this->exportDocument;
        } else {
            throw new \Exception('International detail information not set');
        }

        $body = Shopware()->Events()->filter(
            'Shopware_ViisonDHL_ExpressShipmentRequest_GetBody_FilterBody',
            $body,
            [
                'subject' => $this,
            ]
        );
        // Fire legacy event
        // TODO: Remove this as soon as no other plugins depend on this old event and
        //       the adoption of the new releases of other plugins is close to 100%.
        $body = Shopware()->Events()->filter(
            'Shopware_ViisonIntraship_ExpressShipmentRequest_GetBody_FilterBody',
            $body,
            [
                'subject' => $this,
            ]
        );

        return $body;
    }

    /**
     * @param SoapRequestBodyMutator $soapBodyRequestMutator
     */
    public function addServiceOption(SoapRequestBodyMutator $soapBodyRequestMutator)
    {
        $this->soapBodyRequestMutators[] = $soapBodyRequestMutator;
    }

    /**
     * Sets a new shipment receiver which is created using the given data.
     *
     * @param array $details The shipment details used to create the new receiver.
     */
    public function setReceiver($details)
    {
        $this->recipient = $this->createRecipient($details);
    }

    /**
     * Adds the information that is relevant for international shipments. This information also has to given for national shipments, because the 'InternationalDetail' element is mandatory for the webservice.
     *
     * @param array $order The order containing the date, total amount and currency.
     * @param array $exportInformation An array containing information like the Incoterm or the items contained in the order.
     * @param float $totalWeight The (optional) total weight of the order used to calculate the weight of each item. Pass a value only if the item weights shall be calculated proportionately from that weight.
     */
    public function setExportDocument($exportInformation, $totalWeight = null)
    {
        $this->termsOfTrade = $exportInformation['termsOfTrade'];

        // Create export document ( merge in case that export document array is created earlier, example setAndCalculateCustomsValue )
        $this->exportDocument = array_merge([
            'Description' => mb_substr($exportInformation['typeDescription'], 0, 30, 'utf-8'),
            'NumberOfPieces' => 1, // At this Point we don't support Multi-Coli package transport
        ], is_array($this->exportDocument) ? $this->exportDocument : []);
        $this->setCustomsValue($exportInformation['customsValue'] + $this->order['shippingCosts']);

        if ($exportInformation['invoice'] !== null) {
            // Use the date and number from the invoice document
            $this->exportDocument['invoiceDate'] = $exportInformation['invoice']['date'];
            $this->exportDocument['invoiceNumber'] = $exportInformation['invoice']['number'];
        }
    }

    /**
     * Set amount for CustomsValue
     *
     * NOTE: For a selected transport insurance, this value is
     *       not lower than the value of the transport insurance
     *
     * @param integer $value
     * @throws \Exception
     */
    public function setCustomsValue($value)
    {
        if (!isset($value)) {
            throw new \Exception('[DHLexpress::setCustomsValue] Value is undefined.');
        }

        $this->exportDocument = ($this->exportDocument) ?: ['CustomsValue' => 0];
        $this->exportDocument['CustomsValue'] = max($this->exportDocument['CustomsValue'], $value);
    }

    /**
     * Adds the given service option to the shipment details. For each service, a new service
     * tag is added to the request with group name and service name describing the exact type
     * of service.
     *
     * @param array $value The value of the newly added service option.
     */
    public function addService($value)
    {
        $this->shipmentInfo['SpecialServices'][] = $value;
    }

    /**
     * Creates a structured array containing the basic shipment details. This structure is
     * conform to the one of the DHL API.
     *
     * @param array $order An array containing the main order information.
     * @param string $EKP The DHL EKP of this shipment.
     * @param array $product The product of this shipment.
     * @param float $weight The weight of this shipment.
     * @param array $packageDimensions The length, widht and height of this package.
     * @return array A structured array containing the shipment details.
     */
    private function createShipmentInfo($order, $accountNumber, $product)
    {
        $shipmentInfo = [
            'DropOffType' => 'REGULAR_PICKUP', // TODO other option: REQUEST_COURIER
            'Account' => $accountNumber, // TODO
            'Currency' => $order['currency'] ?: 'EUR',
            'UnitOfMeasurement' => 'SI',// use the metric system for weight and length measures
        ];

        return $shipmentInfo;
    }

    /**
     * Creates a structured array containing the shippers data. This structure is
     * conform to the one of the DHL API.
     *
     * @param array $sender The sender information as an array.
     * @param array $order The order information as an array.
     * @return array A structured array containing the shipper information.
     */
    private function createShipper($sender, $order)
    {
        $shipper = [
            'Contact' => [
                'PersonName' => mb_substr($sender['contactPerson'], 0, self::MAX_PERSON_NAME_LENGTH, 'utf-8'),
                'CompanyName' => mb_substr($sender['companyName'], 0, self::MAX_COMPANY_NAME_LENGTH, 'utf-8'),
                'PhoneNumber' => mb_substr($sender['phoneNumber'], 0, self::MAX_PHONE_NUMBER_LENGTH),
            ],
            'Address' => [
                'StreetLines' => mb_substr($sender['streetName'] . ' ' . $sender['streetNumber'], 0, self::MAX_STREET_NAME_LENGTH, 'utf-8'),
                'StreetName' => mb_substr($sender['streetName'], 0, self::MAX_STREET_NAME_LENGTH, 'utf-8'),
                'StreetNumber' => mb_substr($sender['streetNumber'], 0, self::MAX_STREET_NUMBER_LENGTH, 'utf-8'),
                'PostalCode' => $sender['zip'],
                'City' => mb_substr($sender['city'], 0, self::MAX_CITY_LENGTH, 'utf-8'),
                'CountryCode' => $sender['countryISO'],
            ],
        ];

        if (mb_strlen($sender['email']) > 0) {
            $shipper['Contact']['EmailAddress'] = $sender['email'];
        }

        return $shipper;
    }

    /**
     * Creates a structured array containing the shipment receivers data. This structure is
     * conform to the one of the DHL API.
     *
     * @param array $order The order information as an array.
     * @return array A structured array containing the receiver information.
     */
    private function createRecipient($order)
    {
        $order = array_map('trim', $order);

        // Company incl. Communication
        $salutation = Util::instance()->localizeSalutation($order['salutation'], $order['countryiso']);
        $person = $this->shortenPersonNames([
            'salutation' => (!empty($salutation)) ? ucfirst($salutation) : '',
            'firstname' => (!empty($order['firstname'])) ? $order['firstname'] : '',
            'lastname' => (!empty($order['lastname'])) ? $order['lastname'] : '',
        ]);
        $names = [];
        $addressAddition = '';
        $communication = [];

        if (!empty($order['company'])) {
            // Company
            $names[] = mb_substr($order['company'], 0, self::MAX_ADDRESS_NAME_LENGTH, 'utf-8');
            $names[] = mb_substr($order['department'], 0, self::MAX_ADDRESS_NAME_LENGTH, 'utf-8');
            $communication['contactPerson'] = trim(implode(' ', array_values($person['name'])));
        } else {
            // Person
            //$company['Person'] = $person['name'];
            $names[] = implode(' ', [$person['name']['salutation'], $person['name']['firstname'], $person['name']['lastname']]);
            // TODO
            // Remove duplicate name on label
            if ($order['countryiso'] === 'DE') { // Unfortunately the contact person is mandatory for all international shipments
                $communication['contactPerson'] = ''; // Otherwise computed from the company person by DHL
            }
        }

        // Additional communication
        $phoneNumber = $order['phone'];
        // Clean phone number
        $phoneNumber = preg_replace('/[^+\\d]/', '', $phoneNumber);
        // Check if phone number ist valid
        if (preg_match('/^\\+?\\d{2,}$/', $phoneNumber, $matches) === 0) {
            // If phone number is invalid, use default value to prevent errors
            $phoneNumber = '-';
        }
        $communication['phone'] = mb_substr($phoneNumber, 0, self::MAX_PHONE_NUMBER_LENGTH);

        $streetNumber = trim($order['streetnumber']);

        $additionalAddressLine = implode(', ', array_filter(
            [
                $order['additionaladdressline'],
                mb_substr($person['coForFirstname'], 0, 30, 'utf-8'),
            ]
        ));

        $streetName = trim($order['street']);
        if (empty($streetNumber)) {
            $streetNumber = '.';
        }

        // If the house number is too long, append its first characters to the street name so that no information gets lost
        if (mb_strlen($streetNumber) > self::MAX_STREET_NUMBER_LENGTH) {
            $numExcessCharacters = mb_strlen($streetNumber) - self::MAX_STREET_NUMBER_LENGTH;

            // Shorten the street name if necessary
            $streetName = mb_substr($streetName, 0, max(0, self::MAX_STREET_NAME_LENGTH - $numExcessCharacters), 'utf-8');
            $streetName .= mb_substr($streetNumber, 0, $numExcessCharacters);
            $streetNumber = mb_substr($streetNumber, $numExcessCharacters);
        }

        $zipCode = $order['zipcode'];

        $personName = trim(implode(' ', array_values($person['name'])));
        $companyName = trim(implode(', ', [$order['company'], $order['department']]), ', ');

        // According to DHL Austria when no company name exists the person name should also be used in the company field.
        // Further infomation: https://github.com/pickware/ShopwareDHLAdapter/issues/411
        if (empty($companyName)) {
            $companyName = $personName;
        }

        // Combine parts
        $receiver = [
            'Contact' => [
                'PersonName' => $personName,
                'CompanyName' => $companyName,
                'PhoneNumber' => $phoneNumber,
            ],
            'Address' => [
                'StreetLines' => mb_substr($streetName . ' ' . $streetNumber, 0, self::MAX_STREET_NAME_LENGTH, 'utf-8'),
                'StreetName' => mb_substr($streetName, 0, self::MAX_STREET_NAME_LENGTH, 'utf-8'),
                'StreetNumber' => $streetNumber,
                'PostalCode' => $zipCode,
                'City' => mb_substr($order['city'], 0, self::MAX_CITY_LENGTH, 'utf-8'),
                'CountryCode' => $order['countryiso'],
            ],
        ];

        if (!empty($order['email'])) {
            $receiver['Contact']['EmailAddress'] = $order['email'];
        }

        $additionalAddressLine = mb_substr($additionalAddressLine, 0, self::MAX_ADDITIONAL_ADDRESS_LINE_LENGTH, 'utf-8');
        if (!empty($additionalAddressLine)) {
            $receiver['Address']['StreetLines2'] = $additionalAddressLine;
        }

        return $receiver;
    }

    /** TODO anpassen
     * Shortens the complete name of a person by respecting an overall limit
     * of 30 characters. That is, if all name parts joined exceed that limit,
     * these parts will be deleted step-by-step.
     *
     * @param array $name An array containing all name parts to shorten to at least 30 characters.
     * @return array An array whose name parts will not exceed a length of 30.
     */
    private function shortenPersonNames($name)
    {
        $result = [
            'name' => $name,
            'coForFirstname' => null,
        ];
        if (mb_strlen(implode(' ', $name), 'UTF-8') <= 30) {
            return $result;
        } elseif (mb_strlen($name['firstname'] . ' ' . $name['lastname'], 'UTF-8') <= 30) {
            $result['name']['salutation'] = '';

            return $result;
        } else {
            // Use c/o field for first name
            $result['name']['salutation'] = '';
            $result['name']['firstname'] = '';
            $result['name']['lastname'] = mb_substr($name['lastname'], 0, 30, 'utf-8');
            $result['coForFirstname'] = mb_substr($name['firstname'], 0, 30, 'utf-8');

            return $result;
        }
    }
}
