<?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\ViisonPickwareCommon\Classes;

use Shopware\Bundle\StoreFrontBundle\Struct\Media;
use Shopware\Components\Model\QueryBuilder;
use Shopware\CustomModels\ViisonPickwareERP\ItemProperty\ArticleDetailItemProperty;
use Shopware\CustomModels\ViisonPickwareERP\StockLedger\StockItem;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\ArticleDetailBinLocationMapping;
use Shopware\Models\Article\Price;
use Shopware\Models\Attribute\OrderDetail as OrderDetailAttribute;
use Shopware\Models\Shop\Shop;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Subscribers\PickwareAppConfig;
use Shopware_Plugins_Core_ViisonPickwareERP_Bootstrap;

class Util
{
    /**
     * Parses the app name from the requesting user agent string.
     *
     * @return string
     */
    public static function getRequestingAppName()
    {
        $parts = explode('/', $_SERVER['HTTP_USER_AGENT']);

        return $parts[0];
    }

    /**
     * Parses the app version from the requesting user agent string.
     *
     * @return string
     */
    public static function getRequestingAppVersion()
    {
        $parts = explode(' ', $_SERVER['HTTP_USER_AGENT']);
        $parts = explode('/', $parts[0]);

        return $parts[1];
    }

    /**
     * Fires an event to collect the names and versions of all installed Pickware
     * plugins and returns them in an array.
     *
     * @return array
     */
    final public static function getPickwarePluginVersions()
    {
        /** @var Shopware_Plugins_Core_ViisonPickwareERP_Bootstrap $viisonPickwareERP */
        $viisonPickwareERP = Shopware()->Container()->get('plugin_manager')->get('Core')->get('ViisonPickwareERP');
        $pluginVersions[$viisonPickwareERP->getName()] = [
            'label' => $viisonPickwareERP->getLabel(),
            'version' => $viisonPickwareERP->getVersion(),
        ];
        $pluginVersions['ViisonPickwareCommon'] = [
            'label' => 'PickwareCommon library',
            'version' => include(__DIR__ . '/../Version.php'),
        ];

        $pluginVersions = Shopware()->Container()->get('events')->filter(
            PickwareAppConfig::EVENT_COLLECT_PICKWARE_PLUGIN_VERSIONS,
            $pluginVersions
        );

        return $pluginVersions;
    }

