<?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\ViisonPickwareMobile\Subscribers\Backend;

use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonPickwareERP\StockLedger\OrderStockReservation;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\Warehouse;
use Shopware\CustomModels\ViisonPickwareMobile\PickProfile\PickProfile;
use Shopware\Models\Order\Detail as OrderDetail;
use Shopware\Models\Order\Order;
use Shopware\Models\Order\Status;
use Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonPickwareERP\Subscribers\Backend\BackendOrderSubscriber as ViisonPickwareErpBackendOrderSubscriber;
use Shopware\Plugins\ViisonPickwareMobile\Components\PickedQuantityUpdaterService;
use Shopware\Plugins\ViisonPickwareMobile\Components\StockReservationService;

class OrderSubscriber extends Base
{
    const REQUEST_ATTR_ORDER_BACKUP = 'ViisonPickwareMobile_Backend_Order_OrderBackup';
    const REQUEST_ATTR_ORDER_FILTER_PICK_PROFILE = 'ViisonPickwareMobile_Backend_Order_OrderFilter_PickProfile';
    const REQUEST_ATTR_ORDER_FILTER_WAREHOUSE = 'ViisonPickwareMobile_Backend_Order_OrderFilter_Warehouse';
    const REQUEST_ATTR_POSITION_CLEAR_PICKED_QUANTITIES = 'ViisonPickwareMobile_Backend_Position_ClearPickedQuantities';

    /**
     * Ensure the "save order" subscribers are executed BEFORE the ones from ViisonPickwareERP. This is done to ensure
     * that picked quantities are cleared before an order is marked as completely shipped or cancelled. If the order of
     * the subscribers would be arbitrarily, a cancellation or "complete delivery" of an order that has picked quantities may
     * lead to an order that still has items remaining to ship.
     *
     * We cannot assume that the position of a subscriber is exactly the value provided during registration we take a
     * bigger step (50) to ensure that the subscriber is definitely executed before the ERP subscriber.
     * See https://github.com/shopware/shopware/blob/4496e0a25f74d33f681d7b785f3fc3e8f02ecf47/engine/Library/Enlight/Event/EventManager.php#L115
     */
    const AFTER_SAVE_ORDER_SUBSCRIBER_POSITION = ViisonPickwareErpBackendOrderSubscriber::AFTER_SAVE_ORDER_SUBSCRIBERS_POSITION - 50;

    /**
     * A flag used to indicate, that, in case a dispatch status mail is sent, only order items
     * with a pickwarePickedQuantity > 0 shall be listed in the email. By default, this flag is true.
     *
     * @var boolean $includeOnlyPickedOrderItemsInDispatchStatusMail
     */
    private $includeOnlyPickedOrderItemsInDispatchStatusMail = true;

    /**
     * A flag used to indicate, that, in case a dispatch status mail is sent, a status URL containing all
     * tracking codes of the order shall be added to the that mail. By default, this flag is false.
     *
     * @var boolean $includeAllTrackingCodesInDispatchStatusMail
     */
    private $includeAllTrackingCodesInDispatchStatusMail = false;

    /**
     * @see \Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base::getSubscribedEvents()
     */
    public static function getSubscribedEvents()
    {
        return [
            'Shopware_Controllers_Backend_Order::getListAction::before' => 'onBeforeGetListAction',
            'Shopware_Controllers_Backend_Order::getList::after' => 'onAfterGetList',
            'Shopware_Controllers_Backend_Order::saveAction::before' => 'onBeforeSaveAction',
            'Shopware_Controllers_Backend_Order::saveAction::after' => [
                [
                    'onAfterSaveAction',
                    self::AFTER_SAVE_ORDER_SUBSCRIBER_POSITION,
                ],
                [
                    'onAfterAnySaveAction',
                    self::AFTER_SAVE_ORDER_SUBSCRIBER_POSITION,
                ],
            ],
            'Shopware_Controllers_Backend_Order::batchProcessAction::before' => 'onBeforeBatchProcessAction',
            'Shopware_Controllers_Backend_Order::batchProcessAction::after' => [
                [
                    'onAfterAnySaveAction',
                    self::AFTER_SAVE_ORDER_SUBSCRIBER_POSITION,
                ],
            ],
            'Shopware_Controllers_Backend_OrderState_Notify' => 'onOrderStateNotify',
        ];
    }

