<?php

/**
 * Copyright © 2016 Filoblu S.r.l. All rights reserved.
 */

namespace FiloBlu\Esb\Model\Rest;

use Exception;
use FiloBlu\Esb\Api\Data\AcknowledgeInterface;
use FiloBlu\Esb\Api\ExternalShipmentInterface;
use FiloBlu\Esb\Helper\LoggerProvider;
use Magento\Catalog\Model\Product\Type;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ProductMetadataInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\MailException;
use Magento\Framework\Module\Manager;
use Magento\Framework\Phrase;
use Magento\InventoryApi\Api\Data\StockInterface;
use Magento\InventorySales\Model\ResourceModel\StockIdResolver;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterface;
use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterfaceFactory;
use Magento\InventorySourceSelectionApi\Api\Data\ItemRequestInterfaceFactory;
use Magento\InventorySourceSelectionApi\Api\GetDefaultSourceSelectionAlgorithmCodeInterface;
use Magento\InventorySourceSelectionApi\Api\SourceSelectionServiceInterface;
use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\ShipmentExtensionInterface;
use Magento\Sales\Api\Data\ShipmentInterface;
use Magento\Sales\Api\Data\ShipmentTrackInterface;
use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory;
use Magento\Sales\Api\ShipmentRepositoryInterface;
use Magento\Sales\Exception\CouldNotShipException;
use Magento\Sales\Model\Convert\Order;
use Magento\Sales\Model\Order\Config as OrderConfig;
use Magento\Sales\Model\Order\OrderStateResolverInterface;
use Magento\Sales\Model\Order\Shipment\Item;
use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface;
use Magento\Sales\Model\Order\ShipmentFactory;
use Magento\Sales\Model\OrderRepository;
use Magento\Shipping\Model\ShipmentNotifier;
use Magento\Store\Model\StoreManagerInterface;
use Monolog\Logger;
use RuntimeException;
use Magento\Framework\Event\ManagerInterface as EventManager;

/**
 * REST Api to save external shipments
 */
class ExternalShipment implements ExternalShipmentInterface
{
    /** @var string */
    const CONFIG_SELECTION_ALGORITHM = 'esb/shipment/source_selection_algorithm';

    /**
     * @var OrderRepository
     */
    protected $orderRepository;

    /**
     * @var ShipmentFactory
     */
    protected $shipmentFactory;

    /**
     * @var SearchCriteriaBuilder
     */
    protected $searchCriteriaBuilder;

    /**
     * @var ShipmentFactory
     */
    protected $shipmentNotifier;

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * @var OrderExtensionFactory
     */
    protected $orderExtensionFactory;

    /**
     * @var Logger
     */
    protected $logger;

    /**
     * @var AcknowledgeInterface
     */
    protected $response;

    /**
     * @var ShipmentRepositoryInterface
     */
    private $shipmentRepository;

    /**
     * @var ProductMetadataInterface
     */
    private $productMetadata;

    /**
     * @var ShipmentTrackInterfaceFactory
     */
    private $shipmentTrackInterfaceFactory;
    /**
     * @var ResourceConnection
     */
    private $resourceConnection;
    /**
     * @var OrderRegistrarInterface
     */
    private $orderRegistrar;
    /**
     * @var OrderStateResolverInterface
     */
    private $orderStateResolver;
    /**
     * @var OrderConfig
     */
    private $config;
    /**
     * @var Manager
     */
    private $manager;
    /**
     * @var EventManager
     */
    protected $_eventManager;

