<?php
/**
 * 2018 Paysera
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * @author Paysera <plugins@paysera.com>
 * @copyright 2018 Paysera
 * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
 * International Registered Trademark & Property of Paysera
 */

declare(strict_types=1);

namespace Paysera\Delivery\HookHandler\OrderHandler;

if (!defined('_PS_VERSION_')) {
    exit;
}

use Exception;
use Paysera\Delivery\Entity\MerchantCarrierInterface;
use Paysera\Delivery\Entity\MerchantOrderInterface;
use Paysera\Delivery\Entity\ShipmentRequest;
use Paysera\Delivery\Entity\ShipmentRequestGateway;
use Paysera\Delivery\Factory\MerchantOrderAddressFactoryInterface;
use Paysera\Delivery\HookHandler\HookHandlerParameters;
use Paysera\Delivery\Model\MerchantCarrierModelInterface;
use Paysera\Delivery\Model\MerchantCartModelInterface;
use Paysera\Delivery\Model\MerchantCountryModelInterface;
use Paysera\Delivery\Model\MerchantOrderAddressModelInterface;
use Paysera\Delivery\Model\MerchantOrderModelInterface;
use Paysera\Delivery\Model\PayseraShippingGatewayModelInterface;
use Paysera\Delivery\Model\ShipmentRequestModel;
use Paysera\Delivery\PayseraDeliveryCarriers;
use Paysera\Delivery\PayseraDeliveryConfiguration;
use Paysera\Delivery\Service\DeliveryApiClient;
use Paysera\Delivery\Service\DeliveryOrderMessageGenerator;
use Paysera\DeliveryApi\MerchantClient\Entity\Order as DeliveryOrder;

class DeliveryOrderUpdatedHandler
{
    /**
     * @var PayseraDeliveryConfiguration
     */
    private $configuration;
    /**
     * @var DeliveryApiClient
     */
    private $deliveryApiClient;
    /**
     * @var DeliveryOrderMessageGenerator
     */
    private $deliveryOrderMessageGenerator;
    /**
     * @var ShipmentRequestModel
     */
    private $shipmentRequestModel;
    /**
     * @var PayseraDeliveryCarriers
     */
    private $deliveryCarriers;
    /**
     * @var ShipmentRequest
     */
    private $shipmentRequest;
    /**
     * @var MerchantOrderModelInterface
     */
    private $merchantOrderModel;
    /**
     * @var MerchantOrderAddressModelInterface
     */
    private $merchantOrderAddressModel;
    /**
     * @var MerchantOrderAddressFactoryInterface
     */
    private $merchantOrderAddressFactory;
    /**
     * @var MerchantCartModelInterface
     */
    private $merchantCartModel;
    /**
     * @var MerchantCountryModelInterface
     */
    private $merchantCountryModel;
    /**
     * @var PayseraShippingGatewayModelInterface
     */
    private $payseraShippingGatewayModel;
    /**
     * @var MerchantCarrierModelInterface
     */
    private $merchantCarrierModel;

    public function __construct(
        PayseraDeliveryConfiguration $configuration,
        DeliveryApiClient $deliveryApiClient,
        DeliveryOrderMessageGenerator $deliveryOrderMessageGenerator,
        ShipmentRequestModel $shipmentRequestModel,
        PayseraDeliveryCarriers $deliveryCarriers,
        MerchantOrderModelInterface $merchantOrderModel,
        MerchantOrderAddressModelInterface $merchantOrderAddressModel,
        MerchantOrderAddressFactoryInterface $merchantOrderAddressFactory,
        MerchantCartModelInterface $merchantCartModel,
        MerchantCountryModelInterface $merchantCountryModel,
        PayseraShippingGatewayModelInterface $payseraShippingGatewayModel,
        MerchantCarrierModelInterface $merchantCarrierModel
    ) {
        $this->configuration = $configuration;
        $this->deliveryApiClient = $deliveryApiClient;
        $this->deliveryOrderMessageGenerator = $deliveryOrderMessageGenerator;
        $this->shipmentRequestModel = $shipmentRequestModel;
        $this->deliveryCarriers = $deliveryCarriers;
        $this->merchantOrderModel = $merchantOrderModel;
        $this->merchantOrderAddressModel = $merchantOrderAddressModel;
        $this->merchantOrderAddressFactory = $merchantOrderAddressFactory;
        $this->merchantCartModel = $merchantCartModel;
        $this->merchantCountryModel = $merchantCountryModel;
        $this->payseraShippingGatewayModel = $payseraShippingGatewayModel;
        $this->merchantCarrierModel = $merchantCarrierModel;
    }