    /**
     * Checks the request for a picking order filter configuration and, if set, loads the respective pick profile and
     * warehouse and adds them both to the request attributes and the 'viisonPickwareMobileWaitingForStock' filter
     * element, if available.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onBeforeGetListAction(\Enlight_Hook_HookArgs $args)
    {
        // Try to find the pick profile and warehouse that shall be used for evaluating the picking order filter
        $request = $args->getSubject()->Request();
        $pickProfileId = $request->getParam('viisonPickwareMobilePickingOrderFilterPickProfileId');
        if ($pickProfileId) {
            $pickProfile = $this->get('models')->find(PickProfile::class, $pickProfileId);
        } else {
            $pickProfile = $this->get('models')->getRepository(PickProfile::class)->findOneBy([], ['name' => 'ASC']);
        }
        $request->setAttribute(self::REQUEST_ATTR_ORDER_FILTER_PICK_PROFILE, $pickProfile);
        $warehouseId = $request->getParam('viisonPickwareMobilePickingOrderFilterWarehouseId', 0);
        if ($warehouseId) {
            $warehouse = $this->get('models')->find(Warehouse::class, $warehouseId);
        } else {
            $warehouse = $this->get('models')->getRepository(Warehouse::class)->getDefaultWarehouse();
        }
        $request->setAttribute(self::REQUEST_ATTR_ORDER_FILTER_WAREHOUSE, $warehouse);

        // Save both the pick profile and the warehouse in the 'viisonPickwareMobileWaitingForStock' and
        // 'viisonPickwareMobilePickingAppVisibility' filter, respectively, if set. This allows us to access that
        // information when building the filter query.
        $filter = $request->getParam('filter', []);
        foreach ($filter as &$filterElement) {
            if (is_array($filterElement) && $filterElement['property'] === 'viisonPickwareMobileWaitingForStock') {
                $filterElement['value'] = [
                    'pickProfile' => $pickProfile,
                    'warehouse' => $warehouse,
                ];
            }
            if (is_array($filterElement) && $filterElement['property'] === 'viisonPickwareMobilePickingAppVisibility') {
                $filterElement['value'] = [
                    'pickProfile' => $pickProfile,
                    'warehouse' => $warehouse,
                    'isVisibleInPickingApp' => $filterElement['value'],
                ];
            }
        }
        unset($filterElement);
        $request->setParam('filter', $filter);
    }

    /**
     * Gets all orders contained in the return value and adds a new field 'viisonPickwareMobilePickingOrderFilterResult'
     * to these orders and each of their details. This field is an array containing all unmet conditions, for displaying
     * the order or order detail in the Picking app.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterGetList(\Enlight_Hook_HookArgs $args)
    {
        // Check the result of the original method
        $return = $args->getReturn();
        if (!$return['success']) {
            return;
        }

        // Get the ids of all orders in the result
        $orderIds = array_map(function ($order) {
            return $order['id'];
        }, $return['data']);

        // Try to find the pick profile and warehouse that shall be used for evaluating the picking order filter
        $request = $args->getSubject()->Request();
        /** @var PickProfile $pickProfile */
        $pickProfile = $request->getAttribute(self::REQUEST_ATTR_ORDER_FILTER_PICK_PROFILE);
        if (!$pickProfile) {
            $pickProfile = $this->get('models')->getRepository(PickProfile::class)->findOneBy([], ['name' => 'ASC']);
        }
        $warehouse = $request->getAttribute(self::REQUEST_ATTR_ORDER_FILTER_WAREHOUSE);
        if (!$warehouse) {
            $warehouse = $this->get('models')->getRepository(Warehouse::class)->getDefaultWarehouse();
        }

        // Analyse the loaded orders using the the picking order filter
        $analysisResult = $this->get('viison_pickware_mobile.pick_profile_order_filter_service')->getUnfulfilledConditionsForFilterOfPickProfile(
            $pickProfile,
            $warehouse,
            $orderIds
        );

