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

use Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base;
use Shopware\Plugins\ViisonSetArticles\Util;

class ArticleSubscriber extends Base
{
    /**
     * Remember checkoutBasket data to edit sCheckBasketQuantities later
     *
     * @var array
     */
    private static $checkoutBasketContent = [];

    /**
     * @inheritdoc
     */
    public static function getSubscribedEvents()
    {
        return [
            'sArticles::sGetArticleCharts::after' => 'editArticleCharts',
            'sBasket::sCheckBasketQuantities::after' => 'onAfterCheckBasketQuantities',
            'Shopware_Controllers_Frontend_Checkout::addArticleAction::after' => 'afterAddArticleAction',
            'Shopware_Controllers_Frontend_Checkout::cartAction::after' => 'editCart',
            'Shopware_Controllers_Frontend_Checkout::confirmAction::after' => 'editCheckout',
            'Shopware_Controllers_Frontend_Checkout::finishAction::before' => 'saveCheckoutBasket',
            'Shopware_Controllers_Frontend_Checkout::finishAction::after' => 'editCheckoutFinish',
            'Shopware_Controllers_Frontend_Checkout::getAvailableStock::after' => 'onAfterGetAvailableStock',
            'Shopware_Controllers_Frontend_Detail::indexAction::after' => 'editFrontendDetail',
        ];
    }

    /**
     * Manipulates the articles that are returned by the GetArticlesChart function. This is relevant to Manipulate
     * topseller slider widget or other charts (new articles, hilights, etc)
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function editArticleCharts(\Enlight_Hook_HookArgs $args)
    {
        $articles = $args->getReturn();

        // Manipulate articles
        $articles = $this->makeSimpleArticleManipulation($articles);

        $args->setReturn($articles);
    }

    /**
     * Does a simple "if its a set article edit values" manipulation, since it is needed in multiple hooks. Manipulates
     * instock, shippingtime, maxpurchase, laststock, available
     *
     * @param array $articles
     * @@return array
     */
    private function makeSimpleArticleManipulation($articles)
    {
        $setArticles = array_values(array_filter($articles, static function ($article) {
            return $article['viison_setarticle_active'] == 1;
        }));
        $setArticleDetailIds = array_map(static function ($setArticle) {
            return $setArticle['articleDetailsID'];
        }, $setArticles);
        $setArticleAvailabilities = $this->get('models')
            ->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
            ->getCombinedSetArticleDetailsBatchData($setArticleDetailIds);

        foreach ($setArticles as &$article) {
            if (!array_key_exists($article['articleDetailsID'], $setArticleAvailabilities)) {
                continue;
            }

            $setArticleAvailability = $setArticleAvailabilities[$article['articleDetailsID']];
            $article['instock'] = $setArticleAvailability['instock'];
            $article['shippingtime'] = $setArticleAvailability['shippingtime'];
            $article['maxpurchase'] = $setArticleAvailability['maxpurchase'];
            $article['sReleasedate'] = $setArticleAvailability['releasedate'] ? $setArticleAvailability['releasedate']->format('d.m.Y') : null;
            // Remark: if not available: set laststock = 1 => "buy now" button is disabled
            $article['laststock'] = $setArticleAvailability['available'] ? 0 : 1;
            $article['available'] = $setArticleAvailability['available'];
        }

        return $articles;
    }

    /**
     * Edit Checkout/Finish basket to suppress not-available message for set articles.
     *
     * @param \Enlight_Hook_HookArgs $args
     * @throws \Exception
     */
    public function onAfterCheckBasketQuantities(\Enlight_Hook_HookArgs $args)
    {
        $session = Shopware()->Session();
        $sessionID = $session['sessionId'];
        if (self::$checkoutBasketContent[$sessionID]) {
            $newLastStock = $this->editLastStock($args->getReturn(), self::$checkoutBasketContent[$sessionID]);
            $args->setReturn($newLastStock);
        }
    }

