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

use Shopware\Components\Model\ModelManager;
use Shopware\Models\Article\Detail as ArticleDetail;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonSetArticles\Util;

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

    public function __construct(ModelManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    /**
     * Calculates the number of articles (basket entry) that can be put in the basket right now. This number considers
     * all articles that are inside the basket and considers if sub articles are already inside as part of a set article
     * and vice versa. Returns null if the allow quantity is not limited.
     *
     * Remark: We calculate this number only for set and sub articles since regular articles will be handled correctly
     * by Shopware.
     *
     * @param array $basketEntries
     * @param ArticleDetail $articleDetail
     * @return int|null
     */
    public function getAllowedQuantityToAddToBasketForArticleDetail(array $basketEntries, $articleDetail)
    {
        if ($articleDetail->getAttribute() && $articleDetail->getAttribute()->getViisonSetArticleActive()) {
            return $this->getAllowedQuantityToAddToBasketForSetArticle($basketEntries, $articleDetail);
        }

        return $this->getAllowedQuantityToAddToBasketForRegularArticle($basketEntries, $articleDetail);
    }

    /**
     * Calculates the maximum quantity for an article in the basket by checking how many are currently in the basket
     * and how many can be added. This also checks related set articles and subarticles depending on if the supplied
     * article is a set article or a regular article. Returns null if the maximum quantity is not limited.
     *
     * @param array $basketEntries
     * @param $articleDetail
     * @return int|null
     */
    public function getMaximumPossibleQuantityInBasket(array $basketEntries, $articleDetail)
    {
        $allowedQuantityToAddToBasket = $this->getAllowedQuantityToAddToBasketForArticleDetail($basketEntries, $articleDetail);
        if ($allowedQuantityToAddToBasket === null) {
            return null;
        }

        $basketEntryQuantities = $this->getBasketEntryData($basketEntries);
        $quantityInBasket = $this->getArticleQuantityInBasket($basketEntryQuantities, $articleDetail->getId());

        return $allowedQuantityToAddToBasket + $quantityInBasket;
    }

    /**
     * Calculates the number of sub articles that are inside the given basket. This function only considers real
     * articles that are in the basket individually, but sub articles that are part of a set article inside the basket.
     *
     * @param array $basketEntryQuantities
     * @param int $articleDetailId
     * @return int
     */
    private function getArticleQuantityInBasket($basketEntryQuantities, $articleDetailId)
    {
        $result = 0;
        foreach ($basketEntryQuantities as $basketEntryQuantity) {
            if ($basketEntryQuantity['articleDetail']->getId() === $articleDetailId) {
                $result += $basketEntryQuantity['basketEntry']->getQuantity();
            }
        }

        return $result;
    }

    /**
     * Calculates the number of sub articles that are inside the given basket. This function does not consider
     * sub articles that are in the basket individually, but only sub articles that are part of a set articles in the
     * basket.
     *
     * @param array $basketEntryData
     * @param int $articleDetailId
     * @param array $setArticles
     * @return int
     */
    private function getSubArticleQuantityInBasket($basketEntryData, $articleDetailId, $setArticles)
    {
        // Change set article array to get "id -> set article" association
        $setArticleIds = array_map(function ($setArticle) {
            return $setArticle->getSetId();
        }, $setArticles);
        $setArticles = array_combine($setArticleIds, $setArticles);

        // Map set article entities to the basketEntryData
        $basketEntryData = array_map(function ($basketEntryQuantity) use ($setArticles) {
            $basketEntryQuantity['setArticle'] = $setArticles[$basketEntryQuantity['articleDetail']->getId()];

            return $basketEntryQuantity;
        }, $basketEntryData);

        $result = 0;
        foreach ($basketEntryData as $basketEntry) {
            if (isset($basketEntry['setArticle']) &&
                $basketEntry['setArticle']->getArticleDetailId() === $articleDetailId) {
                $result += $basketEntry['basketEntry']->getQuantity() * $basketEntry['setArticle']->getQuantity();
            }
        }

        return $result;
    }

    /**
     * Considers other set article already inside the basket and their subarticles (as part of a set article and
     * separately as a regular article). Returns null if the allow quantity is not limited.
     *
     * @param array $basketEntries
     * @param $articleDetail
     * @return int|null
     */
    private function getAllowedQuantityToAddToBasketForSetArticle(array $basketEntries, $articleDetail)
    {
        $setArticleRepository = $this->entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle');

        // Check if the set article should be limited at all
        $setArticleAvailability = $setArticleRepository->getCombinedSetArticleDetailsData($articleDetail->getId());
        if (!$setArticleAvailability['laststock'] && !($setArticleAvailability['maxpurchase'] >= 1)) {
            return null;
        }

        $basketEntryQuantities = $this->getBasketEntryData($basketEntries);
        $relatedSetArticles = $setArticleRepository->findBy([
            'setId' => $articleDetail->getId(),
        ]);
        // Consider each sub article separately and check which one limits the current basket update
        $maximumAvailableSetArticle = PHP_INT_MAX;
        foreach ($relatedSetArticles as $relatedSetArticle) {
            $subArticle = $relatedSetArticle->getArticleDetail();
            $limitedInStock = $this->getLimitedArticleInStock($subArticle);
            if (!$limitedInStock) {
                continue;
            }

            $quantityAsRegularArticle = $this->getArticleQuantityInBasket($basketEntryQuantities, $subArticle->getId());
            $allSetArticlesOfCurrentSubArticle = $setArticleRepository->findBy([
                'articleDetailId' => $subArticle->getId(),
            ]);
            $quantityAsOtherSetArticlePart = $this->getSubArticleQuantityInBasket($basketEntryQuantities, $subArticle->getId(), $allSetArticlesOfCurrentSubArticle);

            // All instock that is left for the current set article, according to the current sub article
            $remainingSetArticleInStock = floor(($limitedInStock - $quantityAsRegularArticle - $quantityAsOtherSetArticlePart) / $relatedSetArticle->getQuantity());
            $maximumAvailableSetArticle = min($maximumAvailableSetArticle, $remainingSetArticleInStock);
        }

        return (int) $maximumAvailableSetArticle;
    }

    /**
     * Considers each sub article already inside the basket (as part of a another set or separately as a regular
     * article). Returns null if the allow quantity is not limited.
     *
     * @param array $basketEntries
     * @param $articleDetail
     * @return int|null
     */
    private function getAllowedQuantityToAddToBasketForRegularArticle(array $basketEntries, $articleDetail)
    {
        $setArticleRepository = $this->entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle');
        $relatedSetArticles = $setArticleRepository->findBy([
            'articleDetailId' => $articleDetail->getId(),
        ]);
        if (count($relatedSetArticles) === 0) {
            return null;
        }

        // Check if the article should be limited at all
        $limitedInStock = $this->getLimitedArticleInStock($articleDetail);
        if (!$limitedInStock) {
            return null;
        }

        $basketEntryQuantities = $this->getBasketEntryData($basketEntries);
        $quantityAsSetArticlePart = $this->getSubArticleQuantityInBasket($basketEntryQuantities, $articleDetail->getId(), $relatedSetArticles);
        $quantityAsRegularArticle = $this->getArticleQuantityInBasket($basketEntryQuantities, $articleDetail->getId());

        return $limitedInStock - ($quantityAsSetArticlePart + $quantityAsRegularArticle);
    }

    /**
     * Fetches all article details for the passed basket entries. Since these
     * models have no entity relation, we map them manually in an array.
     *
     * @param array $basketEntries
     * @return array
     */
    private function getBasketEntryData(array $basketEntries)
    {
        $orderNumbers = array_map(function ($basketEntry) {
            return $basketEntry->getOrderNumber();
        }, $basketEntries);
        $indexedBasketEntries = array_combine($orderNumbers, $basketEntries);
        $articleDetails = $this->entityManager->getRepository('Shopware\\Models\\Article\\Detail')->findBy([
            'number' => $orderNumbers,
        ]);

        // Map basket entries and article details together in a single result
        return array_map(function ($articleDetail) use ($indexedBasketEntries) {
            return [
                'basketEntry' => $indexedBasketEntries[$articleDetail->getNumber()],
                'articleDetail' => $articleDetail,
            ];
        }, $articleDetails);
    }

    /**
     * Returns the maximum quantity that can be purchased of the given $articleDetail. Both the `lastStock` and
     * `maxPurchase` configurations are considered for selecting the correct quantity. Returns null if no purchase limit
     * should be applied.
     *
     * Note: For this specific article detail check, we need to check the `lastStock` flag on the detail or article
     * level depending on the Shopware version. In Shopware >= 5.4.0 `lastStock` may be set on the article level,
     * but not on the detail level, in which case it should be considered turned off.
     *
     * @param ArticleDetail $articleDetail
     * @return int|null
     */
    protected function getLimitedArticleInStock(ArticleDetail $articleDetail)
    {
        $supportsDetailLastStock = ViisonCommonUtil::assertMinimumShopwareVersion('5.4.0');
        $article = $articleDetail->getArticle();
        $lastStockActive = $supportsDetailLastStock ? $articleDetail->getLastStock() : $article->getLastStock();
        $maxPurchaseActive = $articleDetail->getMaxPurchase() > 0;

        if ($lastStockActive) {
            if ($maxPurchaseActive) {
                return min($articleDetail->getInStock(), $articleDetail->getMaxPurchase());
            }

            return $articleDetail->getInStock();
        }

        if ($maxPurchaseActive) {
            return $articleDetail->getMaxPurchase();
        }

        return Util::getShopwareMaxPurchase();
    }
}