        // Update the returned orders and order details
        $pickingOrderFilterConditionLocalization = $this->get('viison_pickware_mobile.picking_order_filter_condition_localization_service');
        foreach ($return['data'] as &$order) {
            // Add the unmet conditions for displaying the order in the Picking app
            $order['viisonPickwareMobilePickProfileFilterResult'] = $pickingOrderFilterConditionLocalization->getLocalizedUnfilledConditionsForOrderWithId(
                $analysisResult,
                $order['id']
            );
            foreach ($order['details'] as &$item) {
                // Add the unmet conditions for displaying the order detail in the Picking app
                $item['viisonPickwareMobilePickProfileFilterResult'] = $pickingOrderFilterConditionLocalization->getLocalizedUnfulfilledConditionsForOrderItemWithId(
                    $analysisResult,
                    $item['id'],
                    $pickProfile->getEarliestReleaseDateForPreOrderedItems()
                );

                // Add attribute fields
                /** @var OrderDetail $orderDetail */
                $orderDetail = $this->get('models')->find(OrderDetail::class, $item['id']);
                $item['pickwarePickedQuantity'] = ($orderDetail->getAttribute()) ? $orderDetail->getAttribute()->getPickwarePickedQuantity() : 0;
            }

            // Add custom attribute data
            /** @var Order $orderEntity */
            $orderEntity = $this->get('models')->find(Order::class, $order['id']);
            $order['pickwareReturnShipmentStatusId'] = ($orderEntity->getAttribute()) ? $orderEntity->getAttribute()->getPickwareReturnShipmentStatusId() : null;
            $order['viisonUndispatchedTrackingCodes'] = ($orderEntity->getAttribute()) ? $orderEntity->getAttribute()->getViisonUndispatchedTrackingCodes() : null;
            $order['pickwareWmsInternalPickingInstructions'] = ($orderEntity->getAttribute()) ? $orderEntity->getAttribute()->getPickwareWmsInternalPickingInstructions() : null;
        }