    /**
     * Manipulate "Basket Info" when an Article is added and suppress possible "is not available" message for set
     * article.
     *
     * Remark: this manipulation is needed for SW4.3, but overwritten in SW5+ by editLastStock
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function afterAddArticleAction(\Enlight_Hook_HookArgs $args)
    {
        $db = $this->get('db');
        $orderNumber = $args->getSubject()->Request()->getParam('sAdd');
        $sql = "SELECT
                    s_articles_details.id as articledetailID,
                    s_articles_attributes.viison_setarticle_active
                FROM
                    s_articles_details
                LEFT JOIN s_articles_attributes
                    ON s_articles_details.id = s_articles_attributes.articledetailsID
                WHERE
                    s_articles_details.ordernumber = '" . $db->quote($orderNumber, \PDO::PARAM_STR) . "'";
        $article = $db->fetchRow($sql);

        if ($article['viison_setarticle_active'] == 1) {
            $details = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                ->getCombinedSetArticleDetailsData($article['articledetailID']);
            // If set article is available override message with null (to suppress warning message)
            if ($details['available']) {
                $args->getSubject()->View()->sBasketInfo = null;
            }
        }
    }

    /**
     * Manipulate set article view in Frontend-Cart. Compute and display custom instock, shippingtime, availability
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function editCart(\Enlight_Hook_HookArgs $args)
    {
        $args->getSubject()->View()->sBasket = $this->editBasked($args->getSubject()->View()->sBasket);
    }

    /**
     * Manipulate set article view in Frontend-Checkout. Compute and display custom instock, shippingtime, availability
     * if the basket is set (this may not be the case if the checkout confirm fails due to the customer being logged
     * out)
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function editCheckout(\Enlight_Hook_HookArgs $args)
    {
        $basket = $args->getSubject()->View()->sBasket;
        if (!$basket || !$basket['content']) {
            return;
        }

        $args->getSubject()->View()->sBasket = $this->editBasked($basket);
        $args->getSubject()->View()->sLaststock = $this->editLastStock(
            $args->getSubject()->View()->sLaststock,
            $args->getSubject()->View()->sBasket['content']
        );
    }

    /**
     * Remember basket from Checkout/Finish action to edit item availability in later check
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function saveCheckoutBasket(\Enlight_Hook_HookArgs $args)
    {
        /** @var \Shopware_Controllers_Frontend_Checkout $eventManager */
        $checkoutController = $args->getSubject();
        // When calling `getBasket()` in the checkout controller, multiple other refresh and recalculations are called
        // which causes side effects that overwrite country-specific tax rules. To prevent these side effects, we have
        // to use reflection to access the sBasket entity and call `sGetBasketData()` on the sBasket entity directly to
        // fetch the basket data without refresh.
        $reflection = new \ReflectionClass($checkoutController);
        $property = $reflection->getProperty('basket');
        $property->setAccessible(true);
        $sBasket = $property->getValue($checkoutController);
        $basket = $this->editBasked($sBasket->sGetBasketData());
        $sessionID = $basket['content'][0]['sessionID'];
        self::$checkoutBasketContent[$sessionID] = $basket['content'];
    }

    /**
     * Fix deliverytime in checkout/finish if customer has custom template that shows delivery information
     * @param \Enlight_Hook_HookArgs $args
     */
    public function editCheckoutFinish(\Enlight_Hook_HookArgs $args)
    {
        $basket = $args->getSubject()->View()->sBasket;

        /**
         * Try to restore the availability of set articles from the point where the order was not saved yet. Otherwise a
         * freshly calculated availability may differ from the actual availability of the saved order.
         */
        foreach ($basket['content'] as &$article) {
            if ($article['additional_details'] && $article['additional_details']['viison_setarticle_active']) {
                if (isset(self::$checkoutBasketContent[$article['sessionID']])) {
                    foreach (self::$checkoutBasketContent[$article['sessionID']] as $articleBeforeSaveOrder) {
                        if ($articleBeforeSaveOrder['id'] === $article['id']) {
                            $availabilityRecord = $articleBeforeSaveOrder;
                            break;
                        }
                    }
                } else {
                    // Fallback: fetch availability anew
                    $availabilityRecord = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                        ->getCombinedSetArticleDetailsData($article['articleDetailId']);
                }
                // Skip if no information could be retrieved any way
                if (!$availabilityRecord) {
                    continue;
                }
                $article['shippingtime'] = $availabilityRecord['shippingtime'];
                $article['instock'] = $availabilityRecord['instock'];
                $article['releasedate'] = $availabilityRecord['releasedate'];
                $article['sReleaseDate'] = $availabilityRecord['releasedate'];
                $article['laststock'] = false;
            }
        }

        // set return
        $args->getSubject()->View()->sBasket = $basket;
    }

    /**
     * Edit getAvailableStock after in Checkout suppress not-available messages for set article after AddArticle.
     * ("Grundeinstellungen->Warenbestand/Artikeldetails->Lagerbestand in Warenkorb anzeigen")
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterGetAvailableStock(\Enlight_Hook_HookArgs $args)
    {
        $return = $args->getReturn();
        $details = $this->getDetailsFromOrdernumber($return['ordernumber']);

        if ($details['viison_setarticle_active']) {
            // recalculate instock
            $inStock = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                ->getCombinedSetArticleDetailsData($details['id']);
            $return['instock'] = $inStock['instock'];
            // Same reason as in editFrontendDetail(): handle the sub article laststock in the instock availability and
            // set laststock according to the calculated availability
            $return['laststock'] = !$inStock['available'];
            $args->setReturn($return);
        }
    }

    /**
     * Manipulate set article view in Frontend-Detail (of Article). Compute and display custom instock, shippingtime,
     * availability.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function editFrontendDetail(\Enlight_Hook_HookArgs $args)
    {
        $article = $args->getSubject()->View()->sArticle;
        if ($article['viison_setarticle_active'] == 1) {
            $details = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                ->getCombinedSetArticleDetailsData($article['articleDetailsID'], $article['minpurchase']);
            $article['instock'] = $details['instock'];
            $article['shippingtime'] = $details['shippingtime'];
            $article['isAvailable'] = $details['available'];
            $article['maxpurchase'] = $details['maxpurchase'];
            $article['weight'] = $details['weight'];
            // There are two release date fields in the article information array. Don't ask me why.
            $article['sReleasedate'] = $details['releasedate'] ? $details['releasedate']->format('d.m.Y') : null;
            $article['sReleaseDate'] = $article['sReleasedate'];

            $args->getSubject()->View()->sArticle = $article;
        }
    }

    /**
     * Manipulates "sLastStock" (only set articles) in checkout/confirm view. Supress not-available-messages for set
     * articles that are available.
     *
     * @param $sLastStock
     * @param $basketContent
     * @return array
     */
    private function editLastStock($sLastStock, $basketContent)
    {
        // Return early if no actual set articles are in this basket
        $hasSetArticles = array_reduce(
            $basketContent,
            function ($carry, $item) {
                return $carry || Util::isSetArticleByDetailId($item['articleDetailId']);
            },
            false
        );
        if (!$hasSetArticles) {
            return $sLastStock;
        }

        // Update set article availability
        $setArticleBasketArticles = array_values(array_filter($basketContent, static function ($basketArticle) {
            return $basketArticle['additional_details']['viison_setarticle_active'] == 1;
        }));
        $setArticleDetailIds = array_map(static function ($setArticleBasketArticle) {
            return $setArticleBasketArticle['articleDetailId'];
        }, $setArticleBasketArticles);
        $setArticleAvailabilities = $this->get('models')
            ->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
            ->getCombinedSetArticleDetailsBatchData($setArticleDetailIds);
        foreach ($setArticleBasketArticles as $setArticleBasketArticle) {
            foreach ($sLastStock['articles'] as $ordernumber => &$lastStockArticle) {
                if ((string) $ordernumber === (string) $setArticleBasketArticle['ordernumber']
                && array_key_exists($setArticleBasketArticle['articleDetailId'], $setArticleAvailabilities)) {
                    // Current basketArticle equals current lastStockArticle: override lastStockArticle availability
                    // Remark: since availability is calculated with sub article instock and laststock, use only set
                    // article availability here
                    $details = $setArticleAvailabilities[$setArticleBasketArticle['articleDetailId']];
                    $lastStockArticle['OutOfStock'] = !$details['available'];
                }
            }
        }

        // Fold OutOfStock values: recalculate and set hideBasket
        $sLastStock['hideBasket'] = array_reduce(
            $sLastStock['articles'],
            function ($carry, $item) {
                return $carry || $item['OutOfStock'];
            },
            false
        );

        return $sLastStock;
    }

    /**
     * Manipulate set article in Frontend-Checkout-Confirm view. Compute and display custom instock, shippingtime, availability
     *
     * @param array $basket
     * @return array
     */
    private function editBasked($basket)
    {
        foreach ($basket['content'] as $key => $article) {
            if ($article['additional_details']['viison_setarticle_active'] == 1) {
                $details = $this->get('models')->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                    ->getCombinedSetArticleDetailsData($article['articleDetailId'], $article['quantity']);
                $article['additional_details']['instock'] = $details['instock'];
                $article['shippingtime'] = $details['shippingtime'];
                $article['maxpurchase'] = $details['maxpurchase'];
                $article['instock'] = $details['instock'];
                // There are two release date values
                $article['releasedate'] = $details['releasedate'] ? $details['releasedate']->format('d.m.Y') : null;
                $article['sReleaseDate'] = $article['releasedate'];
                // Also add weight of sub articles to set article to correctly compute its shipping cost
                $article['additional_details']['weight'] = $details['weight'];

                // if maxpurchase == 0 (article not available), show "not available" by setting laststock
                if ($article['maxpurchase'] == 0) {
                    $article['additional_details']['laststock'] = 1;
                } else {
                    $article['additional_details']['laststock'] = 0;
                }

                // Remark: also add availibility (even if its not directly needed) to use in sLastStock calculation
                // later (dont use laststock, because we force-disable it)
                $article['available'] = $details['available'];

                $basket['content'][$key] = $article;
            }
        }

        return $basket;
    }

    /**
     * Fetches some article information by given ordernumber
     *
     * @param string $ordernumber
     * @return array
     */
    private function getDetailsFromOrdernumber($ordernumber)
    {
        $row = $this->get('db')->fetchRow(
            'SELECT
                s_articles_details.id,
                s_articles_details.ordernumber,
                s_articles_attributes.viison_setarticle_active
            FROM s_articles_details
                LEFT JOIN s_articles_attributes
                    ON s_articles_details.id = s_articles_attributes.articledetailsID
                WHERE s_articles_details.ordernumber = "' . $ordernumber . '"'
        );

        return $row;
    }
}
