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

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

use GuzzleHttp6\Exception\GuzzleException;
use Paysera\Component\RestClientCommon\Entity\Filter;
use Paysera\Delivery\Entity\ShipmentRequest;
use Paysera\Delivery\Exception\ShipmentRequestGatewayNotFoundException;
use Paysera\Delivery\PayseraDeliveryConfiguration;
use Paysera\DeliveryApi\MerchantClient\Entity\Address as PayseraAddress;
use Paysera\DeliveryApi\MerchantClient\Entity\CityFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\Contact;
use Paysera\DeliveryApi\MerchantClient\Entity\Country as PayseraCountry;
use Paysera\DeliveryApi\MerchantClient\Entity\CountryFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\GatewaysFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\MethodsFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\Order;
use Paysera\DeliveryApi\MerchantClient\Entity\OrderCreate;
use Paysera\DeliveryApi\MerchantClient\Entity\OrderIdsList;
use Paysera\DeliveryApi\MerchantClient\Entity\OrderFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\OrderNotificationCreate;
use Paysera\DeliveryApi\MerchantClient\Entity\ParcelMachineFilter;
use Paysera\DeliveryApi\MerchantClient\Entity\Party;
use Paysera\DeliveryApi\MerchantClient\Entity\ShipmentCreate;
use Paysera\DeliveryApi\MerchantClient\Entity\ShipmentGateway;
use Paysera\DeliveryApi\MerchantClient\Entity\ShipmentMethod;
use Paysera\DeliveryApi\MerchantClient\Entity\ShipmentPointCreate;
use Paysera\DeliveryApi\MerchantClient\MerchantClient;

class DeliveryApiClient
{
    public const DELIVERY_ORDER_EVENT_UPDATED = 'order_updated';
    public const DELIVERY_ORDER_EVENTS = [
        self::DELIVERY_ORDER_EVENT_UPDATED,
    ];

    /**
     * @var MerchantClient
     */
    private $merchantClient;
    /**
     * @var Logger
     */
    private $logger;
    /**
     * @var string
     */
    private $projectIdHash;
    /**
     * @var string|null
     */
    private $orderNotificationUrl = null;

    public function __construct(MerchantClient $merchantClient, Logger $logger, PayseraDeliveryConfiguration $configuration)
    {
        $this->merchantClient = $merchantClient;
        $this->logger = $logger;
        $this->projectIdHash = $configuration->getResolvedProjectId() ?? $this->getResolvedProjectId();
    }

    public function getResolvedProjectId(): ?string
    {
        try {
            $projectIdHash = $this->merchantClient->getProjects(new Filter())->getList()[0]->getId();
        } catch (\Exception $exception) {
            $this->logger->error('Invalid Project ID.', $exception);

            return null;
        }

        return $projectIdHash;
    }

    public function setOrderNotificationUrl(string $orderNotificationUrl): self
    {
        $this->orderNotificationUrl = $orderNotificationUrl;

        return $this;
    }

    /**
     * @return ShipmentGateway[]
     */
    public function getDeliveryGateways(): array
    {
        $deliveryGateways = [];

        try {
            $deliveryGateways = $this->merchantClient
                ->updateGateway((new GatewaysFilter())->setProjectId($this->projectIdHash))
                ->getList()
            ;
        } catch (\Exception $exception) {
            $this->logger->error('Delivery gateway API can\'t be reached', $exception);
        }

        return $deliveryGateways;
    }

    /**
     * @return ShipmentMethod[]
     */
    public function getShipmentMethods(): array
    {
        $shipmentMethods = [];

        try {
            $shipmentMethods = $this->merchantClient
                ->updateMethod((new MethodsFilter())->setProjectId($this->projectIdHash))
                ->getList()
            ;
        } catch (\Exception $exception) {
            $this->logger->error('Delivery gateway API can\'t be reached.', $exception);
        }

        return $shipmentMethods;
    }

