<?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.

use Doctrine\ORM\Query\Expr\Join;
use Shopware\Components\Api\Exception as ApiException;
use Shopware\Components\Api\Manager as ResourceManager;
use Shopware\Components\Model\QueryBuilder;
use Shopware\CustomModels\ViisonPickwareERP\ItemProperty\ArticleDetailItemProperty;
use Shopware\CustomModels\ViisonPickwareERP\StockLedger\OrderStockReservation;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\ArticleDetailBinLocationMapping;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\BinLocation;
use Shopware\Models\Article\Detail as ArticleDetail;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonPickwareCommon\Classes\QueryBuilderHelper;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Util as PickwareUtil;

class Shopware_Controllers_Api_ViisonPickwareCommonVariants extends Shopware_Controllers_Api_Rest
{
    /**
     * The name of the event, which is fired before applying the sort and limit on the index results.
     */
    const EVENT_FILTER_SORTED_LIMIT_QUERY = 'Shopware_Plugins_ViisonPickwareCommon_Controllers_Api_ViisonPickwareCommonVariants_FilterSortedLimitQuery';

    /**
     * Performs some additional rerouting, if necessary.
     */
    public function init()
    {
        if ($this->Request()->getActionName() === 'putBinLocationMappings' && $this->Request()->getParam('binLocationMappings') !== null && $this->Request()->getParam('stockReservations') !== null) {
            // Manually change the action name to trigger the PUT stock reservations action on a bin location mapping
            $this->Request()->setActionName('putBinLocationMappingsStockReservations');
        } elseif ($this->Request()->getActionName() === 'deleteBinLocationMappings' && $this->Request()->getParam('binLocationMappings') !== null && $this->Request()->getParam('stockReservations') !== null) {
            // Manually change the action name to trigger the DELETE stock reservations action on a bin location mapping
            $this->Request()->setActionName('deleteBinLocationMappingsStockReservations');
        }
    }

