<?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\Components;

use Doctrine\ORM\EntityManager;
use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonDHL\Util;
use Shopware\Plugins\ViisonDHL\Classes\PluginInfo;
use Shopware\Plugins\ViisonShippingCommon\Components\CurrencyConverter;
use Shopware\Plugins\ViisonShippingCommon\Components\ViisonShippingCommonDispatchData;

/**
 * Class ViisonDHLExportDocument
 *
 * This class is responsible for managing and creating the Export documents.
 * The Export documents are required for all not EU countries and they describe
 * all shipping products with price and dimensions.
 *
 * DHL API is creating the Export PDF from the info that is send to it.
 *
 * IMPORTANT: This URL https://github.com/VIISON/ShopwareDHLAdapter/issues/28 provides
 *            a lot information for the rounding and calculations, if some changes are necessary in that logic
 *            please read first the info on that link.
 *
 * @package Shopware\Plugins\ViisonDHL\Components
 */
class ViisonDHLExportDocument extends \Enlight_Class
{
    // If there are more than 99 positions in the order, only show the first 98 and aggregate the rest of the items
    // to one position.
    const MAX_DHL_LABEL_EXPORT_DOC_POSITIONS = 99;

    /**
     * @var CurrencyConverter
     */
    private $currencyConverter;

    /**
     * @var ViisonShippingCommonDispatchData
     */
    private $shippingCommonDispatchDataService;

    /**
     * @var EntityManager
     */
    private $entityManager;

    public function __construct(
        EntityManager $entityManager,
        CurrencyConverter $currencyConverter,
        ViisonShippingCommonDispatchData $shippingCommonDispatchDataService
    ) {
        $this->entityManager = $entityManager;
        $this->currencyConverter = $currencyConverter;
        $this->shippingCommonDispatchDataService = $shippingCommonDispatchDataService;

        parent::__construct();
    }

    /**
     * Gathers information like the total number of items contained in the order with the given id
     * ore information about every item as well as some configuration parameters, which are required
     * to generate an export document.
     *
     * @param int $orderId The id of the order whose items and their total quantity shall be determined.
     * @param int $shopId The id of the shop from whose configuration the export information shall be taken.
     * @param array $containedItems An optional array containing the ids and quantities of all items, which shall be contained in the export document. If an empty array is given (the default), all order positions are included in the export document.
     * @param int $languageID The language that should be used for the export document positions, if a translation is available
     * @return array An array containing configuration parameters, the total item quantity and information about each item.
     */
    public function getExportDocumentInformation($orderId, $shopId, $containedItems = [], $languageID = null)
    {
        // Get the physical order positions that are getting dispatched (no discounts etc.)
        $dispatchOrderPositions = $this->getExportDocumentItems($orderId, $containedItems, $languageID);

        $quantity = array_sum(array_map(function ($item) {
            return $item['quantity'];
        }, $dispatchOrderPositions));

        $customsValue = array_sum(array_map(function ($item) {
            return $item['quantity'] * $item['price'];
        }, $dispatchOrderPositions));

        // Try to get the invoice
        $invoice = Shopware()->Db()->fetchRow(
            'SELECT date, docId AS number
            FROM s_order_documents
            WHERE orderID = ?
            AND type = 1',
            [
                $orderId
            ]
        );
        if (empty($invoice)) {
            $invoice = null;
        }

        /** @var Order $order */
        $order = $this->entityManager->find('Shopware\\Models\\Order\\Order', $orderId);
        $currency = $order->getCurrency();

        return [
            'typeOfShipment' => Util::instance()->config($shopId, 'exportTypeOfShipment'),
            'typeDescription' => Util::instance()->config($shopId, 'exportDocumentTypeDescription'),
            'description' => Util::instance()->config($shopId, 'exportDescription'),
            'placeOfCommittal' => Util::instance()->config($shopId, 'exportPlaceOfCommittal'),
            'invoice' => $invoice,
            'vatNumber' => Shopware()->Config()->taxNumber,
            'customsValue' => $customsValue,
            'items' => $dispatchOrderPositions,
            // No DHL API field, but required for later computations
            'totalNumberOfItems' => $quantity,
            'currency' => $currency,
            'sendersCustomsReference' => Util::instance()->config($shopId, 'sendersCustomsReference'),
        ];
    }

