<?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 Doctrine\ORM\Tools\Pagination;
use Shopware\Components\Api\Exception as ApiException;
use Shopware\Components\Api\Resource\Resource;
use Shopware\CustomModels\ViisonPickwareERP\ItemProperty\ArticleDetailItemProperty;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\BinLocation;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\Warehouse;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonPickwareCommon\Classes\QueryBuilderHelper;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Util as PickwareCommonUtil;
use Shopware\Plugins\ViisonPickwareMobile\Components\PluginConfigService;

/**
 * This controller adds a new REST API resource 'warehouses' including its index
 * as well as a subindex and POST endpoint for a warehouse's bin locations.
 */
class Shopware_Controllers_Api_ViisonPickwareMobileWarehouses extends Shopware_Controllers_Api_Rest
{
    /**
     * Uses basic filter parameter from the request (limit start, filter, sort) to build a query on bin locations of the
     * warehouse (given by id request parameter).
     *
     * If more filters from other properties than the basic binLocation are given (i.e. this is a Picking App stocktake
     * request), these filters are considered as well and more information is joined to the result.
     *
     * GET /api/warehouses/{id}/binLocations
     */
    public function getBinLocationsIndexAction()
    {
        // Try to find the warehouse
        $warehouseId = $this->Request()->getParam('id', 0);
        $warehouse = $this->get('models')->find(Warehouse::class, $warehouseId);
        if (!$warehouse) {
            throw new ApiException\NotFoundException(
                sprintf('Warehouse with ID %d not found', $warehouseId)
            );
        }

        // 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', []);

        // As soon as there is a filter on any property but the basic `binLocation`, consider this request as complex,
        // which results in a more complex query to be used.
        $requiresComplexQuery = false;
        foreach ($filter as $filterElement) {
            if (mb_strpos($filterElement['property'], 'binLocation.') !== 0 || isset($filterElement['children'])) {
                $requiresComplexQuery = true;
                break;
            }
        }

        // Build the query
        $builder = $this->get('models')->createQueryBuilder();
        $builder
            ->select('binLocation')
            ->from(BinLocation::class, 'binLocation')
            ->andWhere('binLocation.warehouseId = :warehouseId')
            ->setParameter('warehouseId', $warehouseId)
            ->addOrderBy($sort)
            ->setFirstResult($offset)
            ->setMaxResults($limit);

        if ($requiresComplexQuery) {
            // Add all selects and joins required for the complex query
            $builder
                ->addSelect('articleDetailBinLocationMapping')
                ->addSelect('variant')
                ->addSelect('attribute')
                ->addSelect('esd')
                ->addSelect('prices')
                ->addSelect('customerGroup')
                ->addSelect('unit')
                ->addSelect('article')
                ->addSelect('supplier')
                ->addSelect('tax')
                ->addSelect('CASE WHEN binLocation.id = warehouse.nullBinLocationId THEN 1 ELSE 0 END AS HIDDEN isNullBinLocation')
                ->leftJoin('binLocation.articleDetailBinLocationMappings', 'articleDetailBinLocationMapping')
                ->leftJoin('articleDetailBinLocationMapping.articleDetail', 'variant')
                ->leftJoin('variant.attribute', 'attribute')
                ->leftJoin('variant.esd', 'esd')
                ->leftJoin('variant.prices', 'prices', Join::WITH, 'variant.id = prices.articleDetailsId')
                ->leftJoin('prices.customerGroup', 'customerGroup')
                ->leftJoin('variant.unit', 'unit')
                ->leftJoin('variant.article', 'article')
                ->leftJoin('article.supplier', 'supplier')
                ->leftJoin('article.tax', 'tax')
                ->leftJoin('binLocation.warehouse', 'warehouse');
        }

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

        // Create the query and execute it to get the results
        $query = $builder->getQuery();
        $query->setHydrationMode(Resource::HYDRATE_ARRAY);
        $paginator = new Pagination\Paginator($query);
        $totalResult = $paginator->count();
        $binLocations = $paginator->getIterator()->getArrayCopy();

        if ($requiresComplexQuery) {
            // Fetch additional information for all article details contained in the result
            $articleDetailIds = [];
            foreach ($binLocations as $binLocation) {
                foreach ($binLocation['articleDetailBinLocationMappings'] as $binLocationMapping) {
                    $articleDetailIds[] = $binLocationMapping['articleDetailId'];
                }
            }
            $additionalTexts = ViisonCommonUtil::getVariantAdditionalTexts($articleDetailIds);

            $imageService = $this->get('viison_common.image_service');
            $imagesOfAllVariants = $imageService->getVariantImages($articleDetailIds);
            $images = PickwareCommonUtil::getRestApiConformingVariantImages($imagesOfAllVariants);

            $mappedPropertyTypes = $this->get('models')->getRepository(ArticleDetailItemProperty::class)->getAssignedItemPropertiesAsArrays(
                $articleDetailIds
            );

            // Add the additional information
            foreach ($binLocations as &$binLocation) {
                foreach ($binLocation['articleDetailBinLocationMappings'] as &$binLocationMapping) {
                    $binLocationMapping['articleDetail']['pickwareImages'] = $images[$binLocationMapping['articleDetailId']];
                    $binLocationMapping['articleDetail']['additionalText'] = ($additionalTexts[$binLocationMapping['articleDetailId']]) ?: '';
                    $binLocationMapping['articleDetail']['pickwareItemProperties'] = ($mappedPropertyTypes[$binLocationMapping['articleDetailId']]) ?: [];

                    // Unit information
                    if ($binLocationMapping['articleDetail']['unit'] !== null) {
                        $binLocationMapping['articleDetail']['unitName'] = $binLocationMapping['articleDetail']['unit']['name'];
                        unset($binLocationMapping['articleDetail']['unit']);
                    }
                }
                unset($binLocationMapping);
            }
            unset($binLocation);
        }

        // To reduce the payload of the result, we remove all non-pickware, non-viison attributes from the article
        // details, since other attributes are not used in the app.
        foreach ($binLocations as &$binLocation) {
            if (!is_array($binLocation['articleDetailBinLocationMappings'])) {
                // Only manipulate the article detail of the article detail bin location mappings if any are set.
                continue;
            }

            foreach ($binLocation['articleDetailBinLocationMappings'] as &$binLocationMapping) {
                $binLocationMapping['articleDetail']['attribute'] = array_filter(
                    $binLocationMapping['articleDetail']['attribute'],
                    function ($attributeKey) {
                        return (
                            mb_strpos($attributeKey, 'pickware') === 0
                            || mb_strpos($attributeKey, 'viison') === 0
                        );
                    },
                    ARRAY_FILTER_USE_KEY
                );
            }
            unset($binLocationMapping);
        }
        unset($binLocation);

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

    /**
     * Checks whether a bin location with the POSTed 'code' already exists in the
     * warehosue having the given ID and, if not, creates a new binLocation for that code.
     *
     * @deprecated Use Shopware_Controllers_Api_ViisonPickwareERPBinLocations instead
     *
     * POST /api/warehouses/{id}/binLocations
     */
    public function postBinLocationsAction()
    {
        /** @var \Shopware\Components\Model\ModelManager $entityManager */
        $entityManager = $this->get('models');

        // Try to find the warehouse
        $warehouseId = $this->Request()->getParam('id', 0);
        $warehouse = $entityManager->find(Warehouse::class, $warehouseId);
        if (!$warehouse) {
            throw new ApiException\NotFoundException(
                sprintf('Warehouse with ID %d not found', $warehouseId)
            );
        }

        // Get and validate the bin location data
        $binLocationData = $this->Request()->getPost();
        if (!isset($binLocationData['code']) || mb_strlen($binLocationData['code']) === 0) {
            throw new ApiException\ParameterMissingException('code');
        } elseif ($binLocationData['code'] === BinLocation::NULL_BIN_LOCATION_CODE) {
            throw new ApiException\CustomValidationException(
                sprintf('The bin location code "%s" is invalid, because it is a reserved value', $binLocationData['code'])
            );
        }

        // Make sure the bin location does not already exist in this warehouse
        $binLocation = $this->get('models')->getRepository(BinLocation::class)->findOneBy([
            'code' => $binLocationData['code'],
            'warehouse' => $warehouse,
        ]);
        if ($binLocation) {
            throw new ApiException\CustomValidationException(
                sprintf('The bin location with code "%s" already exists in warehouse with ID %d', $binLocationData['code'], $warehouseId)
            );
        }

        // Create a new bin location in the warehouse using the given code
        $binLocation = new BinLocation($warehouse, $binLocationData['code']);

        // Save changes
        $entityManager->persist($binLocation);
        $entityManager->flush($binLocation);

        $this->Response()->setHttpResponseCode(201);
        $this->View()->assign([
            'success' => true,
            'data' => [
                'id' => $binLocation->getId(),
                'location' => ($this->apiBaseUrl . 'warehouses/' . $warehouseId . '/binLocations/' . $binLocation->getId()),
            ],
        ]);
    }
}
