<?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\ViisonSetArticles\Subscribers\Api;

use Shopware\CustomModels\ViisonSetArticles\SetArticle;
use Shopware\Models\Article\Article;
use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base;
use Shopware\Plugins\ViisonSetArticles\Components\PluginConfigService;
use Shopware\Plugins\ViisonSetArticles\Components\SetArticleOrderDetailAssociation\SetArticleOrderDetailAssociationDescription;
use Shopware\Plugins\ViisonSetArticles\Components\SetArticleOrderDetailAssociationService;
use Shopware\Plugins\ViisonSetArticles\Util;

/**
 * Subscriber to manipulate orders that are created via Pickware.
 */
class OrderSubscriber extends Base
{
    /**
     * @inheritdoc
     */
    public static function getSubscribedEvents()
    {
        return [
            'ViisonPickwareConnectorOrders_postaction_after_order_creation' => 'onViisonPickwareConnectorOrdersOrderCreation',
            'Shopware_Plugins_ViisonPickwarePOS_API_DataPreparedForOrderCreation' => 'onViisonPickwarePOSOrdersDataPreparedForOrderCreation',
            'Shopware_Plugins_ViisonPickwarePOS_API_OrderCreated' => 'onViisonPickwarePOSOrdersOrderCreation',
            'Shopware_Controllers_Api_ViisonPickwarePOSOrders::postAction::after' => 'onAfterViisonPickwarePOSOrdersPostAction',
            'Shopware_Plugins_ViisonPickwareCommon_API_Orders_FilterOrdersResult' => 'onViisonPickwareCommonFilterOrderResult',
        ];
    }

    /**
     * Is executed when ViisonPickwareConnectorOrders has created an Order via API call. Update Order information: fix
     * shipped status for sub articles.
     *
     * Remark: this hook and method is only used in Pickware2(POS) api calls
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onViisonPickwareConnectorOrdersOrderCreation(\Enlight_Event_EventArgs $args)
    {
        $order = $this->get('models')->find('Shopware\\Models\\Order\\Order', $args->get('orderId'));
        $articles = $args->get('articles');
        $this->updateCreatedOrderInformation($order, $articles);
    }

    /**
     * Is executed when ViisonPickwarePOSOrders is about to create an Order. Update raw OrderData: add sub articles
     * Remark: manipulate article data only if set
     *
     * Return article detail:
     *     'articleDetailId' => <ARTICLE_DETAIL_ID>,
     *     'articleName' => <ARTICLE_NAME>,
     *     'articleNumber' => <ARTICLE_NUMBER>,
     *     'price' => <PRICE>,
     *     'quantity' => <QUANTITY>
     *     'binLocationId' => <BIN_LOCATION_ID>
     *     'shipped' => <SHIPPED_QUANTITY>
     *
     * @param \Enlight_Event_EventArgs $args
     * @return mixed
     */
    public function onViisonPickwarePOSOrdersDataPreparedForOrderCreation(\Enlight_Event_EventArgs $args)
    {
        $orderData = $args->getReturn();
        $warehouseId = array_key_exists('warehouseId', $orderData) ? $orderData['warehouseId'] : null; // Can be null
        $shop = $this->get('models')->find('Shopware\\Models\\Shop\\Shop', $orderData['shopId']);
        $newOrderDetails = [];
        foreach ($orderData['details'] as $key => $detail) {
            if (!array_key_exists('articleDetailId', $detail) || !Util::isSetArticleByDetailId($detail['articleDetailId'])) {
                // Do nothing with articles that are not set articles
                $newOrderDetails[] = $detail;
                continue;
            }

            $detail['viisonSetArticleSetArticleOrderNumber'] = $detail['articleNumber'];
            $newOrderDetails[] = $detail;
            $subArticles = $this->unfoldSingleSetArticle($detail, $shop, $warehouseId);
            $newOrderDetails = array_merge($newOrderDetails, $subArticles);
        }
        $orderData['details'] = $newOrderDetails;

        return $orderData;
    }