    /**
     * Fetches all order detail data including article and article detail as well as bin location mappings, additional
     * variant texts, mapped property types and stock entry items for all orders having one of the given $orderIds.
     * The data is formatted to a hierarchical structure that is expected by the Pickware apps and the items are grouped
     * by order ID. Finally the grouped results are returned. If an ID contained in $orderIds does not have any details
     * an empty array is added for it to the results.
     *
     * @param array $orderIds
     * @return array
     */
    public static function getRestApiConformOrderDetailData(array $orderIds)
    {
        // Determine extra order details attributes that are required
        $extraOrderDetailAttributes = [];
        if (property_exists(OrderDetailAttribute::class, 'swagCustomizing')) {
            $extraOrderDetailAttributes['swagCustomizing'] = 'swag_customizing';
        }
        $extraOrderDetailAttributesString = '';
        if (count($extraOrderDetailAttributes) > 0) {
            $extraOrderDetailAttributesString = ', ' . implode(', ', array_map(function ($attribute) {
                return 'oda.' . $attribute;
            }, $extraOrderDetailAttributes));
        }

        if (property_exists(OrderDetailAttribute::class, 'viisonSetarticleOrderid')) {
            $extraOrderDetailAttributes['pickwareIsSetArticlesSubArticle'] = 'pickwareIsSetArticlesSubArticle';
            $extraOrderDetailAttributesString .= ', IF (oda.viison_setarticle_orderid IS NOT NULL AND oda.viison_setarticle_orderid != od.id, 1, 0) as pickwareIsSetArticlesSubArticle';
        }

        // Get all article information for each item in all orders
        $rawOrderDetailData = Shopware()->Container()->get('dbal_connection')->fetchAll(
            'SELECT
                # Order detail
                od.id AS id,
                od.orderID AS orderId,
                od.status AS statusId,
                od.quantity,
                od.shipped,
                od.price,
                od.name AS orderDetailName,
                od.articleordernumber AS orderDetailArticleNumber,
                od.articleID as orderDetailArticleId,
                # Article detail
                ad.id AS articleDetailId,
                ad.articleID AS articleId,
                ad.ordernumber AS number,
                ad.suppliernumber AS supplierNumber,
                ad.ean AS ean,
                IFNULL(ad.weight, 0) AS weight,
                ad.packunit AS packUnit,
                ad.purchaseunit AS purchaseUnit,
                ad.active,
                ad.purchaseprice as purchasePrice,
                u.unit AS unitName,
                # Article
                a.name AS articleName,
                a.priceGroupID as priceGroupId,
                a.description_long as descriptionLong,
                od.tax_rate AS taxRate,
                od.taxID AS taxId,
                od.modus AS mode,
                IFNULL(derived__returned_quantities.returnedQuantity, 0) AS pickwareReturnedQuantity,
                IFNULL(derived__returned_quantities.cancelledReturnedQuantity, 0) AS pickwareCancelledReturnedQuantity,
                sup.name AS supplierName' .
                $extraOrderDetailAttributesString . '
            FROM s_order_details od
            LEFT OUTER JOIN s_articles_details ad
                ON od.articleordernumber = ad.ordernumber
            LEFT JOIN s_articles a
                ON ad.articleID = a.id
            LEFT JOIN s_core_units u
                ON ad.unitID = u.id
            LEFT JOIN s_order_details_attributes oda
                ON od.id = oda.detailID
            LEFT JOIN s_articles_supplier sup
                ON sup.id = a.supplierID
            LEFT JOIN (
                SELECT
                    orderDetailId,
                    SUM(returnedQuantity) AS returnedQuantity,
                    SUM(cancelledQuantity) AS cancelledReturnedQuantity
                FROM pickware_erp_return_shipment_items
                GROUP BY orderDetailId
            ) AS derived__returned_quantities
                ON derived__returned_quantities.orderDetailId = od.id
            WHERE od.orderID IN (:orderIds)',
            [
                'orderIds' => $orderIds,
            ],
            [
                'orderIds' => \Doctrine\DBAL\Connection::PARAM_INT_ARRAY,
            ]
        );

        // Collect all order detail and article detail IDs
        $articleDetailIds = [];
        $orderDetailIds = [];
        foreach ($rawOrderDetailData as $row) {
            $articleDetailIds[] = intval($row['articleDetailId']);
            $orderDetailIds[] = intval($row['id']);
        }
        $articleDetailIds = array_unique(array_filter($articleDetailIds));

        // Fetch bin location mappings, additional variant texts, mapped property types and stock entry items of all
        // order details and associated article details
        $binLocationMappings = self::getVariantBinLocationMappingArrays($articleDetailIds);
        $additionalTexts = ViisonCommonUtil::getVariantAdditionalTexts($articleDetailIds);
        $entityManager = Shopware()->Container()->get('models');
        $mappedPropertyTypes = $entityManager->getRepository(ArticleDetailItemProperty::class)->getAssignedItemPropertiesAsArrays(
            $articleDetailIds
        );
        $stockEntryItems = $entityManager->getRepository(StockItem::class)->getSaleStockItemsAsArrays(
            $orderDetailIds
        );
        $images = self::getRestApiConformingVariantImages(
            Shopware()->Container()->get('viison_common.image_service')->getVariantImages($articleDetailIds)
        );
        $prices = self::getArticleDetailPricesAsArrays($articleDetailIds);

        // Prepare the results array containing an empty array for each order ID
        $groupedResult = array_fill_keys(
            array_map(
                function ($orderId) {
                    return $orderId . '';
                },
                $orderIds
            ),
            []
        );

        // Format the results
        foreach ($rawOrderDetailData as $row) {
            // Create order detail structure
            $articleDetailId = intval($row['articleDetailId']);
            $orderDetailId = intval($row['id']);

            $tax = [
                'tax' => floatval($row['taxRate']),
            ];
            $articleDetail = null;
            if ($articleDetailId) {
                $articleDetail = [
                    // prices
                    'id' => $articleDetailId,
                    'articleId' => intval($row['articleId']),
                    'article' => [
                        'name' => $row['articleName'],
                        'supplier' => [
                            'name' => $row['supplierName'],
                        ],
                        'tax' => $tax,
                        'taxId' => intval($row['taxId']),
                        'descriptionLong' => $row['descriptionLong'],
                        'priceGroupId' => $row['priceGroupId'],
                    ],
                    'number' => $row['number'],
                    'supplierNumber' => $row['supplierNumber'],
                    'ean' => $row['ean'],
                    'weight' => floatval($row['weight']),
                    'packUnit' => $row['packUnit'],
                    'purchaseUnit' => $row['purchaseUnit'],
                    'unitName' => $row['unitName'],
                    'pickwareImages' => $images[$articleDetailId],
                    'binLocationMappings' => ($binLocationMappings[$articleDetailId]) ?: [],
                    'additionalText' => ($additionalTexts[$articleDetailId]) ?: '',
                    'pickwareItemProperties' => ($mappedPropertyTypes[$articleDetailId]) ?: [],
                    'active' => $row['active'],
                    'purchasePrice' => $row['purchasePrice'],
                    'prices' => $prices[$articleDetailId],
                ];
            } else {
                // Pseudo position (Discount, Surcharge, Voucher)
                // Return the name and article number from the order detail so POS Click & Collect can show these
                // positions and print them on the receipt
                $articleDetail = [
                    'number' => $row['orderDetailArticleNumber'],
                    'article' => [
                        'name' => $row['orderDetailName'],
                        'tax' => $tax,
                    ],
                ];
            }

            $orderDetailData = [
                'id' => $orderDetailId,
                // The order details article id is important for individual vouchers to determine which code
                // has been used for the order.
                'articleId' => intval($row['orderDetailArticleId']),
                'orderId' => intval($row['orderId']),
                'statusId' => intval($row['statusId']),
                'articleName' => $row['orderDetailName'],
                'quantity' => intval($row['quantity']),
                'shipped' => intval($row['shipped']),
                'pickwareReturnedQuantity' => intval($row['pickwareReturnedQuantity']),
                'pickwareCancelledReturnedQuantity' => intval($row['pickwareCancelledReturnedQuantity']),
                'price' => floatval($row['price']),
                'articleDetail' => $articleDetail,
                'pickwareStockItems' => ($stockEntryItems[$orderDetailId]) ?: [],
                'attribute' => [],
                'taxRate' => $row['taxRate'],
                'articleNumber' => $row['number'],
                // In order to determine the order item type (article, coupon, discount, ...)
                'mode' => intval($row['mode']),
            ];
            foreach ($extraOrderDetailAttributes as $dataKey => $columnName) {
                $orderDetailData['attribute'][$dataKey] = $row[$columnName];
                if ($dataKey === 'pickwareIsSetArticlesSubArticle') {
                    $orderDetailData['attribute']['pickwareIsSetArticlesSubArticle'] = boolval($row[$columnName]);
                }
            }

            // Save the detail data in the grouped result
            $groupedResult[$row['orderId']][] = $orderDetailData;
        }

        return $groupedResult;
    }

