<?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\ViisonShippingCommon\Classes;

use Doctrine\ORM\AbstractQuery;
use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonShippingCommon\Shipment;
use Shopware\Models\Shop\Shop;
use Shopware\Plugins\ViisonShippingCommon\Classes\Communication\Communication;
use Shopware\Plugins\ViisonShippingCommon\Util;

require_once __DIR__ . '/../ShippingProviderInterfaceLoader.php';

/**
 * This is a implementation of the ShippingProvider interface defined in ViisonPickwareMobile
 * (and the legacy interface in ViisonPickwareConnector). It can be instantiated to perform basic
 * operations like determining whether the DHL Adapter supports the shipping of a certain order,
 * creating new shipping labels for orders or calculating the weight of shipments which is also
 * used for created shipping labels.
 */
class ShippingProvider implements
    \ViisonPickwareConnector_API_ShippingProvider,
    \ViisonPickwareMobile_Interfaces_ShippingProvider_ShippingProvider
{
    /**
     * GDPR configurations for the shipping adapters. Each adapter can have a different configuration.
     *
     * always = Do not show a checkbox during checkout, ALWAYS transfer the customer data
     * never = Do not show a checkbox during checkout, NEVER transfer the customer data
     * customerChoice = Let the customer choose via checkbox in checkout whether to transfer its data.
     */
    const GDPR_ALWAYS = 'always';
    const GDPR_NEVER = 'never';
    const GDPR_CUSTOMER_CHOICE = 'customerChoice';

    protected $pluginInfo;
    protected $util;
    protected $shippingLabelGenerator;
    protected $shippingUtil;
    protected $communication;

    public function __construct(Util $util, ShippingLabelGenerator $shippingLabelGenerator, ShippingUtil $shippingUtil, Communication $communication = null)
    {
        $this->pluginInfo = $util->getPluginInfo();
        $this->util = $util;
        $this->shippingLabelGenerator = $shippingLabelGenerator;
        $this->shippingUtil = $shippingUtil;
        $this->communication = $communication;
    }

    /* Override */
    public function getIdentifier()
    {
        // Just the class of this instance, which will contain a plugin specific part, since
        // this class is always extended by plugins using ShippingCommon
        return get_class($this);
    }

    /* Override */
    public function getName()
    {
        return $this->pluginInfo->getPluginDisplayName();
    }

    /**
     * @return PluginInfo
     */
    public function getPluginInfo()
    {
        return $this->pluginInfo;
    }

    /**
     * @return Util
     */
    public function getUtil()
    {
        return $this->util;
    }

    /**
     * The GDPR configuration is divided into specific fields (e.g. phone, email). Since this functionality started with
     * a singular GDPR config field, we still have this "general" GDPR function for backwards compatibility. This
     * function should not be used anymore. Instead use the other function that access the specific GDPR config field.
     *
     * @deprecated implement getGdprMailConfiguration and getGdprPhoneConfiguration instead.
     * @param Shop $shop
     * @return string|null One of the constants self::GDPR_* or null, if not configured
     */
    public function getGdprConfiguration(Shop $shop)
    {
        if (!$this->util->hasConfigField('isCustomerContactDataTransferAllowed')) {
            return null;
        }
        $isCustomerContactDataTransferAllowedInConfig = $this->util->config(
            $shop->getId(),
            'isCustomerContactDataTransferAllowed'
        );
        if ($isCustomerContactDataTransferAllowedInConfig === '0') {
            return ShippingProvider::GDPR_NEVER;
        }

        return ShippingProvider::GDPR_CUSTOMER_CHOICE;
    }

    /**
     * Returns the default GDPR configuration of the specific mail field.
     *
     * @param Shop $shop
     * @return string|null One of the constants self::GDPR_* or null, if not configured
     */
    public function getGdprMailConfiguration(Shop $shop)
    {
        return $this->getGdprConfiguration($shop);
    }

    /**
     * Returns the default GDPR configuration of the specific phone field.
     *
     * @param Shop $shop
     * @return string|null One of the constants self::GDPR_ALWAYS, self::GDPR_NEVER or null, if not configured
     */
    public function getGdprPhoneConfiguration(Shop $shop)
    {
        return ShippingProvider::GDPR_NEVER;
    }

    /**
     * @Override from ShippingProvider Interface
     *
     * @param int $orderId
     * @param null $shippingProductIdentifier
     * @param null $weight
     * @param null $dimensions
     * @param array $options
     * @param null $exportDocumentItems
     * @param null $address
     * @return string
     * @throws \Exception
     */
    public function addLabelToOrder($orderId, $shippingProductIdentifier = null, $weight = null, $dimensions = null, $options = array(), $exportDocumentItems = null, $address = null)
    {
        // Check the shipping product identifier
        if ($shippingProductIdentifier !== null) {
            // Add the product to the settings
            $settings = array(
                'product' => $shippingProductIdentifier,
                'createexportdocument' => ($options['createExportDocument'] === true)
            );
        }

        // No extra settings
        $extraSettings = array();

        // Determine the shipment weight
        if ($weight === null) {
            $weight = $this->determineShipmentWeight($orderId, $exportDocumentItems);
        }
        $shopId = $this->util->originatingShop($orderId);
        $useItemWeights = $this->util->config($shopId, 'calculateWeight');

        if ($address !== null) {
            $address = $this->util->addCountryISOAndStateShortCode($address);
        }

        if ($this->pluginInfo->getShipmentModelName() === null) {
            // Create label and get result
            $result = $this->shippingLabelGenerator->createLabel(
                $orderId,
                $weight,
                $useItemWeights,
                $address,
                $dimensions,
                $settings,
                $extraSettings,
                $exportDocumentItems
            );
            /**
             * @deprecated block, backwards compatibility
             */
            $identifierForDownload = $result->getNewTrackingCode();

            if (!isset($identifierForDownload)) {
                $this->util->log('No tracking code or shippingLabelId found. ShippingProvider::addLabelToOrder');
                throw new \Exception('An unexpected error occurred, the product does not have a Tracking Code or Label ID. Please try again!');
            }

            // Download the label and accompanying documents
            $this->shippingUtil->downloadDocuments($identifierForDownload);
        } else {
            // Get shipment information
            $shipment = $this->communication->createLabel(
                $orderId,
                $weight,
                false, // isReturn flag, at this point over the Picking app the return labels can't be created
                $useItemWeights,
                $address,
                $settings,
                $extraSettings,
                $dimensions,
                $exportDocumentItems
            );

            $result = ShippingLabelCreationResult::initializeFromShipment($shipment);
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function getDocument($orderId, $identifier)
    {
        // Get the document with the identifier from the dispatch service provider
        return $this->shippingUtil->getDocument($identifier);
    }

    /**
     * @inheritdoc
     */
    public function getAllDocumentDescriptions($orderId, $undispatchedOnly = false)
    {
        // Find all labels of this order considering the 'undispatched only' filter
        if ($undispatchedOnly) {
            /** @var \Shopware\Models\Attribute\Order $orderAttribute */
            $orderAttribute = Shopware()->Container()->get('models')->getRepository(
                'Shopware\Models\Attribute\Order'
            )->findOneBy(array(
                'orderId' => $orderId
            ));

            /** @var string $undispatchedTrackingCodes Fetch the tracking codes of undispatched labels first */
            $undispatchedTrackingCodes = $orderAttribute->getViisonUndispatchedTrackingCodes();
            if (empty($undispatchedTrackingCodes)) {
                // No undispatched labels, so no results
                return array();
            }

            if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
                $builder = Shopware()->Container()->get('models')->createQueryBuilder();
                $builder
                    ->select('document')
                    ->from($this->pluginInfo->getShipmentDocumentModelName(), 'document')
                    ->where('document.trackingCode IN (:trackingCodes)')
                    ->setParameter('trackingCodes', explode(',', $undispatchedTrackingCodes));
                $labels = $builder->getQuery()->getArrayResult();
            } else {
                /** @deprecated block, backwards compatibility */

                // Prepare the tracking codes for the SQL query
                $undispatchedTrackingCodes = implode(
                    ', ',
                    array_map(
                        function ($trackingCode) {
                            return '\'' . $trackingCode . '\'';
                        },
                        explode(',', $undispatchedTrackingCodes)
                    )
                );

                $orderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
                $labels = $orderTable->fetchAll(
                    $orderTable->select()
                        ->where('trackingCode IN (' . $undispatchedTrackingCodes . ')')
                )->toArray();
            }
        } elseif ($this->pluginInfo->getShipmentDocumentModelName() === null) {
            /** @deprecated block, backwards compatibility */
            $orderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
            // Just fetch all labels
            $labels = $orderTable->fetchAll(
                $orderTable->select()
                    ->where('orderId = ?', $orderId)
            )->toArray();
        } else {
            /** @var Shipment[] $shipments */
            $shipments = Shopware()->Container()->get('models')->getRepository(
                $this->pluginInfo->getShipmentModelName()
            )->findBy(array(
                'orderId' => $orderId
            ));

            // Create the document dictionaries based on the shipments
            $shippingDocuments = array();
            /** @var Shipment\Shipment $shipment */
            foreach ($shipments as $shipment) {
                /** @var Shipment\Document $document */
                foreach ($shipment->getDocuments() as $document) {
                    $shippingDocuments[] = new ShippingDocument(
                        $document->getType(),
                        $shipment->getTrackingCode(),
                        $document->getPageSize(),
                        $document->getDocumentTypeId(),
                        $document->getGuid()
                    );
                }
            }

            return $shippingDocuments;
        }

        // Create the document dictionaries based on the created labels and export documents
        $documents = array();
        foreach ($labels as $label) {
            // Add label
            $labelIdentifier = $this->util->getIdentifierFromLabelUrl($label['url']);
            $documents[] = new ShippingDocument(
                ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL,
                $label['trackingCode'],
                ($label['pageSize']) ?: 'A5'
            );

            if (!empty($label['exportDocumentUrl'])) {
                // Add export document as well (for now export documents are always A4)
                $labelIdentifier = $this->util->getIdentifierFromLabelUrl($label['exportDocumentUrl']);
                $documents[] = new ShippingDocument(
                    ShippingUtil::DOCUMENT_TYPE_EXPORT_DOCUMENT,
                    $label['trackingCode'],
                    'A4'
                );
            }
        }

        return $documents;
    }

    /**
     * @Override
     * @param string $identifier
     * @return bool
     */
    public function hasCreatedDocumentWithIdentifier($identifier)
    {
        if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
            // As identifier the guid of the shipping documents is used

            /** @var ModelManager $modelManager */
            $modelManager = Shopware()->Container()->get('models');
            $shippingDocument = $modelManager->getRepository($this->pluginInfo->getShipmentDocumentModelName())->findOneBy(array(
                'guid' => $identifier
            ));

            return $shippingDocument != null;
        } else {
            /**
             * @deprecated block, backwards compatibility
             */
            // Split the document identifier
            $splitPos = $this->util->getIdentifierPosition($identifier);
            $docType = substr($identifier, 0, $splitPos);
            $trackingCode = substr($identifier, ($splitPos + 1));
            if (!in_array($docType, array(ShippingUtil::DOCUMENT_TYPE_SHIPPING_LABEL, ShippingUtil::DOCUMENT_TYPE_EXPORT_DOCUMENT)) || strlen($trackingCode) == 0) {
                // Unknown document type / invalid tracking code
                return false;
            }
        }

        return $this->hasCreatedTrackingCode($trackingCode);
    }

    /**
     * @Override
     *
     * Note: Although the declaration and name of this method indicate the matching of tracking codes,
     *       this implementation also supports matching of document identifiers, which are not necessarily
     *       tracking codes. To match the ShippingProvider interface, the name of the method is not changed.
     *       However, internally the passed $trackingCode is evaluated in a way, that matches both tracking
     *       codes and document identifiers.
     */
    public function hasCreatedTrackingCode($trackingCode)
    {
        if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
            // Check if a shipment with the trackingCode exist
            $shipment = Shopware()->Container()->get('models')->getRepository(
                $this->pluginInfo->getShipmentModelName()
            )->findOneBy(array('trackingCode' => $trackingCode));

            return $shipment !== null;
        } else {
            /** @deprecated block, backwards compatibility */
            // Try to find the tracking code/document idenfifier in the database
            $result = $this->getShippingLabelInfoViaDocumentIdentifier($trackingCode);
        }

        return !empty($result);
    }

    /**
     * @override
     *
     * Note: Although the declaration and name of this method indicate the matching of tracking codes,
     *       this implementation also supports matching of document identifiers, which are not necessarily
     *       tracking codes. To match the ShippingProvider interface, the name of the method is not changed.
     *       However, internally the passed $trackingCode is evaluated in a way, that matches both tracking
     *       codes and document identifiers.
     *
     * @param string $trackingCode
     * @return string|null
     */
    public function getOrderIdForTrackingCode($trackingCode)
    {
        if ($this->getPluginInfo()->getShipmentModelName()) {
            /** @var ModelManager $modelManager */
            $modelManager = Shopware()->Container()->get('models');
            /** @var Shipment\Shipment $shipment */
            $shipment = $modelManager->getRepository($this->getPluginInfo()->getShipmentModelName())->findOneBy(array(
                'trackingCode' => $trackingCode
            ));
            if (!$shipment) {
                return null;
            }

            return ($shipment->getOrder()) ? $shipment->getOrder()->getId() : null;
        } else {
            /**
             * @deprecated block, backwards compatibility
             */
            // Try to find the tracking code/document idenfifier in the database
            $result = $this->getShippingLabelInfoViaDocumentIdentifier($trackingCode);

            return (!empty($result)) ? $result['orderId'] : null;
        }
    }

    /* Override */
    public function getOrdersIdsForTrackingCodeFragment($trackingCodeFragment)
    {
        if ($this->getPluginInfo()->getShipmentModelName()) {
            /** @var ModelManager $modelManager */
            $modelManager = Shopware()->Container()->get('models');
            $queryBuilder = $modelManager->createQueryBuilder();
            $queryBuilder
                ->select('shipment.orderId')
                ->from($this->getPluginInfo()->getShipmentModelName(), 'shipment')
                ->where('shipment.trackingCode LIKE :trackingCodeFragment AND shipment.orderId IS NOT NULL')
                ->setParameter('trackingCodeFragment', '%' . $trackingCodeFragment . '%');

            $results = $queryBuilder->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);

            return $results;
        } else {
            /**
             * @deprecated block, backwards compatibility
             */

            // Find all entries in the database matching the tracking code fragment
            $trackingCodeFragment = '%' . trim($trackingCodeFragment, '%') . '%';
            $orderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
            $result = $orderTable->fetchAll(
                $orderTable->select()->where('trackingCode LIKE ?', $trackingCodeFragment)
            );

            // Select only the order IDs
            $orderIds = array();
            foreach ($result as $row) {
                $orderIds[] = $row['orderId'];
            }

            return $orderIds;
        }
    }

    /**
     * @override
     * @param int $orderId
     * @return bool
     */
    public function isShippingSupported($orderId)
    {
        // Fetch product for order
        $product = $this->util->getProduct($orderId);

        // Shipping is supported, if a product was found
        return ($product !== null);
    }

    /**
     * @override
     * @param int $orderId
     * @param array $orderDetails
     * @return float|mixed
     */
    public function determineShipmentWeight($orderId, $orderDetails = null)
    {
        return $this->shippingUtil->determineShipmentWeight($orderId, $orderDetails);
    }

    /**
     * @override
     * @param int $orderId
     * @return bool
     */
    public function packageDimensionsRequired($orderId)
    {
        return $this->shippingUtil->packageDimensionsRequired($orderId);
    }

    /**
     * @override
     * @return array
     */
    public function validDispatchIds()
    {
        // Determine all product mappings that have a valid assignment to a dispatch method
        $validProductMappings = array_filter($this->util->getProductMappings(), function ($mapping) {
            return $mapping['productId'] != 0;
        });

        // Return the respective dispatch method IDs
        return array_map(function ($mapping) {
            return $mapping['dispatchId'];
        }, $validProductMappings);
    }

    /* Override */
    public function shippingProducts()
    {
        // Create a ShippingProduct for all available products
        $shippingUtil = $this->shippingUtil;
        $defaultPackageDimensions = $this->getDefaultPackageDimensions();
        $shippingProducts = array_map(function ($product) use ($shippingUtil, $defaultPackageDimensions) {
            return new ShippingProduct(
                $product['id'],
                $product['name'],
                ($product['packageDimensionsRequired'] === '1'),
                $shippingUtil->getProductOptions($product['id']),
                $defaultPackageDimensions
            );
        }, $this->util->getProducts());

        // Sort the products by name
        usort($shippingProducts, function ($lhs, $rhs) {
            return strcmp($lhs->getName(), $rhs->getName());
        });

        return $shippingProducts;
    }

    /* Override */
    public function shippingProductIdentifierForOrder($orderId)
    {
        // Get the dispatch service provider product for the order
        $product = $this->util->getProduct($orderId);

        return ($product !== null) ? $product['productId'] : null;
    }

    /* Override */
    public function shippingProductOptionsForOrder($orderId)
    {
        // Get the dispatch service provider product for the order
        $product = $this->util->getProduct($orderId);
        if ($product === null) {
            return array();
        }

        // Add and return the options
        return array(
            'createExportDocument' => $product['exportDocumentRequired']
        );
    }

    /**
     * @param string[] $trackingCodes
     * @return string
     */
    public function statusURLForTrackingCodes($trackingCodes)
    {
        return
            $this->pluginInfo->getTrackingURL() .
            implode($this->pluginInfo->getTrackingNumberDelimiter(), $trackingCodes) .
            $this->pluginInfo->additionalParametersForTrackingUrl($trackingCodes);
    }

    /**
     * @inheritdoc
     */
    public function statusUrlsForTrackingCodes(array $trackingCodes)
    {
        if ($this->pluginInfo->supportsMultipleTrackingCodesPerUrl()) {
            return array(
                $this->statusURLForTrackingCodes($trackingCodes)
            );
        } else {
            return array_map(function ($trackingCode) {
                return $this->statusURLForTrackingCodes(array($trackingCode));
            }, $trackingCodes);
        }
    }

    /**
     * Tries to find a shipping label matching the given document identifier. That is, a shipping label,
     * whose respective URL has the suffix ':$documentIdentifier'.
     *
     * @param string $documentIdentifier
     * @return array|null
     */
    protected function getShippingLabelInfoViaDocumentIdentifier($documentIdentifier)
    {
        // Try to find the url in the database
        $orderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
        $result = $orderTable->fetchRow(
            $orderTable->select()
                ->where('url LIKE ?', '%' . PluginInfo::LABEL_IDENTIFIER_SEPARATOR . $documentIdentifier)
                ->orWhere('trackingCode = ?', $documentIdentifier) // Fallback
        );

        return $result;
    }

    /**
     * @return array|null
     */
    protected function getDefaultPackageDimensions()
    {
        $shopId = Shopware()->Container()->get('models')->getRepository(Shop::class)->getActiveDefault()->getId();
        $width = intval($this->util->config($shopId, 'defaultPackageWidth', true));
        $height = intval($this->util->config($shopId, 'defaultPackageHeight', true));
        $length = intval($this->util->config($shopId, 'defaultPackageLength', true));
        if ($width === 0 && $height === 0 && $length === 0) {
            return null;
        }

        return [
            'width' => $width,
            'height' => $height,
            'length' => $length,
        ];
    }
}