    /**
     * Init plugin
     *
     * @param ScopeConfigInterface $scopeConfig
     * @param OrderRepository $orderRepository
     * @param ShipmentFactory $shipmentFactory
     * @param OrderExtensionFactory $orderExtensionFactory
     * @param ShipmentNotifier $shipmentNotifier
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param LoggerProvider $loggerProvider
     * @param ShipmentRepositoryInterface $shipmentRepository
     * @param ProductMetadataInterface $productMetadata
     * @param AcknowledgeInterface $response
     * @param ShipmentTrackInterfaceFactory $shipmentTrackInterfaceFactory
     * @param OrderRegistrarInterface $orderRegistrar
     * @param OrderStateResolverInterface $orderStateResolver
     * @param OrderConfig $config
     * @param ResourceConnection $resourceConnection
     * @param Manager $manager
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        OrderRepository $orderRepository,
        ShipmentFactory $shipmentFactory,
        OrderExtensionFactory $orderExtensionFactory,
        ShipmentNotifier $shipmentNotifier,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        LoggerProvider $loggerProvider,
        ShipmentRepositoryInterface $shipmentRepository,
        ProductMetadataInterface $productMetadata,
        AcknowledgeInterface $response,
        ShipmentTrackInterfaceFactory $shipmentTrackInterfaceFactory,
        OrderRegistrarInterface $orderRegistrar,
        OrderStateResolverInterface $orderStateResolver,
        OrderConfig $config,
        ResourceConnection $resourceConnection,
        Manager $manager,
        EventManager $eventManager
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->orderRepository = $orderRepository;
        $this->shipmentFactory = $shipmentFactory;
        $this->orderExtensionFactory = $orderExtensionFactory;
        $this->shipmentNotifier = $shipmentNotifier;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->logger = $loggerProvider->getLogger();
        $this->response = $response;
        $this->shipmentRepository = $shipmentRepository;
        $this->productMetadata = $productMetadata;
        $this->shipmentTrackInterfaceFactory = $shipmentTrackInterfaceFactory;
        $this->resourceConnection = $resourceConnection;
        $this->orderRegistrar = $orderRegistrar;
        $this->orderStateResolver = $orderStateResolver;
        $this->config = $config;
        $this->manager = $manager;
        $this->_eventManager = $eventManager;
    }

    /**
     * Save External Shipment data. Save PDF file and additional data on Sales Order.
     *
     * @param ShipmentInterface $shipmentData
     * @return AcknowledgeInterface
     * @throws Exception
     */
    public function save(ShipmentInterface $shipmentData)
    {
        $shipmentExtensionAttributes = $shipmentData->getExtensionAttributes();

        if (null === $shipmentExtensionAttributes || !$shipmentExtensionAttributes->getOrderIncrementId()) {
            $message = __('Cannot find order to ship');
            $this->logger->critical($message->render());
            throw new CouldNotShipException(new Phrase($message->render()));
        }

        $orderIncrementId = $shipmentExtensionAttributes->getOrderIncrementId();
        $carrierCode = $shipmentExtensionAttributes->getCarrierCode();
        if (!$carrierCode) {
            $carrierCode = 'ups';
        }
        $carrierTitle = $shipmentExtensionAttributes->getCarrierTitle();
        if (!$carrierTitle) {
            $carrierTitle = 'UPS';
        }
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('increment_id', $orderIncrementId, 'eq')->create();
        $orderList = $this->orderRepository->getList($searchCriteria);
        if (count($orderList) !== 1) {
            $message = __('Cannot find order %1', $orderIncrementId);
            $this->logger->critical($message->render());
            throw new CouldNotShipException(new Phrase($message->render()));
        }
        try {
            /** @var OrderInterface $order */
            $order = $orderList->getFirstItem();

            //Check if order is already shipped

            if($order->getStatus() === 'complete'){
                $assignedTracks = $this->assingTrackNumbersToShipment($order, $shipmentData, $carrierCode, $carrierTitle);

                $this->response->setStatus('skipped');
                $this->response->setOrderIncrementId($order->getIncrementId());

                if(empty($assignedTracks)){
                    $this->response->setMessage(__('Order %1 has already been shipped.', $order->getIncrementId()));
                    return  $this->response;
                }

                $this->response->setMessage(__('Order %1 has already been shipped, only tracks were added', $order->getIncrementId()));

                return $this->response;
            }

            $tracks = $shipmentData->getTracks();

            $shipmentTracks = [];
            $numbers = [];
            if ($tracks) {
                foreach ($tracks as $track) {
                    $shipmentTrack = $this->shipmentTrackInterfaceFactory->create();
                    $shipmentTrack->setOrderId($order->getId());
                    $shipmentTrack->setCarrierCode($carrierCode);
                    $shipmentTrack->setTitle($carrierTitle);
                    $shipmentTrack->setTrackNumber($track->getTrackNumber());
                    $numbers[] = $track->getTrackNumber();
                    $shipmentTracks[] = $shipmentTrack;
                }
            }

            $this->createShipment($order, $shipmentTracks, $shipmentData);

            $this->response->setStatus('success');
            $this->response->setMessage(
                __('Order %1 has been shipped:  %2', $order->getIncrementId(), implode(',', $numbers))
            );
            $this->response->setFlow('update_shipment');
            $this->response->setOrderIncrementId($order->getIncrementId());
            return $this->response;
        } catch (Exception $e) {
            $this->logger->critical($e->getMessage(), ['exception', $e]);
            throw $e;
        }
    }