    /**
     * Maps an array of [articleDetailId] => [images] to the correct Rest Api format of
     * [articleDetailId] => [
     *                          'large' => image,
     *                          'thumbnail' => image,
     *                      ]
     *
     * @param Media[] $variantImages
     * @param int $minWidth (optional, default 250)
     * @param int $minHeight (optional, default 250)
     * @param int $minThumbnailWidth (optional, default 90)
     * @param int $minThumbnailHeight (optional, default 90)
     * @return array[]
     */
    public static function getRestApiConformingVariantImages(
        array $variantImages = [],
        $minWidth = 250,
        $minHeight = 250,
        $minThumbnailWidth = 90,
        $minThumbnailHeight = 90
    ) {
        $mediaService = Shopware()->Container()->get('viison_common.image_service');

        return array_map(function ($variantImages) use (
            $mediaService,
            $minWidth,
            $minHeight,
            $minThumbnailWidth,
            $minThumbnailHeight
        ) {
            $result = [];
            foreach ($variantImages as $image) {
                $images = [];
                $largeImage = $mediaService->findImageUrlForSize($image, $minWidth, $minHeight);
                if ($largeImage) {
                    $images['large'] = $largeImage;
                }
                $thumbnailImage = $mediaService->findImageUrlForSize($image, $minThumbnailWidth, $minThumbnailHeight);
                if ($thumbnailImage) {
                    $images['thumbnail'] = $thumbnailImage;
                }
                if (count($images) > 0) {
                    $result[] = $images;
                }
            }

            return $result;
        }, $variantImages);
    }

    /**
     * Fetches all bin location mappings incl. bin location and stock reservations of the article details with the given
     * $articleDetailIds and returns the result as arrays, groupd by article detail ID. That is, the passed
     * $articleDetailIds are the keys and the arrays of bin location data are the values.
     *
     * @param array $articleDetailIds
     * @return array
     */
    public static function getVariantBinLocationMappingArrays(array $articleDetailIds)
    {
        if (count($articleDetailIds) === 0) {
            return [];
        }

        // Fetch all bin location mappings of the article details incl. bin location and reserved stocks
        $builder = Shopware()->Container()->get('models')->createQueryBuilder();
        $builder
            ->select(
                'binLocationMapping',
                'binLocation',
                'stockReservations'
            )
            ->from(ArticleDetailBinLocationMapping::class, 'binLocationMapping')
            ->join('binLocationMapping.binLocation', 'binLocation')
            ->leftJoin('binLocationMapping.stockReservations', 'stockReservations')
            ->where('binLocationMapping.articleDetailId IN (:articleDetailIds)')
            ->setParameter('articleDetailIds', $articleDetailIds);

        return self::groupArrayByKey($builder->getQuery()->getArrayResult(), 'articleDetailId');
    }