    public function handle(array $parameters): void
    {
        /** @var MerchantOrderInterface $order */
        $order = $parameters['order'] ?? null;

        if ($order === null) {
            return;
        }

        $this->shipmentRequest = $this->shipmentRequestModel->find($order->getId());

        if ($this->shipmentRequest === null || $this->shipmentRequest->getDeliveryOrderApiId() === null) {
            throw new Exception('Delivery Order ID not found');
        }

        $deliverOrder = $this->deliveryApiClient->getOrder($this->shipmentRequest->getDeliveryOrderApiId());

        if (!$deliverOrder) {
            throw new Exception('Failed to fetch delivery order');
        }

        $this->updateAddress($order, $deliverOrder);
        $this->updateDeliveryGateway($order, $deliverOrder);
    }

    private function updateAddress(MerchantOrderInterface $merchantOrder, DeliveryOrder $deliveryOrder): void
    {
        $receiver = $deliveryOrder->getReceiver();

        if ($receiver === null) {
            return;
        }

        $contact = $receiver->getContact();

        if ($contact === null) {
            return;
        }

        $contactShippingInfo = $contact->getParty();
        $shippingAddress = $contact->getAddress();

        $currentAddress = $this->merchantOrderAddressModel->find($merchantOrder->getShippingAddressId());
        $previousRecipientData = [
            'shipping_phone' => $currentAddress->getPhone(),
            'shipping_country' => $currentAddress->getIdCountry(),
            'shipping_city' => $currentAddress->getCity(),
            'shipping_street' => $currentAddress->getStreet(),
            'shipping_postcode' => $currentAddress->getPostcode(),
        ];

        $actualRecipientData = [
            'shipping_phone' => (string)$contactShippingInfo->getPhone(),
            'shipping_country' => $this->merchantCountryModel->getIdByIso($shippingAddress->getCountry()),
            'shipping_city' => (string)$shippingAddress->getCity(),
            'shipping_street' => (string)$shippingAddress->getStreet(),
            'shipping_postcode' => (string)$shippingAddress->getPostalCode(),
        ];

        if (
            $deliveryOrder->getShipmentMethod()->getReceiverCode() === PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE
        ) {
            $parcelMachine = $deliveryOrder->getReceiver()->getParcelMachine();

            $actualRecipientData['shipping_street'] = $parcelMachine === null
                ? 'none'
                : (string)$parcelMachine->getAddress()->getStreet();
        }

        $diffKeys = array_keys(
            array_diff_assoc(
                array_map('trim', $previousRecipientData),
                array_map('trim', $actualRecipientData)
            )
        );

        $diff = array_filter(
            $actualRecipientData,
            function ($key) use ($diffKeys) {
                return in_array($key, $diffKeys);
            },
            ARRAY_FILTER_USE_KEY
        );

        if (empty($diff)) {
            return;
        }

        $newAddress = $this->merchantOrderAddressFactory->create()
            ->setIdCustomer($currentAddress->getIdCustomer())
            ->setIdCountry($diff['shipping_country'] ?? $currentAddress->getIdCountry())
            ->setAlias('Order #' . $merchantOrder->getId())
            ->setFirstname($currentAddress->getFirstname())
            ->setLastname($currentAddress->getLastname())
            ->setStreet($diff['shipping_street'] ?? $currentAddress->getStreet())
            ->setPostcode($diff['shipping_postcode'] ?? $currentAddress->getPostcode())
            ->setCity($diff['shipping_city'] ?? $currentAddress->getCity())
            ->setPhone($diff['shipping_phone'] ?? $currentAddress->getPhone())
        ;

        if (!$this->merchantOrderAddressModel->save($newAddress)) {
            throw new Exception('Failed to save new address');
        }

        $merchantOrder->setShippingAddressId($newAddress->getId());

        if (!$this->merchantOrderModel->update($merchantOrder)) {
            throw new Exception('Failed to update order with new delivery address');
        }

        [$previousRecipientData, $actualRecipientData] = $this->extractRecipientDataChanges(
            $previousRecipientData,
            $actualRecipientData
        );

        $this->deliveryOrderMessageGenerator->generateShippingAddressChangedMessage(
            $previousRecipientData,
            $actualRecipientData
        );
    }

    private function extractRecipientDataChanges(array $previousData, array $currentData): array
    {
        $changedData = array_diff_assoc($currentData, $previousData);
        $changedFields = array_keys($changedData);
        $changedDataPreviousValues = array_filter(
            $previousData,
            function (string $key) use ($changedFields): bool
            {
                return in_array($key, $changedFields, true);
            },
            ARRAY_FILTER_USE_KEY
        );

        return [
            $changedDataPreviousValues,
            $changedData,
        ];
    }