    /**
     * Uses all given parameters like limit, start, sort and filter to
     * build a query on article variants an their articles and adds the
     * result to the response.
     *
     * GET /api/variants
     */
    public function indexAction()
    {
        // Check the privileges
        ResourceManager::getResource('variant')->checkPrivilege('read');

        // Get the request parameters
        $limit = $this->Request()->getParam('limit', 1000);
        $offset = $this->Request()->getParam('start', 0);
        $sort = $this->Request()->getParam('sort', []);
        $filter = $this->Request()->getParam('filter', []);

        // Add a 'variant.' prefix to all filter properties, which don't have any prefixes,
        // because the query contains named joins. It is safe to use a 'variant.' prefix,
        // because this is the variants resource and no prefix basically means, that the
        // filter property is part of the variant itself.
        $filter = array_map(function ($filterElement) {
            if (isset($filterElement['property']) && mb_strpos($filterElement['property'], '.') === false) {
                $filterElement['property'] = 'variant.' . $filterElement['property'];
            }

            return $filterElement;
        }, $filter);

        // Find our pickwareBinLocationFilter if present and remove it from the filter array
        $filteredFilter = [];
        $binLocationFilter = null;
        foreach ($filter as $element) {
            if (isset($element['isPickwareBinLocationFilter'])) {
                $binLocationFilter = $element;
            } else {
                $filteredFilter[] = $element;
            }
        }
        $filter = $filteredFilter;

        // Prepare the sort order
        if (empty($sort)) {
            // Sort the items by article name and use the article number as a fallback
            $sort = 'article.name, LENGTH(variant.number), variant.number';
        } elseif (is_array($sort)) {
            // Unroll the filer array
            $sort = implode(',', $sort);
        }

        if ($binLocationFilter) {
            // Find all matching bin locations and add a new condition on their IDs to the main filter
            /** @var QueryBuilder $builder */
            $builder = $this->get('models')->createQueryBuilder();
            $builder
                ->select('binLocation.id')
                ->from(BinLocation::class, 'binLocation');

            $queryBuilderHelper = new QueryBuilderHelper($builder);
            $queryBuilderHelper->addNestedFilter([$binLocationFilter]);

            $result = $builder->getQuery()->getArrayResult();
            $binLocationIds = array_column($result, 'id');

            if (count($binLocationIds) > 0) {
                $filter[0]['children'][] = [
                    'property' => 'wblad.binLocationId',
                    'expression' => 'IN',
                    'value' => $binLocationIds,
                ];
            }
        }

        // Create a query builder for filtering the results
        $builder = $this->get('models')->createQueryBuilder();
        $builder
            ->select('variant.id')
            ->distinct()
            ->from(ArticleDetail::class, 'variant')
            ->leftJoin('variant.article', 'article')
            ->leftJoin('article.supplier', 'supplier')
            ->leftJoin('article.tax', 'tax')
            ->leftJoin('variant.prices', 'prices', Join::WITH, 'variant.id = prices.articleDetailsId')
            ->leftJoin('variant.attribute', 'attribute')
            ->leftJoin('variant.esd', 'esd')
            ->leftJoin(ArticleDetailBinLocationMapping::class, 'wblad', Join::WITH, 'variant.id = wblad.articleDetailId');

        // Add the filter using our own helper to support nested conditions
        $queryBuilderHelper = new QueryBuilderHelper($builder);
        $queryBuilderHelper->addNestedFilter($filter);

        // Fire a filter event to allow plugins to modify the filter query builder
        $eventArgs = new \Enlight_Event_EventArgs([
            'subject' => $this,
            'sort' => $sort,
            'offset' => $offset,
            'limit' => $limit,
        ]);
        $this->get('events')->filter(
            self::EVENT_FILTER_SORTED_LIMIT_QUERY,
            $builder,
            $eventArgs
        );

        // Complete the query builder by adding the sort order, offset and limit
        if ($eventArgs['sort']) {
            $builder->addOrderBy($eventArgs['sort']);
        }
        if (isset($eventArgs['offset']) && isset($eventArgs['limit'])) {
            $builder->setFirstResult($eventArgs['offset'])
                    ->setMaxResults($eventArgs['limit']);
        }
        $result = $builder->getQuery()->getArrayResult();
        $variantIds = array_map(function ($row) {
            return $row['id'];
        }, $result);

        // Stop computation, if no results are left
        $totalResult = count($variantIds);
        if ($totalResult === 0) {
            $this->View()->assign([
                'success' => true,
                'data' => [],
                'total' => $totalResult,
            ]);

            return;
        }

        // Create a new query builder for fetching the complete, filtered,
        // sorted and paginated variants
        $builder = $this->get('models')->createQueryBuilder();
        $builder->select(
            'variant',
            'article',
            'supplier',
            'prices',
            'customerGroup',
            'tax',
            'attribute',
            'esd',
            'unit',
            'configuratorOptions',
            'configuratorOptionGroup'
        )->from(ArticleDetail::class, 'variant')
            ->leftJoin('variant.article', 'article')
            ->leftJoin('article.supplier', 'supplier')
            ->leftJoin('article.tax', 'tax')
            ->leftJoin('variant.prices', 'prices', Join::WITH, 'variant.id = prices.articleDetailsId')
            ->leftJoin('prices.customerGroup', 'customerGroup')
            ->leftJoin('variant.attribute', 'attribute')
            ->leftJoin('variant.esd', 'esd')
            ->leftJoin('variant.unit', 'unit')
            ->leftJoin('variant.configuratorOptions', 'configuratorOptions')
            ->leftJoin('configuratorOptions.group', 'configuratorOptionGroup')
            ->where('variant.id IN (:variantIds)')
            ->setParameter('variantIds', $variantIds);
        $variants = $builder->getQuery()->getArrayResult();

        // Fetch the bin locations and additional texts of all matching variants
        $binLocationMappings = PickwareUtil::getVariantBinLocationMappingArrays($variantIds);
        $additionalTexts = ViisonCommonUtil::getVariantAdditionalTexts($variantIds);
        $mappedPropertyTypes = $this->get('models')->getRepository(ArticleDetailItemProperty::class)->getAssignedItemPropertiesAsArrays(
            $variantIds
        );
        $images = PickwareUtil::getRestApiConformingVariantImages(
            $this->get('viison_common.image_service')->getVariantImages($variantIds)
        );

        // Add additional fields for our apps
        foreach ($variants as &$variant) {
            $variant['pickwareImages'] = $images[$variant['id']];
            $variant['binLocationMappings'] = ($binLocationMappings[$variant['id']]) ?: [];
            $variant['additionalText'] = ($additionalTexts[$variant['id']]) ?: '';
            $variant['pickwareItemProperties'] = ($mappedPropertyTypes[$variant['id']]) ?: [];

            // Unit information
            if ($variant['unit'] !== null) {
                $variant['unitName'] = $variant['unit']['unit'];
                unset($variant['unit']);
            }
        }

        // Sort the items by article name
        PickwareUtil::sortArticlesByFieldAtKeyPath($variants, 'article.name');

        $this->View()->assign([
            'success' => true,
            'data' => $variants,
            'total' => $totalResult,
        ]);
    }