    /**
     * Returns an array containing e.g. name, address and currency, customerGroup and discounts of each available subshop.
     *
     * @return array
     */
    public static function getSubShopData()
    {
        // Get all shops including complete customerGroup and discounts
        /** @var QueryBuilder $builder */
        $builder = Shopware()->Container()->get('models')->createQueryBuilder()
            ->select(
                'shop',
                'customerGroup',
                'customerGroupDiscounts',
                'currency'
            )
            ->from(Shop::class, 'shop')
            ->leftJoin('shop.customerGroup', 'customerGroup')
            ->leftJoin('shop.currency', 'currency')
            ->leftJoin('customerGroup.discounts', 'customerGroupDiscounts');
        $result = $builder->getQuery()->getArrayResult();

        // The query returns far more information than we want to transmit
        $resultWhitelist = [
            'id',
            'name',
            'address',
            'displayName',
            'address',
            'customerGroup',
            'currencyCode',
        ];

        $shops = array_map(function ($shopArr) use ($resultWhitelist) {
            // Get the shop's config
            $shop = Shopware()->Container()->get('models')->find(Shop::class, $shopArr['id']);
            $config = ViisonCommonUtil::getShopConfig($shop);

            // Add some more information to the shop array
            $shopArr['displayName'] = $config->get('shopName');
            $shopArr['address'] = $config->get('address');
            $shopArr['currencyCode'] = $shopArr['currency']['currency'];

            // Apply whitelist by copying all whitelisted attributes
            $shopResult = [];
            foreach ($resultWhitelist as $key) {
                if (isset($shopArr[$key])) {
                    $shopResult[$key] = $shopArr[$key];
                }
            }

            return $shopResult;
        }, $result);

        return $shops;
    }

    /**
     * Sorts the given article array using the field (key path), which is set in the plugin
     * configuration. If two items are equal, the article number is used as a fallback.
     *
     * @param &array $articles
     * @param string $keyPath
     * @param boolean $numeric (optional)
     */
    public static function sortArticlesByFieldAtKeyPath(&$articles, $keyPath, $numeric = false)
    {
        usort($articles, function ($a, $b) use ($keyPath, $numeric) {
            // Compare the fields defined by the index
            $valA = static::arrayGet($a, $keyPath);
            $valB = static::arrayGet($b, $keyPath);
            $comparison = ViisonCommonUtil::natCaseCompare($valA, $valB, $numeric);
            if ($comparison !== 0 || $keyPath === 'number') {
                return $comparison;
            }

            // Compare the article number as a fallback
            return ViisonCommonUtil::natCaseCompare($a['number'], $b['number'], $numeric);
        });
    }

    /**
     * Uses the given keypath to get an element from a nested array. That is, the key path
     * is split using the given delimiter (which defaults to '.') and its components
     * are used to consecutively access the nested arrays. The element found for the last
     * component of the path is returned.
     *
     * @param array $array
     * @param string $keyPath
     * @param string delimiter (optional)
     * @return string|null
     */
    public static function arrayGet($array, $keyPath, $delimiter = '.')
    {
        if (empty($keyPath)) {
            return null;
        }

        // Split the given key path
        $pathComponents = explode($delimiter, $keyPath);
        $current = &$array;
        foreach ($pathComponents as $component) {
            if (!isset($current[$component])) {
                // Invalid component
                return null;
            }

            // Use the current component for the next iteration
            $current = $current[$component];
        }

        return $current;
    }

    public static function getArticleDetailPricesAsArrays(array $articleDetailIds)
    {
        if (count($articleDetailIds) === 0) {
            return [];
        }

        $builder = Shopware()->Container()->get('models')->createQueryBuilder();
        $builder
            ->select(
                'prices',
                'customerGroup'
            )
            ->from(Price::class, 'prices')
            ->leftJoin('prices.customerGroup', 'customerGroup')
            ->where('prices.articleDetailsId in (:articleDetailIds)')
            ->setParameter('articleDetailIds', $articleDetailIds);

        return self::groupArrayByKey($builder->getQuery()->getArrayResult(), 'articleDetailsId');
    }

    /**
     * @param array $source
     * @param string $key
     * @return array
     */
    protected static function groupArrayByKey(array $source, $key)
    {
        $groupedResult = [];
        foreach ($source as $row) {
            $groupKey = $row[$key];
            if (!isset($groupedResult[$groupKey])) {
                $groupedResult[$groupKey] = [];
            }
            $groupedResult[$groupKey][] = $row;
        }

        return $groupedResult;
    }
}