    /**
     * @param string $deliveryGatewayCode
     *
     * @return PayseraCountry[]
     *
     * @throws GuzzleException
     */
    public function getCountries(string $deliveryGatewayCode): array
    {
        try {
            return $this->merchantClient
                ->getCountries((new CountryFilter())->setShipmentGatewayCode($deliveryGatewayCode))
                ->getItems()
            ;

        } catch (\Exception $exception) {
            $this->logger->error('Can\'t get countries.', $exception);

            return [];
        }
    }

    /**
     * @param string $deliveryGatewayCode
     * @param string $countryCode
     * @param string $city
     *
     * @return array<string, string>
     *
     * @throws GuzzleException
     */
    public function getTerminalLocations(string $deliveryGatewayCode, string $countryCode, string $city): array
    {
        $parcelMachineFilter = (new ParcelMachineFilter())
            ->setShipmentGatewayCode($deliveryGatewayCode)
            ->setCountry($countryCode)
            ->setCity($city)
        ;

        try {
            $parcelMachines = $this->merchantClient->getParcelMachines($parcelMachineFilter)->getList();
        } catch (\Exception $exception) {
            $this->logger->error('Can\'t get terminal locations.', $exception);

            return [];
        }

        $locations = [];

        foreach ($parcelMachines as $parcelMachine) {
            $locations[$parcelMachine->getId()] =
                $parcelMachine->getAddress()->getStreet() . ', ' . $parcelMachine->getLocationName()
            ;
        }

        asort($locations);

        return $locations;
    }

    /**
     * @param string $deliveryGatewayCode
     * @param string $countryCode
     *
     * @return string[]
     *
     * @throws GuzzleException
     */
    public function getCities(string $deliveryGatewayCode, string $countryCode): array
    {

        try {
            $cities = $this->merchantClient
                ->getCities((new CityFilter())->setCountry($countryCode)->setGatewayCode($deliveryGatewayCode))
                ->getItems()
            ;
        } catch (\Exception $exception) {
            $this->logger->error('Can\'t get cities.', $exception);

            return [];
        }

        $normalizedCities = [];

        foreach ($cities as $city) {
            $normalizedCities[] = $city->getName();
        }

        asort($normalizedCities);

        return $normalizedCities;
    }

    /**
     * @param int $orderId
     * @param array<string, int|string> $carrierInfo
     * @param \Address $address
     * @param \Customer $customer
     * @param array<int, array<string, mixed>> $products
     * @param ShipmentRequest $shipmentRequest
     *
     * @return Order|null
     *
     * @throws GuzzleException
     * @throws ShipmentRequestGatewayNotFoundException
     */
    public function createOrder(
        int $orderId,
        array $carrierInfo,
        \Address $address,
        \Customer $customer,
        array $products,
        ShipmentRequest $shipmentRequest
    ): ?Order {
        $shipments = [];

        foreach ($products as $item) {
            for ($productQuantity = 1; $productQuantity <= $item['product_quantity']; ++$productQuantity) {
                $shipments[] = (new ShipmentCreate())
                    ->setWeight((int) ($item['weight'] * 1000))
                    ->setLength((int) ($item['depth'] * 10))
                    ->setWidth((int) ($item['width'] * 10))
                    ->setHeight((int) ($item['height'] * 10))
                ;
            }
        }

        try {
            $receiverParty = (new Party())
                ->setTitle($address->firstname . ' ' . $address->lastname)
                ->setEmail($customer->email !== null ? $customer->email : '')
                ->setPhone($address->phone !== null ? $address->phone : '')
            ;
        } catch (\Exception $exception) {
            $this->logger->error('Receiver party creation error.', $exception);

            return null;
        }

        try {
            if ($carrierInfo['type'] === PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE) {
                $context = \Context::getContext();

                if ($context === null) {
                    throw new \Exception('Context is null'); // TODO: PhpBasic convention 3.20.1: We almost never throw base \Exception class
                }

                $shipmentRequestGateway = $shipmentRequest->getShippingRequestGateway();

                if ($shipmentRequestGateway === null) {
                    throw new ShipmentRequestGatewayNotFoundException();
                }

                $receiverAddress = (new PayseraAddress())
                    ->setCountry($shipmentRequestGateway->getIsoCode2())
                    ->setCity($shipmentRequestGateway->getCity())
                ;
            } else {
                $receiverAddress = (new PayseraAddress())
                    ->setCountry((new \Country($address->id_country))->iso_code)
                    ->setCity($address->city)
                    ->setStreet($address->address1 . ' ' . $address->address2)
                    ->setPostalCode($address->postcode)
                ;
            }
        } catch (\Exception $exception) {
            $this->logger->error('Receiver address creation error.', $exception);

            return null;
        }

        try {
            $receiverContact = (new Contact())->setParty($receiverParty)->setAddress($receiverAddress);
        } catch (\Exception $exception) {
            $this->logger->error('Receiver contact creation error.', $exception);

            return null;
        }

        try {
            $orderCreate = (new OrderCreate())
                ->setProjectId($this->projectIdHash)
                ->setShipmentGatewayCode((string) $carrierInfo['code'])
                ->setShipmentMethodCode((string) $carrierInfo['shipment_code'])
                ->setShipments($shipments)
                ->setReceiver(
                    $this->createOrderParty(
                        (string) $carrierInfo['type'],
                        'receiver',
                        $receiverContact,
                        $shipmentRequest
                    )
                )
                ->setEshopOrderId((string) $orderId);

            if ($this->orderNotificationUrl !== null) {
                $orderCreate->setOrderNotification(
                    (new OrderNotificationCreate())
                        ->setEvents(self::DELIVERY_ORDER_EVENTS)
                        ->setUrl($this->orderNotificationUrl)
                );
            }
        } catch (\Exception $exception) {
            $this->logger->error('Order creation error.', $exception);

            return null;
        }

        try {
            return $this->merchantClient->createOrder($orderCreate);
        } catch (\Exception $exception) {
            $this->logger->error('Order creation error.', $exception);

            return null;
        }
    }