    /**
     * Is executed when ViisonPickwarePOSOrders has created an Order via API call. We update the persisted OrderDetails
     * by setting the SetArticleOrderId attribute for all relevant sub and set articles.
     *
     * Remark: We need Subscriber to fix this especially for POS, because it does not use the
     * Shopware_Modules_Order_SaveOrder_ProcessDetails event.
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onViisonPickwarePOSOrdersOrderCreation(\Enlight_Event_EventArgs $args)
    {
        $orderData = $args->get('orderData');
        $order = $args->get('order');

        // Add order detail id to $orderData (since it is not provided). Compare multiple fields to ensure sub articles
        // that are bought separately (outside the set article) are not mixed up with sub articles inside a set.
        // "used order detail id" condition: If the same sub article is part of multiple set articles and they are
        // bought together, this id assignment would overwrite itself.
        $usedOrderDetailIds = [];
        $setArticlePositionRelations = [];
        foreach ($orderData['details'] as $orderDetailData) {
            foreach ($order->getDetails() as $orderDetail) {
                if (!in_array($orderDetail->getId(), $usedOrderDetailIds) &&
                    $orderDetailData['articleNumber'] === $orderDetail->getArticleNumber() &&
                    $orderDetailData['quantity'] === $orderDetail->getQuantity() &&
                    $orderDetailData['price'] === $orderDetail->getPrice()
                ) {
                    $usedOrderDetailIds[] = $orderDetail->getId();
                    $setArticlePositionRelations[] = new SetArticleOrderDetailAssociationDescription(
                        $orderDetail->getId(),
                        $orderDetailData['viisonSetArticleSetArticleOrderNumber']
                    );
                    break;
                }
            }
        }

        /** @var SetArticleOrderDetailAssociationService $setArticleOrderDetailAssociationService */
        $setArticleOrderDetailAssociationService = $this->get('viison_set_articles.set_article_order_detail_association_service');
        $setArticleOrderDetailAssociationService->associateSetArticleOrderDetails(
            $order,
            $setArticlePositionRelations
        );
    }

    /**
     * Updates Order information by matching set/sub article with API call content ($articles). Set sub articles to
     * "fully shipped"
     *
     * @param $order Order
     * @param $articles Article Array of API call
     */
    public function updateCreatedOrderInformation($order, $articles)
    {
        $changedEntities = [];
        foreach ($order->getDetails() as $orderItem) {
            // Find any sub article in this order
            $isSubArticle = ($orderItem->getAttribute()->getViisonSetarticleOrderid() !== null) && ($orderItem->getAttribute()->getViisonSetarticleOrderid() != $orderItem->getId());

            if ($isSubArticle) {
                foreach ($order->getDetails() as $nextItem) {
                    // Find the respective set article in order
                    if ($orderItem->getAttribute()->getViisonSetarticleOrderid() == $nextItem->getId()) {
                        $setArticleArticleNumber = $nextItem->getArticleNumber();

                        // Find the respective set article in $articles
                        foreach ($articles as $apiArticle) {
                            // Remark: "old" POS api call used "ordernumber", new POS plugins api call uses
                            // "articleNumber"
                            if ($apiArticle['ordernumber'] == $setArticleArticleNumber || $apiArticle['articleNumber'] == $setArticleArticleNumber) {
                                // Update Order information of sub article according to set article shipping information
                                // ($articles)
                                $subArticleQuantity = $this->getQuantityOfSubArticle($setArticleArticleNumber, $orderItem->getArticleNumber());
                                $orderItem->setShipped($apiArticle['shipped'] * $subArticleQuantity);
                                $changedEntities[] = $orderItem;
                            }
                        }
                    }
                }
            }
        }
        $this->get('models')->flush($changedEntities);
    }

    /**
     * This hook is executed before a created order is returned to the POS app (where the invoice document is created).
     * If set articles are not unfolded, mark sub articles with attribute 'showOnReceipt' (false) so the do not appear
     * on documents.
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onAfterViisonPickwarePOSOrdersPostAction(\Enlight_Event_EventArgs $args)
    {
        // Early return if set articles are unfolded anyway
        /** @var PluginConfigService $pluginConfigService */
        $pluginConfigService = $this->get('viison_set_articles.plugin_config');
        if ($pluginConfigService->getDisplaySubArticlesOnDocuments() === PluginConfigService::ALWAYS_SHOW_SUB_ARTICLES_ON_DOCUMENTS) {
            return;
        }
        $subject = $args->getSubject();
        $success = $subject->View()->getAssign('success');
        if (!$success) {
            return;
        }
        $orderResourceData = $subject->View()->getAssign('data');
        $orderResourceData = $this->setHideOnReceiptAttributeInOrderData($orderResourceData);

        $subject->View()->assign('data', $orderResourceData);
    }

    /**
     * This hook is executed in the GET /api/orders hook of ViisonPickwareCommon. This is also the case when a click
     * and collect order is requested from the Shopware backend. Mark sub articles with attribute 'showOnReceipt'
     * (false) so the do not appear on documents if the order data was enriched with detail (order position) information
     * in the first place (e.g. by PickwareCommon/Subscribers/Api/Orders)
     *
     * @param \Enlight_Event_EventArgs $args
     * @return array
     */
    public function onViisonPickwareCommonFilterOrderResult(\Enlight_Event_EventArgs $args)
    {
        $orders = $args->getReturn();
        /** @var PluginConfigService $pluginConfigService */
        $pluginConfigService = $this->get('viison_set_articles.plugin_config');
        if ($pluginConfigService->getDisplaySubArticlesOnDocuments() === PluginConfigService::ALWAYS_SHOW_SUB_ARTICLES_ON_DOCUMENTS) {
            return $orders;
        }

        foreach ($orders as &$orderData) {
            // Ignore if no detail (order position) data was attached to the order
            if (!array_key_exists('details', $orderData)) {
                continue;
            }
            $orderData = $this->setHideOnReceiptAttributeInOrderData($orderData);
        }

        return $orders;
    }

    /**
     * Marks all sub articles in the given $orderData with attribute 'showOnReceipt' (false) so the do not appear on
     * documents.
     *
     * @param array $orderData
     * @return array the manipulated $orderData
     */
    private function setHideOnReceiptAttributeInOrderData($orderData)
    {
        // Fetch corresponding order detail entities beforehand to minimize database requests
        $orderDetailDataIds = array_map(function ($orderDetailData) {
            return $orderDetailData['id'];
        }, $orderData['details']);
        $orderDetails = $this->get('models')->getRepository('Shopware\\Models\\Order\\Detail')->findBy([
            'id' => $orderDetailDataIds,
        ]);
        $orderDetailIds = array_map(function ($orderDetail) {
            return $orderDetail->getId();
        }, $orderDetails);
        // Combine ids and entities to associative array
        $orderDetails = array_combine($orderDetailIds, $orderDetails);

        foreach ($orderData['details'] as &$orderDetailData) {
            if (!array_key_exists($orderDetailData['id'], $orderDetails)) {
                // Do nothing if the respective order detail was not found
                continue;
            }

            /** @var \Shopware\Models\Order\Detail $orderDetail */
            $orderDetail = $orderDetails[$orderDetailData['id']];
            $isSubArticle = $orderDetail->getAttribute()
                && $orderDetail->getAttribute()->getViisonSetarticleOrderid()
                && (intval($orderDetail->getAttribute()->getViisonSetarticleOrderid()) !== $orderDetail->getId());
            $orderDetailData['attribute']['pickwarePosHideOnReceipt'] = $isSubArticle;
            // Legacy (< 5.0.0) Pickware
            $orderDetailData['attribute']['viisonPosHideOnReceipt'] = $isSubArticle;
        }

        return $orderData;
    }

    /**
     * Fetches sub article quantity in a set article from DB by their article numbers
     *
     * @param string $setArticleArticleNumber ArticleNumber of set article
     * @param string $subArticleArticleNumber ArticleNumber of sub article
     * @return int Quantity of sub article in its set
     */
    private function getQuantityOfSubArticle($setArticleArticleNumber, $subArticleArticleNumber)
    {
        $sql = "SELECT quantity FROM s_articles_viison_setarticles
                WHERE setid IN
                    (SELECT id FROM s_articles_details WHERE ordernumber = '" . $setArticleArticleNumber . "')
                AND articledetailid IN
                    (SELECT id FROM s_articles_details WHERE ordernumber = '" . $subArticleArticleNumber . "')";

        return $this->get('db')->fetchOne($sql);
    }

    /**
     * Unfolds a single set article (given by Article/Detail information) and returns all corresponding sub articles as
     * an array. Information is fetched as needed in ViisonPickwarePOS_API_DataPreparedForOrderCreation, but add
     * articleName:
     *
     * Return article detail:
     *     'articleDetailId' => <ARTICLE_DETAIL_ID>,
     *     'articleName' => <ARTICLE_NAME>,
     *     'price' => <PRICE>,
     *     'quantity' => <QUANTITY>
     *     'binLocationId' => <BIN_LOCATION_ID>
     *     'shipped' => <SHIPPED_QUANTITY>
     *
     * @param array $detail
     * @param null $shop
     * @param null|int $warehouseId
     * @return array
     */
    private function unfoldSingleSetArticle($detail, $shop = null, $warehouseId = null)
    {
        $result = [];

        // Return early if no article detail is is set (e.g. it's a dummy article)
        if (!array_key_exists('articleDetailId', $detail) || !$detail['articleDetailId']) {
            return $result;
        }

        if (!$shop) {
            $shop = Util::fetchShop();
        }
        $subArticles = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')->findBy([
            'setId' => $detail['articleDetailId'],
        ]);

        /** @var SetArticle $subArticle */
        foreach ($subArticles as $subArticle) {
            // Since we are handling a PickwarePOS request, PickwareERP is installed, so we fetch Warehouse information
            $quantity = $subArticle->getQuantity() * $detail['quantity'];
            if ($warehouseId) {
                $warehouse = $this->get('models')->find('Shopware\\CustomModels\\ViisonPickwareERP\\Warehouse\\Warehouse', $warehouseId);
                if ($this->getContainer()->has('pickware.erp.stock_change_list_factory_service')) {
                    // Pickware >= 5.0.0
                    $stockChangeList = $this->get('pickware.erp.stock_change_list_factory_service')->createStockChangeList(
                        $warehouse,
                        $subArticle->getArticleDetail(),
                        $quantity
                    );
                    $binLocationId = array_shift($stockChangeList->getChangedLocations())->getId();
                } else {
                    $binLocations = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonPickwareERP\\Warehouse\\Warehouse')->findAllBinLocations(
                        $warehouse,
                        $subArticle->getArticleDetail()
                    );
                    $binLocation = (array_shift($binLocations)) ?: $warehouse->getDefaultBinLocation();
                    $binLocationId = $binLocation->getId();
                }
            } else {
                $binLocationId = null;
            }

            $subArticleOrderData = [
                'articleDetailId' => $subArticle->getArticleDetail()->getId(),
                'articleName' => $subArticle->getSubArticleName($shop),
                'articleNumber' => $subArticle->getArticleDetail()->getNumber(),
                'price' => 0.00,
                'quantity' => $quantity,
                'viisonSetArticleSetArticleOrderNumber' => $subArticle->getSetArticleDetail()->getNumber(),
            ];
            // If this is not a POS order but any order that comes via API request, fill in whatever data is given.
            if ($detail['taxId']) {
                $subArticleOrderData['taxId'] = $detail['taxId'];
            }
            if ($binLocationId) {
                $subArticleOrderData['binLocationId'] = $binLocationId;
            }
            if ($detail['shipped']) {
                $subArticleOrderData['shipped'] = $subArticle->getQuantity() * $detail['shipped'];
            }

            $result[] = $subArticleOrderData;
        }

        return $result;
    }
}