    /**
     * Finds the requested variant and responds with all its bin location mappings of all warehouses, including the
     * respective stock reservations.
     *
     * GET /api/variants/{id}/binLocationMappings
     */
    public function getBinLocationMappingsIndexAction()
    {
        // Check the privileges
        ResourceManager::getResource('variant')->checkPrivilege('read');

        // Try to find the article detail
        $articleDetail = $this->findAndValidateArticleDetail();

        // Fetch all bin location mappings
        $articleDetailId = $articleDetail->getId();
        $result = PickwareUtil::getVariantBinLocationMappingArrays([
            $articleDetailId
        ]);
        $result = $result[$articleDetailId];

        $this->View()->assign([
            'success' => true,
            'data' => $result,
            'total' => count($result),
        ]);
    }

    /**
     * Creates a new bin location mapping for the requested variant and POSTed 'binLocationId'. Currently the only way
     * this resource can be used is to select a bin location as the default bin location of a variant, although that
     * bin location is not mapped to the variant already. That is, only if the POSTed 'defaultMapping' parameter is
     * true, a mapping will be created. Upon successful creation, the updated list of bin location mappings of the
     * requested variant is responded.
     *
     * POST /api/variants/{id}/binLocationMappings
     */
    public function postBinLocationMappingsAction()
    {
        // Check the privileges
        ResourceManager::getResource('variant')->checkPrivilege('update');

        // Try to find the entities
        $articleDetail = $this->findAndValidateArticleDetail();

        // Check the required parameters
        $binLocationId = $this->Request()->getParam('binLocationId', 0);
        $binLocation = $this->get('models')->find(BinLocation::class, $binLocationId);
        if (!$binLocation) {
            throw new ApiException\NotFoundException(sprintf(
                'Bin location with ID %d not found.',
                $binLocationId
            ));
        }

        // Check for an existing mapping to the bin location
        $binLocationMapping = $this->get('models')->getRepository(ArticleDetailBinLocationMapping::class)->findOneBy([
            'articleDetail' => $articleDetail,
            'binLocation' => $binLocation,
        ]);
        if ($binLocationMapping) {
            throw new ApiException\CustomValidationException(sprintf(
                'A mapping from product "%s" to bin location "%s" already exists.',
                $articleDetail->getNumber(),
                $binLocation->getCode()
            ));
        }

        // Currently creating new bin location mappings via the API is only allowed when setting the 'defaultMapping'
        // flag, hence check that it's contained in the request
        if ($this->Request()->getParam('defaultMapping') !== true) {
            throw new ApiException\CustomValidationException(
                'Missing or invalid parameter "defaultMapping". Creating bin location mappings via the REST API can only be used to map a new default bin location.'
            );
        }

        // Update the default bin location
        $this->get('pickware.erp.stock_ledger_service')->selectDefaultBinLocation(
            $articleDetail,
            $binLocation->getWarehouse(),
            $binLocation
        );

        // Prepare the response data
        $binLocationMappings = PickwareUtil::getVariantBinLocationMappingArrays([$articleDetail->getId()]);
        $responseData = [
            'binLocationMappings' => ($binLocationMappings[$articleDetail->getId()]) ?: [],
        ];

        $this->Response()->setHttpResponseCode(201);
        $this->View()->assign([
            'success' => true,
            'data' => $responseData,
        ]);
    }