    public function getOrder(string $orderId): ?Order
    {
        try {
            return $this->merchantClient->getOrder($orderId);
        } catch (\Exception $exception) {
            $this->logger->error('Order getting error.', $exception);

            return null;
        }
    }

    private function createOrderParty(
        string $orderPartyMethod,
        string $type,
        ?Contact $contact,
        ShipmentRequest $shipmentRequest
    ): ShipmentPointCreate {
        $orderParty = (new ShipmentPointCreate())
            ->setType($type)
            ->setSaved(false)
            ->setDefaultContact(false)
        ;

        if ($this->projectIdHash !== null) {
            $orderParty->setProjectId($this->projectIdHash);
        }

        $orderParty->setContact($contact);

        if (
            $orderPartyMethod === PayseraDeliveryConfiguration::TYPE_PARCEL_MACHINE
            && $type === PayseraDeliveryConfiguration::TYPE_RECEIVER
        ) {
            $context = \Context::getContext();

            if ($context === null) {
                throw new \Exception('Context is null'); // TODO: PhpBasic convention 3.20.1: We almost never throw base \Exception class
            }

            $shipmentRequestGateway = $shipmentRequest->getShippingRequestGateway();

            if ($shipmentRequestGateway === null) {
                throw new ShipmentRequestGatewayNotFoundException();
            }

            $orderParty->setParcelMachineId($shipmentRequestGateway->getTerminal());
        }

        return $orderParty;
    }

    /**
     * @param ShipmentRequest $shipmentRequest
     *
     * @return void
     */
    public function setOrderStatusPrepaid(ShipmentRequest $shipmentRequest): void
    {

        try {
            $this->merchantClient->createOrdersPrepaid(
                $this->getOrderIdsListFromShipmentRequest($shipmentRequest)
            );
        } catch (\Exception $exception) {
            $this->logger->error(
                sprintf(
                    'Cannot set order status to prepaid for order ID: %s. %s',
                    $shipmentRequest->getDeliveryOrderApiId(),
                    $exception->getMessage()
                ),
                $exception);

            return;
        }
    }

    private function getOrderIdsListFromShipmentRequest(ShipmentRequest $shipmentRequest): OrderIdsList
    {
        return new OrderIdsList([
            'order_ids' => [
                $shipmentRequest->getDeliveryOrderApiId(),
            ]
        ]);
    }
}