    /**
     * @param int $orderId
     * @param array $containedItems
     * @param int|null $languageId
     * @return array
     */
    public function getExportDocumentItems($orderId, array $containedItems = [], $languageId = null)
    {
        $dispatchOrderPositions = $this->shippingCommonDispatchDataService->getDispatchOrderPositions(
            $orderId,
            new PluginInfo(),
            $languageId
        );
        if (!$containedItems) {
            return $dispatchOrderPositions;
        }

        // Filter for the relevant items
        foreach ($dispatchOrderPositions as $key => &$item) {
            if (array_key_exists($item['id'], $containedItems)) {
                // Change quantity and update price
                $item['quantity'] = $containedItems[$item['id']];
                $item['value'] = floatval($item['price']) * $item['quantity'];
            } else {
                unset($dispatchOrderPositions[$key]);
            }
        }

        return array_values($dispatchOrderPositions);
    }

    /**
     * @param array $exportDocumentItems
     * @param array $allowedCurrencies
     * @param string $currencyIsoToConvertTo
     * @return array
     */
    public function convertExportDocumentItemPrices(array $exportDocumentItems, array $allowedCurrencies, $currencyIsoToConvertTo)
    {
        if (count($exportDocumentItems) === 0) {
            return $exportDocumentItems;
        }
        $currencyIsoCode = $exportDocumentItems[0]['currency'];
        if (in_array($currencyIsoCode, $allowedCurrencies)) {
            return $exportDocumentItems;
        }

        $currencyConversionFactor = $this->currencyConverter->getConversionFactor($currencyIsoCode, $currencyIsoToConvertTo);
        $exportDocumentItems = array_map(function ($dispatchOrderPosition) use ($currencyConversionFactor) {
            $dispatchOrderPosition['price'] = $dispatchOrderPosition['price'] * $currencyConversionFactor;
            $dispatchOrderPosition['currency'] = 'EUR';

            return $dispatchOrderPosition;
        }, $exportDocumentItems);

        return $exportDocumentItems;
    }

    /**
     * @param array $exportDocumentItems
     * @param int $maxExportDocumentItems
     * @return array
     */
    public function condenseExportDocumentItems(array $exportDocumentItems, $maxExportDocumentItems)
    {
        return $this->shippingCommonDispatchDataService->condenseExportDocumentItems(
            $exportDocumentItems,
            $maxExportDocumentItems
        );
    }

    /**
     * @param array $exportItems
     * @param string $shipperCountryCode
     * @param float|null $shipmentWeightOverride (optional) If null, the shipment weight will be calculated automatically
     * @return array
     */
    public function getExportDocPositions(array $exportItems, $shipperCountryCode, $shipmentWeightOverride = null)
    {
        // Set 0.1 kg as weight for each item with zero weight. This is done because we use the same default weight in
        // determineShipmentWeight() in the ShippingUtil in ShippingCommon
        $exportItems = array_map(
            function ($item) {
                if ($item['weight'] == 0) {
                    $item['weight'] = 0.1;
                }

                return $item;
            },
            $exportItems
        );

        // If $shipmentWeightOverride is set, then proportionally increase/decrease the weight of every item.
        if ($shipmentWeightOverride !== null) {
            $actualShipmentWeight = array_reduce($exportItems, function ($shipmentWeight, $item) {
                return $shipmentWeight + $item['quantity'] * $item['weight'];
            }, 0);

            $factor = $shipmentWeightOverride / $actualShipmentWeight;

            $exportItems = array_map(
                function ($item) use ($factor) {
                    $item['weight'] = $item['weight'] * $factor;

                    return $item;
                },
                $exportItems
            );
        }

        $exportItems = $this->condenseExportDocumentItems(
            $exportItems,
            self::MAX_DHL_LABEL_EXPORT_DOC_POSITIONS
        );

        // Because the array can be associative (picked quantities via PickingApp), we are filtering first all
        // values and after that we are mapping the response.
        return array_values(
            array_map(
                function ($item) use ($shipperCountryCode) {
                    return [
                        'description' => mb_substr($item['name'] ?: $item['articleordernumber'], 0, 40, 'utf-8'),
                        'countryCodeOrigin' => $item['countryOfOriginCode'] ?: $shipperCountryCode,
                        'amount' => intval($item['quantity']),
                        // Use EUR as fallback if something goes wrong
                        'currency' => $item['currency'] ?: 'EUR',
                        // We floor on the 3rd decimal here because the total weight calculated by DHL (amount * netWeightInKG)
                        // must be equal or smaller than the total weight supplied via the api request. As Shopware currently
                        // only allows 3 decimal weights this is only relevant for a fixed total weight where each items weight
                        // is calculated automatically based on the total amount of items. Furthermore we use a minimum of
                        // 0.001 kg here because that's the minimum the DHL api supports.
                        'netWeightInKG' => max(floor($item['weight'] * 1000) / 1000, 0.001),
                        'customsValue' => max(round($item['price'], 2), 0.01),
                        'customsTariffNumber' => ($item['customsTariffNumber']) ?: '',
                    ];
                },
                $exportItems
            )
        );
    }
}