    /**
     * Create shipment
     *
     * @param OrderInterface $order
     * @param ShipmentTrackInterface [] $shipmentTracks
     * @param ShipmentInterface $requestData
     * @throws CouldNotShipException
     * @throws MailException
     * @see \Magento\Sales\Model\ShipOrder
     */
    protected function createShipment(OrderInterface $order, $shipmentTracks, ShipmentInterface $requestData)
    {
        $this->checkIsShippable($order);

        $objectManager = ObjectManager::getInstance();

        $convertOrder = $objectManager->create(Order::class);
        $shipment = $convertOrder->toShipment($order);

        foreach ($shipmentTracks as $track) {
            $shipment->addTrack($track);
        }

        $shippedItems = [];
        $requestedOrderItems = [];

        $requestedSku =[];
        $orderSku =[];

        $shipment_items = $requestData->getItems();

        foreach ($order->getItems() as $order_item) {

            $orderSku[$order_item->getSku()] = $order_item->getSku();

            foreach (($shipment_items ?? []) as $key => $shipment_item) {
                if ($order_item->getTypeId() != "configurable" && $order_item->getTypeId() != "bundle") {

                    $requestedSku[$shipment_item->getSku()] = $shipment_item->getSku();

                    if($shipment_item->getOrderItemId()){
                        if($shipment_item->getOrderItemId() == $order_item->getId()){
                            $requestedOrderItems[$order_item->getId()] = $shipment_item->getQty();
                            break;
                        }
                    }else{
                        if($shipment_item->getSku() === $order_item->getSku()){
                            $requestedOrderItems[$order_item->getId()] = $shipment_item->getQty();
                            break;
                        }
                    }

                    //unset($shipment_items[$key]);
                }
            }
        }

        $wrongSkus = array_diff($requestedSku, $orderSku);

        if(!empty($wrongSkus))
        {
            throw new RuntimeException(__("Invalid sku found %1", implode(',' , $wrongSkus)));
        }

        foreach ($order->getAllItems() as $orderItem) {

            $qtyToShip = $orderItem->getQtyToShip();
            if (!$orderItem->getQtyToShip() || $orderItem->getIsVirtual()) {
                continue;
            }

            $isRequestedItem = array_key_exists($orderItem->getId(), $requestedOrderItems);
            if (!empty($requestedOrderItems) && !$isRequestedItem) {
                continue;
            }

            if ($isRequestedItem) {
                $qtyToShip = $requestedOrderItems[$orderItem->getId()];
            }

            if ($orderItem->getProductType() === Type::DEFAULT_TYPE) {
                $shippedItems[$orderItem->getProduct()->getSku()] = $orderItem->getSimpleQtyToShip();
            } else {
                $shippedItems[$orderItem->getSku()] = $orderItem->getSimpleQtyToShip();
            }

            $shipmentItem = $convertOrder->itemToShipmentItem($orderItem)->setQty($qtyToShip);
            $shipment->addItem($shipmentItem);
        }

        $this->addMultiStockInformationIfNeeded($shipment, $order, $shippedItems, $requestData);

        $connection = $this->resourceConnection->getConnection('sales');
        $connection->beginTransaction();

        try {
            $this->orderRegistrar->register($order, $shipment);
            $order->setState(
                $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS])
            );

            $order->setStatus($this->config->getStateDefaultStatus($order->getState()));
            $this->shipmentRepository->save($shipment);

            if ($shipmentTracks && !empty($shipmentTracks)) {
                foreach ($shipmentTracks as $track) {
                    $order->addStatusHistoryComment(
                        __('Order has been shipped: %1 ', $track->getTrackNumber()),
                        'shipped'
                    );
                }
            } else {
                $order->addStatusHistoryComment(__('Order has been shipped: no tracking information'), 'shipped');
            }

            if(($order->getState() == "processing" || $order->getState() == "complete") && !$order->canInvoice() && !$order->canShip()) {
                $order->setState("complete");
                $order->addStatusHistoryComment("", "complete");
            }

            $this->orderRepository->save($order);
            $connection->commit();

