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

use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonPickwareERP\StockLedger\OrderStockReservation;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\ArticleDetailBinLocationMapping;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\Warehouse;
use Shopware\Models\Article\Detail as ArticleDetail;
use Shopware\Models\Order\Detail as OrderDetail;
use Shopware\Plugins\ViisonPickwareERP\Components\DerivedPropertyUpdater\DerivedPropertyUpdater;
use Shopware\Plugins\ViisonPickwareERP\Components\OrderDetailQuantityCalculator\OrderDetailQuantityCalculator;
use Shopware\Plugins\ViisonPickwareERP\Components\StockLedger\StockChangeList\BinLocationStockChange;
use Shopware\Plugins\ViisonPickwareERP\Components\StockLedger\StockChangeList\StockChangeListFactory;
use Shopware\Plugins\ViisonPickwareERP\Components\StockReservation\StockReservation as PickwareERPStockReservation;

class StockReservationService
{
    /**
     * @var ModelManager
     */
    protected $entityManager;

    /**
     * @var PickwareERPStockReservation
     */
    protected $pickwareErpStockReservation;

    /**
     * @var StockChangeListFactory
     */
    protected $stockChangeListFactory;

    /**
     * @var DerivedPropertyUpdater
     */
    protected $derivedPropertyUpdater;

    /**
     * @var OrderDetailQuantityCalculator
     */
    private $orderDetailQuantityCalculator;

    /**
     * @param ModelManager $entityManager
     * @param PickwareERPStockReservation $pickwareErpStockReservation
     * @param StockChangeListFactory $stockChangeListFactory
     * @param DerivedPropertyUpdater $derivedPropertyUpdater
     * @param OrderDetailQuantityCalculator $orderDetailQuantityCalculator
     */
    public function __construct(
        ModelManager $entityManager,
        PickwareERPStockReservation $pickwareErpStockReservation,
        StockChangeListFactory $stockChangeListFactory,
        DerivedPropertyUpdater $derivedPropertyUpdater,
        OrderDetailQuantityCalculator $orderDetailQuantityCalculator
    ) {
        $this->entityManager = $entityManager;
        $this->pickwareErpStockReservation = $pickwareErpStockReservation;
        $this->stockChangeListFactory = $stockChangeListFactory;
        $this->derivedPropertyUpdater = $derivedPropertyUpdater;
        $this->orderDetailQuantityCalculator = $orderDetailQuantityCalculator;
    }

    /**
     * First tries to find any stock reservations for the given $orderDetail in the given $warehouse and removes
     * them from the database. If the remaining/open quantity (quantity - shipped - picked) is greater zero, new stock
     * reservations for that remaining quantity are created. Finally, the cached reserved stock of the respective
     * article detail is updated.
     *
     * @param Warehouse $warehouse
     * @param OrderDetail $orderDetail
     * @return OrderStockReservation[]
     */
    public function reservePickableStockForOrderDetail(Warehouse $warehouse, OrderDetail $orderDetail)
    {
        // Check the order detail for a real article detail whose stock is managed
        /** @var ArticleDetail|null $articleDetail */
        $articleDetail = $this->entityManager->getRepository(ArticleDetail::class)->findOneBy([
            'number' => $orderDetail->getArticleNumber(),
        ]);
        if (!$articleDetail || ($articleDetail->getAttribute() && $articleDetail->getAttribute()->getPickwareStockManagementDisabled())) {
            return [];
        }

        // Remove all existing reservations for the order detail
        $this->pickwareErpStockReservation->clearStockReservationsForOrderDetail($warehouse, $orderDetail);

        // Create new stock reservations that are pre-allocated to the correct bin locations
        $remainingQuantity = $this->orderDetailQuantityCalculator->calculateRemainingQuantityToShip($orderDetail);
        $stockChanges = $this->createPickingStockChanges(
            $articleDetail,
            $warehouse,
            $remainingQuantity
        );
        $stockReservations = $this->pickwareErpStockReservation->reserveStockForStockChanges(
            $articleDetail,
            $orderDetail,
            $stockChanges
        );

        $this->derivedPropertyUpdater->recalculateReservedStockForArticleDetailInWarehouse($articleDetail, $warehouse);

        return $stockReservations;
    }

    /**
     * Fetches all bin location mappings sorted according to the 'picking' strategy:
     *
     *  - bin location mappings sorted by ascending stock (alphabetical order in case of same stock)
     *  - unknwon bin location mapping
     *
     * This sort order ensures that bin locations mappings with little stock are eventually dissolved and hence the
     * total number of bin location mappings per article detail is minimized. Finally the stock changes for the bin
     * location mappings are created starting with the first result and respecting the reserved quantity of the mappings
     * until all given $quantity is allocated across one or more bin locations. Finally the created bin location stock
     * changes are returned.
     *
     * @param ArticleDetail $articleDetail
     * @param Warehouse $warehouse
     * @param int $quantity
     * @return BinLocationStockChange[]
     */
    private function createPickingStockChanges(ArticleDetail $articleDetail, Warehouse $warehouse, $quantity)
    {
        if ($quantity <= 0) {
            return [];
        }

        // Find all bin location mappings, sorted by ascending stock (null bin location last)
        $builder = $this->entityManager->createQueryBuilder();
        $builder
            ->select(
                'binLocationMapping',
                'CASE WHEN binLocation = :nullBinLocation THEN 1 ELSE 0 END AS HIDDEN isNullBinLocation'
            )
            ->from(ArticleDetailBinLocationMapping::class, 'binLocationMapping')
            ->join('binLocationMapping.binLocation', 'binLocation')
            ->where('binLocationMapping.articleDetail = :articleDetail')
            ->andWhere('binLocation.warehouse = :warehouse')
            ->orderBy('isNullBinLocation', 'ASC')
            ->addOrderBy('binLocationMapping.stock', 'ASC')
            ->addOrderBy('binLocation.code', 'ASC')
            ->setParameters([
                'articleDetail' => $articleDetail,
                'nullBinLocation' => $warehouse->getNullBinLocation(),
                'warehouse' => $warehouse,
            ]);
        $binLocationMappings = $builder->getQuery()->getResult();

        return $this->stockChangeListFactory->createPickingStockChangesFromBinLocationMappings(
            $quantity,
            $binLocationMappings,
            $warehouse->getNullBinLocation()
        );
    }
}