    private function updateDeliveryGateway(MerchantOrderInterface $order, DeliveryOrder $deliveryOrder): void
    {
        $deliveryGatewayId = $this->getMatchedActiveGatewayForDeliveryOrder($deliveryOrder);

        if ($deliveryGatewayId === null) {
            $this->deliveryOrderMessageGenerator->generateShippingMethodNotFoundMessage($deliveryOrder->getNumber());

            return;
        }

        $actualCarrierInfo = $this->deliveryCarriers->getCarrierById($order->getCarrierId());

        if ($actualCarrierInfo === null) {
            return;
        }

        if ($order->getCarrierId() !== $deliveryGatewayId) {
            $this->changeDeliveryGateway($order, $this->merchantCarrierModel->find($deliveryGatewayId));
        }

        $newCarrierInfo = $this->deliveryCarriers->getCarrierById($deliveryGatewayId);

        if (
            in_array(
                $newCarrierInfo['type'],
                [
                    PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE,
                    PayseraDeliveryConfiguration::TYPE_TERMINALS,
                ]
            )
        ) {
            $this->updateParcelMachine($deliveryOrder);
        } else {
            $this->deleteParcelMachine();
        }
    }

    private function changeDeliveryGateway(MerchantOrderInterface $order, MerchantCarrierInterface $deliveryGateway): void
    {
        $order->setCarrierId($deliveryGateway->getId());
        $cart = $this->merchantCartModel
            ->find($order->getCartId())
            ->setCarrierId($deliveryGateway->getId())
            ->setAddressId($order->getShippingAddressId())
        ;

        $cart = $this->merchantCartModel->refreshItems($cart, $order);

        $diffShippingTaxIncl = $order->getTotalShippingTaxIncl() - $cart->getShippingTotal(true);
        $diffShippingTaxExcl = $order->getTotalShippingTaxExcl() - $cart->getShippingTotal(false);

        $order
            ->setTotalShippingTaxIncl(
                $order->getTotalShippingTaxIncl() - $diffShippingTaxIncl
            )
            ->setTotalShippingTaxExcl(
                $order->getTotalShippingTaxExcl() - $diffShippingTaxExcl
            )
            ->setTotalShipping($order->getTotalShippingTaxIncl())
            ->setTotalPaidTaxIncl(
                $order->getTotalPaidTaxIncl() - $diffShippingTaxIncl
            )
            ->setTotalPaidTaxExcl(
                $order->getTotalPaidTaxExcl() - $diffShippingTaxExcl
            )
            ->setTotalPaid($order->getTotalPaidTaxIncl())
        ;

        if ($this->merchantOrderModel->update($order)) {
            $this->merchantOrderModel->updateCarrier($order);
            $this->deliveryOrderMessageGenerator->generateShippingMethodIsChangedMessage($deliveryGateway->getName());
        }
    }

    private function getMatchedActiveGatewayForDeliveryOrder(DeliveryOrder $deliveryOrder): ?int
    {
        $deliveryGatewayCode = $this->getGatewayCodeFromDeliveryOrder($deliveryOrder);

        return $this->payseraShippingGatewayModel->findIdByGatewayCode($deliveryGatewayCode);
    }

    public function getGatewayCodeFromDeliveryOrder(DeliveryOrder $order): string
    {
        $receiverCode = $order->getShipmentMethod()->getReceiverCode();
        $shipmentMethodCode = PayseraDeliveryConfiguration::TYPE_COURIER;

        if (
            in_array(
                $receiverCode,
                [PayseraDeliveryConfiguration::TYPE_TERMINALS, PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE]
            )
        ) {
            $shipmentMethodCode = PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE;
        }

        return sprintf('%s_%s', $order->getShipmentGateway()->getCode(), $shipmentMethodCode);
    }

    private function updateParcelMachine(DeliveryOrder $deliveryOrder): void
    {
        $parcelMachine = $deliveryOrder->getReceiver()->getParcelMachine();

        if ($parcelMachine === null) {
            return;
        }

        $address = $parcelMachine->getAddress();
        $terminalId = $parcelMachine->getId();
        $actualTerminal = $this->shipmentRequest->getShippingRequestGateway();

        if ($terminalId === $actualTerminal->getTerminal()) {
            return;
        }

        $newTerminal = new ShipmentRequestGateway(
            $address->getCountry(),
            $address->getCity(),
            $terminalId
        );

        if ($address->getCountry() === $actualTerminal->getIsoCode2()) {
            $newTerminal->setIsoCode2($actualTerminal->getIsoCode2());
        }

        if ($address->getCity() === $actualTerminal->getCity()) {
            $newTerminal->setCity($actualTerminal->getCity());
        }

        $this->shipmentRequest->setShippingRequestGateway($newTerminal);

        if ($this->shipmentRequestModel->update($this->shipmentRequest)) {
            $this->deliveryOrderMessageGenerator->generateTerminalDeliveryOrderChangedMessage(
                $deliveryOrder->getShipmentGateway()->getDescription(),
                $deliveryOrder->getShipmentMethod()->getDescription(),
                $parcelMachine->getAddress()->getStreet(),
                $parcelMachine->getLocationName()
            );
        }
    }

    private function deleteParcelMachine(): void
    {
        $this->shipmentRequest->setShippingRequestGateway(null);
        $this->shipmentRequestModel->update($this->shipmentRequest);
    }
}
