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

use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Shopware\Components\Model\ModelRepository;
use Shopware\Plugins\ViisonSetArticles\Util;

class Repository extends ModelRepository
{

    /**
     * Copies all sub articles from one set article to another. Old entries in the target are not deleted! This method
     * does not check if the articles are actually set articles, since it only copies entries from the SetArticle model.
     *
     * @param int $fromArticleDetailId
     * @param int $toArticleDetailId
     */
    public function copySubArticles($fromArticleDetailId, $toArticleDetailId)
    {
        $sets = $this->getEntityManager()->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')->findBy([
            'setId' => $fromArticleDetailId,
        ]);

        foreach ($sets as $set) {
            $newSet = new SetArticle();
            $newSet->setSetId($toArticleDetailId);
            $newSet->setArticleDetailId($set->getArticleDetailId());
            $newSet->setQuantity($set->getQuantity());

            $this->getEntityManager()->persist($newSet);
            $this->getEntityManager()->flush($newSet);
        }
    }

    /**
     * Returns the calculated weight of one given set article.
     *
     * @param int $setArticleDetailId
     * @return Query
     */
    public function getSetArticleWeight($setArticleDetailId)
    {
        $builder = $this->getEntityManager()->createQueryBuilder();
        $builder
            ->add('select', 'SUM(ad.weight * sa.quantity) as weight')
            ->from('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle', 'sa')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'ad', 'WITH', 'ad.id = sa.articleDetailId')
            ->andWhere('sa.setId = :setArticleDetailId')
            ->setParameter('setArticleDetailId', $setArticleDetailId)
            ->groupBy('sa.setId');

        $result = $builder->getQuery()->getArrayResult();

        return $result[0]['weight'];
    }

    /**
     * Returns a query builder with the common sub article properties required to calculate set article availabilities.
     *
     * @return QueryBuilder
     */
    private function getSubArticlePropertyQueryBuilder()
    {
        $builder = $this->getEntityManager()->createQueryBuilder();
        $builder
            ->select(
                'sa.setId as setArticleDetailId',
                'ad.id as articleDetailId',
                'ad.inStock as instock',
                'ad.maxPurchase as maxpurchase',
                'ad.shippingTime as shippingtime',
                'sa.quantity as quantityInSet',
                'a.active as mainActive',
                'ad.active as variantActive',
                'ad.releaseDate',
                'a.configuratorSetId as configuratorId',
                'ad.weight'
            )
            ->from('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle', 'sa')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'ad', 'WITH', 'ad.id = sa.articleDetailId')
            ->leftJoin('Shopware\\Models\\Article\\Article', 'a', 'WITH', 'a.id = ad.articleId');

        // Add sub article main detail shipping time
        $builder
            ->addSelect('mainArticleDetail.shippingTime as mainShippingtime')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'mainArticleDetail', 'WITH', 'mainArticleDetail.articleId = ad.articleId')
            ->andWhere('mainArticleDetail.kind = 1');

        // Add lastStock selection
        $builder = Util::addLastStockSelectionToQueryBuilder($builder);

        return $builder;
    }

    /**
     * @param array $subArticle
     * @param int $setQuantity
     * @param int $shopwareDefaultMaxPurchase
     * @return array
     */
    private function createSubArticleAvailability(array $subArticle, $setQuantity, $shopwareDefaultMaxPurchase)
    {
        return [
            'instock' => Util::getSubArticleInstock($subArticle),
            'available' => Util::getSubArticleAvailability($subArticle, $setQuantity),
            'maxpurchase' => Util::getSubMaxPurchase($subArticle, $shopwareDefaultMaxPurchase),
            'weight' => Util::getSubWeight($subArticle),
            'shippingtime' => Util::getSubArticleDeliveryTime($subArticle),
            'releaseDate' => $subArticle['releaseDate'],
            'laststock' => $subArticle['laststock'],
            'articleDetailId' => $subArticle['articleDetailId'],
        ];
    }

    /**
     * Fetches sub article availability of each sub article separately for all given articleDetails in
     * $setArticleDetailIds.
     * Remark: The supplied $setQuantity is used for all set articles, individual setQuantities are not possible
     * currently.
     *
     * @param int[] $setArticleDetailIds
     * @param int $setQuantity
     * @return array
     */
    private function getSubArticleAvailabilitiesBySetArticleDetailIds($setArticleDetailIds, $setQuantity = 1)
    {
        $builder = $this->getSubArticlePropertyQueryBuilder();

        // Fetch sub articles of all the given set article details.
        $builder
            ->andWhere('sa.setId IN (:setArticleDetailIds)')
            ->setParameter('setArticleDetailIds', $setArticleDetailIds);

        $subArticles = $builder->getQuery()->getArrayResult();

        $subArticlesBySetArticleDetailId = [];
        $shopwareDefaultMaxPurchase = Util::getShopwareMaxPurchase();
        foreach ($subArticles as $subArticle) {
            if (!array_key_exists($subArticle['setArticleDetailId'], $subArticlesBySetArticleDetailId)) {
                $subArticlesBySetArticleDetailId[$subArticle['setArticleDetailId']] = [];
            }
            $subArticlesBySetArticleDetailId[$subArticle['setArticleDetailId']][] = $this->createSubArticleAvailability(
                $subArticle,
                $setQuantity,
                $shopwareDefaultMaxPurchase
            );
        }

        return $subArticlesBySetArticleDetailId;
    }

    /**
     * Fetches sub article availability of each sub article separately for all given articleDetails in
     * $setArticleDetailIds and their variants(siblings).
     * Remark: The supplied $setQuantity is used for all set articles, individual setQuantities are not possible
     * currently.
     *
     * @param int[] $setArticleDetailIds
     * @param int $setQuantity
     * @return array
     */
    private function getSubArticleAvailabilitiesBySetArticleDetailIdsWithSiblings($setArticleDetailIds, $setQuantity = 1)
    {
        $builder = $this->getSubArticlePropertyQueryBuilder();

        // Fetch sub articles of all set article variants of the given set article details. Hence filter by all set
        // article variants (siblings) of the given set articles.
        $builder
            ->addSelect(
                'setArticleArticle.id as setArticleArticleId'
            )
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'setArticleDetail', 'WITH', 'setArticleDetail.id = sa.setId')
            ->leftJoin('Shopware\\Models\\Article\\Article', 'setArticleArticle', 'WITH', 'setArticleArticle.id = setArticleDetail.articleId')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'setArticleSibling', 'WITH', 'setArticleSibling.articleId = setArticleArticle.id')
            ->andWhere('setArticleSibling.id IN (:setArticleDetailIds)')
            ->setParameter('setArticleDetailIds', $setArticleDetailIds)
            ->distinct();

        $subArticles = $builder->getQuery()->getArrayResult();

        $shopwareDefaultMaxPurchase = Util::getShopwareMaxPurchase();
        foreach ($subArticles as &$subArticle) {
            $subArticleAvailability = $this->createSubArticleAvailability(
                $subArticle,
                $setQuantity,
                $shopwareDefaultMaxPurchase
            );
            $subArticleAvailability['setArticleArticleId'] = $subArticle['setArticleArticleId'];
            $subArticleAvailability['setArticleDetailId'] = $subArticle['setArticleDetailId'];
            $subArticle = $subArticleAvailability;
        }

        return $subArticles;
    }

    /**
     * Groups the sub article availabilities by their set article detail id with the addition of the set article article
     * id.
     *
     * @param array $subArticleAvailabilities
     * @return array grouped sub article availabilities by their set article. Example:
     *
     *  $result = [
     *      setArticleDetailId => [
     *          setArticleArticleId => ..
     *          subArticleAvailabilities => [
     *              [ .. ],
     *              [ .. ],
     *          ],
     *      ],
     *  ]
     */
    private function groupSubArticleAvailabilitiesBySetArticles(array $subArticleAvailabilities)
    {
        $subArticlesBySetArticleDetailId = [];

        foreach ($subArticleAvailabilities as $subArticle) {
            $setArticleDetailId = $subArticle['setArticleDetailId'];
            if (!array_key_exists($setArticleDetailId, $subArticlesBySetArticleDetailId)) {
                $subArticlesBySetArticleDetailId[$setArticleDetailId] = [];
            }
            $subArticlesBySetArticleDetailId[$setArticleDetailId]['setArticleArticleId'] = $subArticle['setArticleArticleId'];
            unset($subArticle['setArticleArticleId']);
            unset($subArticle['setArticleDetailId']);
            $subArticlesBySetArticleDetailId[$setArticleDetailId]['subArticleAvailabilities'][] = $subArticle;
        }

        return $subArticlesBySetArticleDetailId;
    }

    /**
     * Calculated the maximum amount of set articles that can be bought, calculated by the given SubArticleDetails
     * array. This method does not consider set articles that may already be in the cart.
     *
     * @param array $subArticleDetails
     * @return int
     */
    public function getMaximumAvailableNumberOfSetArticleBySubArticleDetails($subArticleDetails)
    {
        $result = PHP_INT_MAX;
        foreach ($subArticleDetails as $subArticleDetail) {
            if (!$subArticleDetail['available']) {
                // Available = 0 if sub article is not available. No need to look further.
                $result = 0;
                break;
            } elseif ($subArticleDetail['laststock']) {
                // Available = inStock if sub article lastStock is set
                $result = min($result, $subArticleDetail['instock']);
            }
            // Limit availability by maxPurchase anyway
            $result = min($result, $subArticleDetail['maxpurchase']);
        }

        return $result;
    }

    /**
     * Compute availability information for set article from sub articles, logically combined into one array.
     *
     * @param int $setArticleDetailId article detail id of set article
     * @param int $setQuantity amount of set articles (in normal view: 'minpurchase', can vary in cart)
     * @return array manipulated availability information
     */
    public function getCombinedSetArticleDetailsData($setArticleDetailId, $setQuantity = 1)
    {
        return $this->getCombinedSetArticleDetailsBatchData([$setArticleDetailId], $setQuantity)[$setArticleDetailId];
    }

    /**
     * Aggregates the given $subArticles to a single result availability information array. E.g. it reduces all
     * `instock` down to the min() value of them.
     *
     * @param array $defaultSubArticleInformation
     * @param array $subArticles
     * @return array
     */
    private function aggregateSubArticleAvailabilities(array $defaultSubArticleInformation, array $subArticles)
    {
        $result = $defaultSubArticleInformation;
        foreach ($subArticles as $subArticle) {
            // max shippingtime
            $result['shippingtime'] = max($result['shippingtime'], $subArticle['shippingtime']);

            // min available instock
            $result['instock'] = min($result['instock'], $subArticle['instock']);

            // min available maxpurchase
            $result['maxpurchase'] = min($result['maxpurchase'], $subArticle['maxpurchase']);

            // general laststock
            $result['laststock'] = $result['laststock'] || ($subArticle['laststock'] && $subArticle['instock'] <= 0);

            // combined availability
            $result['available'] = $result['available'] && $subArticle['available'];

            // combined weight
            $result['weight'] += $subArticle['weight'];

            // latest releasedate
            $result['releasedate'] = $this->getLatestReleaseDate($result['releasedate'], $subArticle['releaseDate']);
        }

        return $result;
    }

    /**
     * Compute availability information for set articles from sub articles, logically combined into one array grouped by
     * set article detail ids.
     *
     * This method works similar to `getCombinedSetArticleDetailsBatchDataWithSiblings()` but ignores the sibling of the
     * set articles.
     *
     * @param int[] $setArticleDetailIds article detail ids of set articles
     * @param int $setQuantity amount of set articles (in normal view: 'minpurchase', can vary in cart)
     * @return array manipulated availability information by set article detail id
     */
    public function getCombinedSetArticleDetailsBatchData($setArticleDetailIds, $setQuantity = 1)
    {
        $defaultSubArticleInformation = $this->getDefaultSubArticleInformation();
        $subArticlesBySetArticleDetailId = $this->getSubArticleAvailabilitiesBySetArticleDetailIds($setArticleDetailIds, $setQuantity);

        // Return early with default values if no sub articles could be found at all
        if (count($subArticlesBySetArticleDetailId) === 0) {
            $result = [];
            foreach ($setArticleDetailIds as $setArticleDetailId) {
                $result[$setArticleDetailId] = $defaultSubArticleInformation;
            }

            return $result;
        }

        $combinedSetArticleDetailsBySetArticleDetailId = [];
        foreach ($subArticlesBySetArticleDetailId as $setArticleDetailId => $subArticles) {
            // Initialize threshold values
            $result = $this->aggregateSubArticleAvailabilities($defaultSubArticleInformation, $subArticles);
            $combinedSetArticleDetailsBySetArticleDetailId[$setArticleDetailId] = $result;
        }

        return $combinedSetArticleDetailsBySetArticleDetailId;
    }

    /**
     * Compute availability information for set articles and their siblings from sub articles, logically combined
     * into one array grouped by set article detail ids.
     *
     * This method works similar to `getCombinedSetArticleDetailsBatchData()` but also fetches the sibling of the set
     * articles.
     *
     * @param int[] $setArticleDetailIds article detail ids of set articles
     * @param int $setQuantity amount of set articles (in normal view: 'minpurchase', can vary in cart)
     * @return array manipulated availability information by set article detail id
     */
    public function getCombinedSetArticleDetailsBatchDataWithSiblings(array $setArticleDetailIds, $setQuantity = 1)
    {
        $defaultSubArticleInformation = $this->getDefaultSubArticleInformation();
        $subArticleAvailabilitiesWithSiblings = $this->getSubArticleAvailabilitiesBySetArticleDetailIdsWithSiblings(
            $setArticleDetailIds,
            $setQuantity
        );

        // Return early with default values if no sub articles could be found at all
        if (count($subArticleAvailabilitiesWithSiblings) === 0) {
            $result = [];
            foreach ($setArticleDetailIds as $setArticleDetailId) {
                $result[$setArticleDetailId] = $defaultSubArticleInformation;
            }

            return $result;
        }

        $subArticlesBySetArticleDetailId = $this->groupSubArticleAvailabilitiesBySetArticles(
            $subArticleAvailabilitiesWithSiblings
        );
        $combinedSetArticleDetailsBySetArticleDetailId = [];
        foreach ($subArticlesBySetArticleDetailId as $setArticleDetailId => $subArticles) {
            // Initialize threshold values
            $result = $this->aggregateSubArticleAvailabilities(
                $defaultSubArticleInformation,
                $subArticles['subArticleAvailabilities']
            );
            $combinedSetArticleDetailsBySetArticleDetailId[$setArticleDetailId]['setArticleArticleId'] = $subArticles['setArticleArticleId'];
            $combinedSetArticleDetailsBySetArticleDetailId[$setArticleDetailId]['availability'] = $result;
        }

        // Because we fetched all the siblings in this method, we need to group then again "down" to the set articles
        // that were actually requested.
        return $this->groupBySetArticle($combinedSetArticleDetailsBySetArticleDetailId, $setArticleDetailIds);
    }

    /**
     * Groups variants of the same set article and returns the availability information of the requested variant
     * with the additional information if one variant of the set article is available ('hasAvailableVariant').
     *
     * @param array $combinedSetArticleDetailsBySetArticleDetailId
     * @param array $setArticleDetailIds
     * @return array availability of set articles with information if any variant of the set article is available
     */
    private function groupBySetArticle(array $combinedSetArticleDetailsBySetArticleDetailId, array $setArticleDetailIds)
    {
        $setArticles = [];
        foreach ($setArticleDetailIds as $setArticleDetailId) {
            $articleId = $combinedSetArticleDetailsBySetArticleDetailId[$setArticleDetailId]['setArticleArticleId'];
            $variants = array_filter($combinedSetArticleDetailsBySetArticleDetailId, function ($element) use ($articleId) {
                return $element['setArticleArticleId'] === $articleId;
            });
            $available = array_column(array_column($variants, 'availability'), 'available');
            $availableVariant = in_array(true, $available);
            $setArticles[$setArticleDetailId] = $combinedSetArticleDetailsBySetArticleDetailId[$setArticleDetailId]['availability'];
            $setArticles[$setArticleDetailId]['hasAvailableVariant'] = $availableVariant;
        }

        return $setArticles;
    }

    /**
     * Fetches set article compositions for any set article given by order detail ids.
     *
     * @param array $orderDetailIds
     * @return array
     */
    public function getSetArticleCompositionByOrderDetailId($orderDetailIds)
    {
        $result = $this->getEntityManager()
            ->createQueryBuilder()
            ->select(
                'orderDetail.id as setArticleOrderDetailId',
                'subArticleDetail.number as subArticleOrderNumber',
                'setArticleComposition.quantity as subArticleQuantity'
            )
            ->from('Shopware\\Models\\Order\\Detail', 'orderDetail')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'articleDetail', 'WITH', 'articleDetail.number = orderDetail.articleNumber')
            ->leftJoin('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle', 'setArticleComposition', 'WITH', 'setArticleComposition.setId = articleDetail.id')
            ->leftJoin('Shopware\\Models\\Article\\Detail', 'subArticleDetail', 'WITH', 'subArticleDetail.id = setArticleComposition.articleDetailId')
            ->andWhere('orderDetail.id IN (:oderDetailIds)')
            ->setParameter('oderDetailIds', $orderDetailIds)
            ->getQuery()
            ->getArrayResult();

        $setArticleCompositions = [];
        foreach ($result as $setArticleComposition) {
            if (!array_key_exists($setArticleComposition['setArticleOrderDetailId'], $setArticleCompositions)) {
                $setArticleCompositions[$setArticleComposition['setArticleOrderDetailId']] = [];
            }

            $setArticleCompositions[$setArticleComposition['setArticleOrderDetailId']][] = [
                'ordernumber' => $setArticleComposition['subArticleOrderNumber'],
                'quantity' => $setArticleComposition['subArticleQuantity'],
            ];
        }

        return $setArticleCompositions;
    }

    /**
     * @param string|null $currentDate
     * @param string|null$subArticleDate
     * @return string|null mixed
     */
    private function getLatestReleaseDate($currentDate, $subArticleDate)
    {
        if (!$currentDate) {
            return $subArticleDate;
        }

        if ($currentDate && !$subArticleDate) {
            return $currentDate;
        }

        // Compare both dates, return latest (farthest in the future)
        return ($currentDate > $subArticleDate) ? $currentDate : $subArticleDate;
    }

    /**
     * @return array
     */
    private function getDefaultSubArticleInformation()
    {
        return [
            'instock' => 100000,
            'shippingtime' => 0,
            'maxpurchase' => Util::getShopwareMaxPurchase(),
            'available' => true,
            'laststock' => false,
            'weight' => 0.000,
            'releasedate' => null,
        ];
    }
}