        // Update the result
        $args->setReturn($return);
    }

    /**
     * Changes the status email flags, so that all order items and tracking codes will be included.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onBeforeSaveAction(\Enlight_Hook_HookArgs $args)
    {
        $this->includeOnlyPickedOrderItemsInDispatchStatusMail = false;
        $this->includeAllTrackingCodesInDispatchStatusMail = true;

        /** @var \Enlight_Controller_Request_Request $request */
        $request = $args->getSubject()->Request();
        /** @var Order $order */
        $order = $this->get('models')->find(Order::class, $request->getParam('id'));
        if ($order) {
            $request->setAttribute(
                self::REQUEST_ATTR_ORDER_BACKUP,
                [
                    $order->getId() => [
                        'statusId' => $order->getOrderStatus()->getId(),
                    ],
                ]
            );
        }
    }

    /**
     * Saves the custom order fields in the database, if the original operation was successful.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterSaveAction(\Enlight_Hook_HookArgs $args)
    {
        if (!$args->getSubject()->View()->success) {
            return;
        }

        // Get saved order and save its custom fields
        $request = $args->getSubject()->Request();
        $order = $this->get('models')->find(Order::class, $request->getParam('id'));
        if ($order->getAttribute()) {
            $params = $request->getParams();
            if (array_key_exists('viisonUndispatchedTrackingCodes', $params)) {
                $order->getAttribute()->setViisonUndispatchedTrackingCodes($params['viisonUndispatchedTrackingCodes']);
            }
            if (array_key_exists('pickwareWmsInternalPickingInstructions', $params)) {
                $order->getAttribute()->setPickwareWmsInternalPickingInstructions($params['pickwareWmsInternalPickingInstructions']);
            }
            $this->get('models')->flush($order->getAttribute());
        }
    }

    /**
     * In case the status emails shall be sent automatically, the email flags are changed,
     * so that all order items and tracking codes will be included.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onBeforeBatchProcessAction(\Enlight_Hook_HookArgs $args)
    {
        $request = $args->getSubject()->Request();
        if ($request->getParam('autoSend') == 'true') {
            $this->includeOnlyPickedOrderItemsInDispatchStatusMail = false;
            $this->includeAllTrackingCodesInDispatchStatusMail = true;
        }

        // Get orders data the same way as in batchProcessAction
        $orders = $request->getParam('orders', [
            $request->getParams()
        ]);

        // Back up current order statuses
        $orderBackup = [];
        foreach ($orders as $orderData) {
            $order = $this->get('models')->find(Order::class, $orderData['id']);
            if ($order) {
                $orderBackup[$order->getId()] = [
                    'statusId' => $order->getOrderStatus()->getId(),
                ];
            }
        }
        $request->setAttribute(self::REQUEST_ATTR_ORDER_BACKUP, $orderBackup);
    }

    /**
     * If the original operation was successful, one ore more orders were saved and its status is anything else than
     * 'in process', all reserved stock of its order details is freed.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterAnySaveAction(\Enlight_Hook_HookArgs $args)
    {
        if (!$args->getSubject()->View()->success) {
            return;
        }

        /** @var \Enlight_Controller_Request_Request $request */
        $request = $args->getSubject()->Request();
        $orderBackupData = $request->getAttribute(self::REQUEST_ATTR_ORDER_BACKUP, []);
        if (count($orderBackupData) === 0) {
            return;
        }

        /** @var StockReservationService $stockReservationService */
        $stockReservationService = $this->get('pickware.erp.stock_reservation_service');
        /** @var PickedQuantityUpdaterService $pickedQuantityUpdater */
        $pickedQuantityUpdater = $this->get('viison_pickware_mobile.picked_quantity_updater');
        /** @var ModelManager $entityManager */
        $entityManager = $this->get('models');
        $resetOrderStatuses = [
            Status::ORDER_STATE_OPEN,
            Status::ORDER_STATE_CANCELLED_REJECTED,
        ];
        $shippedOrderStatuses = [
            Status::ORDER_STATE_PARTIALLY_DELIVERED,
            Status::ORDER_STATE_COMPLETELY_DELIVERED,
        ];
        $shippedBackup = [];
        if (defined(ViisonPickwareErpBackendOrderSubscriber::class . '::REQUEST_ATTRIBUTE_ORIGINAL_SHIPPED_VALUES')) {
            // Constant ViisonPickwareErpBackendOrderSubscriber::REQUEST_ATTRIBUTE_ORIGINAL_SHIPPED_VALUES is
            // introduced by ViisonPickwareERP 6.1.3, therefore do nothing when the constant does not exist.
            $shippedBackup = $args->getSubject()->Request()->getAttribute(ViisonPickwareErpBackendOrderSubscriber::REQUEST_ATTRIBUTE_ORIGINAL_SHIPPED_VALUES);
        }
        /** @var Order[] $orders */
        $orders = $entityManager->getRepository(Order::class)->findBy([
            'id' => array_keys($orderBackupData),
        ]);

        $moveStockToOriginalLocation = (bool) $request->getParam('pickwareMoveStockToOriginalLocation', false);
        foreach ($orders as $order) {
            // Determine whether the order status has changed
            $oldOrderStatus = $orderBackupData[$order->getId()]['statusId'];
            if ($oldOrderStatus === $order->getOrderStatus()->getId()) {
                continue;
            }

            // Determine whether the order status is either one of the reset or shipped statuses
            $newStatusIsReset = in_array($order->getOrderStatus()->getId(), $resetOrderStatuses);
            $newStatusIsShipped = in_array($order->getOrderStatus()->getId(), $shippedOrderStatuses);
            if (!$newStatusIsReset && !$newStatusIsShipped) {
                continue;
            }

            // Clear all picking attributes
            $order->getAttribute()->setPickwareBatchPickingBoxId(null);
            $order->getAttribute()->setPickwareBatchPickingTransactionId(null);
            $order->getAttribute()->setPickwareProcessingWarehouseId(null);
            $order->getAttribute()->setViisonUndispatchedTrackingCodes(null);
            $this->get('models')->flush($order->getAttribute());

            foreach ($order->getDetails() as $orderDetail) {
                // Free any reserved stock
                $reservedStock = $this->get('models')->getRepository(OrderStockReservation::class)->findOneBy([
                    'orderDetail' => $orderDetail,
                ]);
                if ($reservedStock) {
                    $stockReservationService->clearStockReservation($reservedStock);
                }

                if ($newStatusIsShipped) {
                    // Add any picked quantity to the detail's shipped quantity
                    $totalPickedQuantity = $orderDetail->getAttribute()->getPickwarePickedQuantity();
                    $orderDetail->setShipped($orderDetail->getShipped() + $totalPickedQuantity);
                    // Increase the original shipped values back upped by ERP to avoid duplicate stock entries. If this
                    // was not done, ERP would recognize the change to the shipped quantity of the orders details as a
                    // change done by the user via backend and would write stock entries for it.
                    $shippedBackup[$orderDetail->getId()] += $totalPickedQuantity;
                }

                // Reset any picked quantity
                $pickedQuantityUpdater->clearPickedQuantities($orderDetail, !$newStatusIsShipped, $moveStockToOriginalLocation);
            }
            if ($newStatusIsShipped) {
                $this->get('models')->flush($order->getDetails()->toArray());
            }
        }
        if (defined(ViisonPickwareErpBackendOrderSubscriber::class . '::REQUEST_ATTRIBUTE_ORIGINAL_SHIPPED_VALUES')) {
            $args->getSubject()->Request()->setAttribute(
                ViisonPickwareErpBackendOrderSubscriber::REQUEST_ATTRIBUTE_ORIGINAL_SHIPPED_VALUES,
                $shippedBackup
            );
        }
    }

    /**
     * Tries to get the order, for which a new status email is about to be created and
     * adds the status URL(s) of the associated tracking codes to the custom template
     * values. If the 'includeAllTrackingCodesInDispatchStatusMail' flag is set, all
     * tracking codes of the order will be used to create the status links. If it is not
     * set, only the undispatched tracking codes will be used. Similarly if the
     * 'includeOnlyPickedOrderItemsInDispatchStatusMail' flag is set, only picked order
     * items are listed in the emails, otherwise all items.
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onOrderStateNotify(\Enlight_Event_EventArgs $args)
    {
        // Get the order, for which a email is about to be created
        $order = $this->get('models')->find(Order::class, $args->get('id'));
        if (!$order) {
            return;
        }

        // Get the current event context values or create new ones
        $values = ($args->getValues()) ?: [];

        if ($args->get('status') == Status::ORDER_STATE_COMPLETELY_DELIVERED) {
            if ($this->includeOnlyPickedOrderItemsInDispatchStatusMail) {
                // Determine whether this is the first shipment by inspecting the order items
                $orderContainsPickedItems = false;
                $orderContainsShippedItems = false;
                foreach ($order->getDetails() as $detail) {
                    if ($detail->getArticleId() == 0 || $detail->getEsdArticle() || $detail->getPrice() < 0) {
                        continue;
                    }

                    $orderContainsPickedItems |= $detail->getAttribute() && $detail->getAttribute()->getPickwarePickedQuantity() > 0;
                    $orderContainsShippedItems |= $detail->getShipped() > 0;
                }
                $values['isFirstShipment'] = $orderContainsPickedItems && !$orderContainsShippedItems;
            } else {
                // Make sure to send an email indicating a complete shipment of the order, no matter if any items have
                // previously been shipped. This is required when changing the order status via the backend.
                $values['isFirstShipment'] = true;
            }

            $args->setValues($values);
        }

        if ($this->includeOnlyPickedOrderItemsInDispatchStatusMail) {
            // Add only the order items having a picked quantity to the view data
            $orderItemsWhitelist = [];
            foreach ($order->getDetails() as $detail) {
                if ($detail->getAttribute() && $detail->getAttribute()->getPickwarePickedQuantity() > 0) {
                    $orderItemsWhitelist[$detail->getId()] = $detail->getAttribute()->getPickwarePickedQuantity();
                }
            }

            // Add the filter to the custom context values
            $values['orderItemsWhitelist'] = $orderItemsWhitelist;
            $args->setValues($values);
        }

        // Decide which trackingCodes to use
        if ($this->includeAllTrackingCodesInDispatchStatusMail) {
            // Use all tracking codes
            $values['trackingCodes'] = ViisonCommonUtil::safeExplode(',', $order->getTrackingCode());
        } elseif ($order->getAttribute()) {
            // Only use the undispatched tracking codes
            $values['trackingCodes'] = ViisonCommonUtil::safeExplode(',', $order->getAttribute()->getViisonUndispatchedTrackingCodes());
        } else {
            // Don't add any tracking codes
            $values['trackingCodes'] = [];
        }

        if (!empty($values['trackingCodes'])) {
            // Sort the tracking codes by shipping provider
            $providers = [];
            $otherTrackingCodes = [];
            $shippingProviderRepository = $this->get('viison_pickware_mobile.shipping_provider_repository');
            foreach ($values['trackingCodes'] as $trackingCode) {
                // Try to find the shipping provider, which created the tracking code
                $provider = $shippingProviderRepository->findProviderForTrackingCode($trackingCode);
                if (!$provider) {
                    $otherTrackingCodes[] = $trackingCode;
                    continue;
                }

                // Add the tracking code to provider
                if (!$providers[$provider->getIdentifier()]) {
                    $providers[$provider->getIdentifier()] = [
                        'provider' => $provider,
                        'trackingCodes' => [],
                    ];
                }
                $providers[$provider->getIdentifier()]['trackingCodes'][] = $trackingCode;
            }

            $values['trackingCodes'] = $otherTrackingCodes;
            $values['statusURLs'] = array_reduce($providers, function ($carry, $item) {
                if (!method_exists($item['provider'], 'statusUrlsForTrackingCodes')) {
                    // DEPRECATED Case
                    // If the ShippingProvider uses an outdated interface version of
                    // \ViisonPickwareMobile_Interfaces_ShippingProvider_ShippingProvider call the old method.
                    $statusUrl = $item['provider']->statusURLForTrackingCodes($item['trackingCodes']);
                    if ($statusUrl === null) {
                        return $carry;
                    } else {
                        $carry[] = $statusUrl;

                        return $carry;
                    }
                }

                return array_merge(
                    $carry,
                    $item['provider']->statusUrlsForTrackingCodes($item['trackingCodes'])
                );
            }, []);
        }

        // Save tracking codes and status URLs in context
        $args->setValues($values);
    }
}