            switch ($requestData->getExtensionAttributes()->getSendMail()) {
                case 'not_send_and_mark':
                    $this->markShipmentAsSent($shipment);
                    break;
                case 'not_send':
                    break;
                case 'send':
                default:
                    $this->shipmentNotifier->notify($shipment);
            }
        } catch (Exception $e) {
            $this->logger->critical($e);
            $connection->rollBack();
            throw new CouldNotShipException(__("{$e->getMessage()} : %1", $e->getPrevious()? $e->getPrevious()->getMessage(): 'See log for details'));
        }
    }

    /**
     * @param $shipment
     */
    protected function markShipmentAsSent($shipment)
    {
        $connection = $this->resourceConnection->getConnection();
        $table = $connection->getTableName('sales_shipment');
        $shipmentId = $shipment->getId();
        $connection->update($table, ['email_sent' => 1, 'send_email' => 1], "entity_id = {$shipmentId}");
    }

    /**
     * @param ShipmentInterface $shipment
     * @param OrderInterface $order
     * @param array $shippedItems
     * @param ShipmentInterface $requestData
     * @throws CouldNotShipException
     */
    protected function addMultiStockInformationIfNeeded(
        ShipmentInterface $shipment,
        OrderInterface $order,
        array $shippedItems,
        ShipmentInterface $requestData
    ) {
        $shipmentItems = $shipment->getItems();

        if (!$shipmentItems || empty($shipmentItems)) {
            throw new CouldNotShipException(new Phrase('Empty shipment'));
        }

        if (version_compare($this->productMetadata->getVersion(), '2.3.0', '<')) {
            return;
        }

        if (!$this->manager->isEnabled('Magento_Inventory')) {
            return;
        }

        $objectManager = ObjectManager::getInstance();

        $itemRequestFactory = $objectManager->create(ItemRequestInterfaceFactory::class);
        $inventoryRequestFactory = $objectManager->create(InventoryRequestInterfaceFactory::class);
        $sourceSelectionService = $objectManager->create(SourceSelectionServiceInterface::class);
        $storeManager = $objectManager->create(StoreManagerInterface::class);
        $websiteId = $storeManager->getStore($shipment->getStoreId())->getWebsiteId();
        $websiteCode = $storeManager->getWebsite($websiteId)->getCode();

        /** @var StockIdResolver $stockIdResolver */
        $stockIdResolver = $objectManager->create(StockIdResolver::class);
        /** @var StockInterface $stock */
        $stockId = $stockIdResolver->resolve(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);

        $items = [];
        foreach ($shippedItems as $sku => $qty) {
            $items[] = $itemRequestFactory->create([
                'sku' => $sku,
                'qty' => $qty
            ]);
        }

        /** @var InventoryRequestInterface $inventoryRequest */
        $inventoryRequest = $inventoryRequestFactory->create([
            'stockId' => $stockId,
            'items'   => $items
        ]);

        if ($this->manager->isEnabled('FiloBlu_ExtInventory')) {
            $inventoryRequest->getExtensionAttributes()->setOrder($order);
            $inventoryRequest->getExtensionAttributes()->setIsShipping(true);
        }

        $algorithmCode = $this->scopeConfig->getValue(self::CONFIG_SELECTION_ALGORITHM);

        if (empty($algorithmCode)) {
            $sourceSelection = $objectManager->create(GetDefaultSourceSelectionAlgorithmCodeInterface::class);
            $algorithmCode = $sourceSelection->execute();
        }

        $sourceSelectionResult = $sourceSelectionService->execute(
            $inventoryRequest,
            $algorithmCode
        );

        // TODO: re-enable when fixed
        //  if (!$sourceSelectionResult->isShippable()) {
        //      throw new CouldNotShipException(new Phrase('No quantity available for shipment'));
        // }

        $preferredSourceCode = $requestData->getExtensionAttributes()->getFinalInventorySourceId();
        $quantity = [];
        foreach ($sourceSelectionResult->getSourceSelectionItems() as $product) {
            /** @var ShipmentExtensionInterface $extensionAttributes */
            $extensionAttributes = $shipment->getExtensionAttributes();
            $extensionAttributes->setSourceCode($preferredSourceCode??$product->getSourceCode());
            $shipment->setExtensionAttributes($extensionAttributes);
            $quantity[$product->getSku()][$preferredSourceCode??$product->getSourceCode()] = $product->getQty();
        }

        $this->_eventManager->dispatch(
            'filoblu_esb_externalshipment_multistockinformation_after',
            ['shipment' => $shipment, 'order' => $order, 'shippedItems' => $shippedItems, 'requestData' => $requestData]);

    }

    /**
     * @param OrderInterface $order
     * @return bool
     */
    protected function isPartialShipment(OrderInterface $order): bool
    {
        return $order->canShip();
    }

    /**
     * Check order shipment availability
     *
     * @param OrderInterface $order
     * @throws CouldNotShipException
     */
    public function checkIsShippable(OrderInterface $order)
    {
        $incrementId = $order->getIncrementId();
        if ($order->canUnhold()) {
            throw new CouldNotShipException(new Phrase('Order %1 is in Payment Review.', [$incrementId]));
        }

        if($order->isPaymentReview()){
            throw new CouldNotShipException(new Phrase('Order %1 is on Hold.', [$incrementId]));
        }

        if ($order->getIsVirtual()) {
            throw new CouldNotShipException(new Phrase('Order %1 is Virtual.', [$incrementId]));
        }

        if($order->isCanceled()){
            throw new CouldNotShipException(new Phrase('Order %1 is Canceled.', [$incrementId]));
        }

        if ($order->getActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_SHIP) === false) {
            throw new CouldNotShipException(new Phrase('Order %1 is .', [$incrementId]));
        }

        foreach ($order->getAllItems() as $item) {
            if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() &&
                !$item->getLockedDoShip() && !($item->getQtyRefunded() == $item->getQtyOrdered())) {
                return;
            }
        }
        throw new CouldNotShipException(new Phrase('Nothing to ship in order %1.', [$incrementId]));
    }

    /**
     * @param OrderInterface $order
     * @param ShipmentInterface $shipmentData
     * @param $carrierCode
     * @param $carrierTitle
     * @return array|void
     * @throws MailException
     */
    public function assingTrackNumbersToShipment(OrderInterface $order, ShipmentInterface $shipmentData, $carrierCode, $carrierTitle)
    {
        foreach ($order->getShipmentsCollection() as $shipment) {

            if ($shipment->getTracksCollection()->getSize() > 0) {
                continue;
            }

            $tracks = $shipmentData->getTracks();

            if (empty($tracks)) {
                continue;
            }

            $shipmentTracks = [];
            foreach ($tracks as $track) {
                $shipmentTrack = $this->shipmentTrackInterfaceFactory->create();
                $shipmentTrack->setOrderId($order->getId());
                $shipmentTrack->setCarrierCode($carrierCode);
                $shipmentTrack->setTitle($carrierTitle);
                $shipmentTrack->setTrackNumber($track->getTrackNumber());
                $shipment->addTrack($shipmentTrack);
                $shipmentTracks[] = $shipmentTrack;
            }

            $this->shipmentRepository->save($shipment);

            $sendMail = null;

            if($shipmentData->getExtensionAttributes() && $shipmentData->getExtensionAttributes()->getSendMail()) {
                $sendMail = $shipmentData->getExtensionAttributes()->getSendMail();
            }

            switch ($sendMail) {
                case 'not_send_and_mark':
                    $this->markShipmentAsSent($shipment);
                    break;
                case 'not_send':
                    break;
                case 'send':
                default:
                    $this->shipmentNotifier->notify($shipment);
            }

            return $shipmentTracks;
        }

    }

    /**
     * @param OrderInterface $order
     * @param ShipmentInterface $shipmentData
     * @param $carrierCode
     * @param $carrierTitle
     * @return array|void
     * @throws MailException
     */
    public function assingTrackNumbersToShipment(OrderInterface $order, ShipmentInterface $shipmentData, $carrierCode, $carrierTitle)
    {
        foreach ($order->getShipmentsCollection() as $shipment) {

            if ($shipment->getTracksCollection()->getSize() > 0) {
                continue;
            }

            $tracks = $shipmentData->getTracks();

            if (empty($tracks)) {
                continue;
            }

            $shipmentTracks = [];
            foreach ($tracks as $track) {
                $shipmentTrack = $this->shipmentTrackInterfaceFactory->create();
                $shipmentTrack->setOrderId($order->getId());
                $shipmentTrack->setCarrierCode($carrierCode);
                $shipmentTrack->setTitle($carrierTitle);
                $shipmentTrack->setTrackNumber($track->getTrackNumber());
                $shipment->addTrack($shipmentTrack);
                $shipmentTracks[] = $shipmentTrack;
            }

            $this->shipmentRepository->save($shipment);

            $sendMail = null;

            if($shipmentData->getExtensionAttributes() && $shipmentData->getExtensionAttributes()->getSendMail()) {
                $sendMail = $shipmentData->getExtensionAttributes()->getSendMail();
            }

            switch ($sendMail) {
                case 'not_send_and_mark':
                    $this->markShipmentAsSent($shipment);
                    break;
                case 'not_send':
                    break;
                case 'send':
                default:
                    $this->shipmentNotifier->notify($shipment);
            }

            return $shipmentTracks;
        }

    }
}