    /**
     * Updates the requested bin location mapping. Currently the only way this resource can be updated is by selecting
     * or deselecting the bin location mapping as the default bin location mapping of a variant. That is, only if the
     * parameter 'defaultMapping' is contained in the POST data, the bin location mapping will be updated. Upon
      *successful update, the updated list of bin location mappings of the requested variant is responded.
     *
     * PUT /api/variants/{id}/binLocationMappings/{binLocationMappings}
     */
    public function putBinLocationMappingsAction()
    {
        // Check the privileges
        ResourceManager::getResource('variant')->checkPrivilege('update');

        // Try to find the entities
        $articleDetail = $this->findAndValidateArticleDetail();
        $binLocationMapping = $this->findAndValidateBinLocationMapping($articleDetail);

        // Currently updating bin location mappings via the API is only allowed when setting the 'defaultMapping' flag,
        // hence check that it's contained in the request
        $defaultMappingFlag = $this->Request()->getParam('defaultMapping');
        if ($defaultMappingFlag === null) {
            throw new ApiException\CustomValidationException(
                'Missing parameter "defaultMapping". Updating bin location mappings via the REST API can only be used to select a new default bin location mapping.'
            );
        }

        if ($defaultMappingFlag !== $binLocationMapping->isDefaultMapping()) {
            $warehouse = $binLocationMapping->getBinLocation()->getWarehouse();
            $newDefaultBinLocation = ($defaultMappingFlag) ? $binLocationMapping->getBinLocation() : null;
            $this->get('pickware.erp.stock_ledger_service')->selectDefaultBinLocation(
                $articleDetail,
                $warehouse,
                $newDefaultBinLocation
            );
        }

        // Prepare the response data
        $binLocationMappings = PickwareUtil::getVariantBinLocationMappingArrays([$articleDetail->getId()]);
        $responseData = [
            'binLocationMappings' => ($binLocationMappings[$articleDetail->getId()]) ?: [],
        ];

        $this->View()->assign([
            'success' => true,
            'data' => $responseData,
        ]);
    }

    /**
     * @return ArticleDetail
     * @throws ApiException\NotFoundException
     */
    protected function findAndValidateArticleDetail()
    {
        $articleDetailId = $this->Request()->getParam('id', 0);
        $articleDetail = $this->get('models')->find(ArticleDetail::class, $articleDetailId);
        if (!$articleDetail) {
            throw new ApiException\NotFoundException(
                sprintf('Variant with ID %d not found.', $articleDetailId)
            );
        }

        return $articleDetail;
    }

    /**
     * @param ArticleDetail $articleDetail
     * @return ArticleDetailBinLocationMapping
     * @throws ApiException\NotFoundException
     */
    protected function findAndValidateBinLocationMapping(ArticleDetail $articleDetail)
    {
        $binLocationMappingId = intval($this->Request()->getParam('binLocationMappings'));
        $binLocationMapping = $this->get('models')->getRepository(ArticleDetailBinLocationMapping::class)->findOneBy([
            'id' => $binLocationMappingId,
            'articleDetail' => $articleDetail,
        ]);
        if (!$binLocationMapping) {
            throw new ApiException\NotFoundException(
                sprintf('Bin location mapping with ID %d for variant with ID %d not found.', $binLocationMappingId, $articleDetail->getId())
            );
        }

        return $binLocationMapping;
    }
}
