<?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\SupplierOrder\SupplierOrder;
use Shopware\CustomModels\ViisonPickwareERP\SupplierOrder\SupplierOrderAttachment;
use Shopware\CustomModels\ViisonPickwareERP\SupplierOrder\SupplierOrderItem;
use Shopware\CustomModels\ViisonPickwareERP\SupplierOrder\SupplierOrderItemStatus;
use Shopware\CustomModels\ViisonPickwareERP\SupplierOrder\SupplierOrderStatus;
use Shopware\CustomModels\ViisonPickwareERP\Warehouse\BinLocation;
use Shopware\Models\Media\Media;
use Shopware\Plugins\ViisonCommon\Classes\Util\Currency as CurrencyUtil;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ViisonCommonUtil;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Util as PickwareUtil;
use Shopware\Plugins\ViisonPickwareERP\Components\BarcodeLabel\Article\ArticleBarcodeLabelType;

/**
 * This controller adds the index as well as a PUT action of a new
 * supplierOrders resource to the REST API.
 */
class Shopware_Controllers_Api_ViisonPickwareMobileSupplierOrders extends Shopware_Controllers_Api_Rest
{
    /**
     * Performs some additional rerouting, if necessary.
     */
    public function init()
    {
        $request = $this->Request();
        if ($request->getActionName() === 'postItems' && $request->getParam('subId') !== null && $request->getParam('purchases') !== null) {
            // Manually change the action name to trigger the POST purchases action on a supplier order item
            $request->setActionName('postItemsPurchases');
        }
    }

    /**
     * Uses all given parameters like limit, start, sort and filter to
     * build a query on supplier orders and adds the result to the response.
     *
     * GET /api/supplierOrders
     */
    public function indexAction()
    {
        // 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', []);

        // Get all supplier orders respecting the parameters
        $builder = $this->get('models')->createQueryBuilder();
        $builder->select(
            'supplierOrder',
            'supplier',
            'items',
            'articleDetail',
            'article',
            'prices',
            'customerGroup',
            'attribute',
            'manufacturer',
            'tax',
            'currency',
            'unit'
        )->from(SupplierOrder::class, 'supplierOrder')
            ->leftJoin('supplierOrder.supplier', 'supplier')
            ->leftJoin('supplierOrder.currency', 'currency')
            ->leftJoin('supplierOrder.items', 'items')
            ->leftJoin('items.articleDetail', 'articleDetail')
            ->leftJoin('articleDetail.article', 'article')
            ->leftJoin('articleDetail.prices', 'prices', Join::WITH, 'articleDetail.id = prices.articleDetailsId')
            ->leftJoin('prices.customerGroup', 'customerGroup')
            ->leftJoin('articleDetail.attribute', 'attribute')
            ->leftJoin('article.supplier', 'manufacturer')
            ->leftJoin('article.tax', 'tax')
            ->leftJoin('articleDetail.unit', 'unit')
            ->addOrderBy($sort)
            ->setFirstResult($offset)
            ->setMaxResults($limit);

        // Determine and add filter
        if ($filter === 'pickware') {
            // Use custom pickware filter conditions
            $builder->where('supplierOrder.statusId IN (:statusIds)')
                    ->setParameter('statusIds', $this->validPickwareFilterStatusIds());
        } else {
            // Use default filter
            $builder->addFilter($filter);
        }
        $warehouseId = $this->Request()->getParam('warehouseId');
        if ($warehouseId !== null) {
            $builder->andWhere('supplierOrder.warehouseId = :warehouseId')
                    ->setParameter('warehouseId', $warehouseId);
        }

        // 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();
        $orders = $paginator->getIterator()->getArrayCopy();

        // Fetch the bin locations and variant additional texts of all article details contained in the result
        $articleDetailIds = [];
        foreach ($orders as $order) {
            foreach ($order['items'] as $article) {
                $articleDetailIds[] = $article['articleDetail']['id'];
            }
        }
        $binLocationMappings = PickwareUtil::getVariantBinLocationMappingArrays($articleDetailIds);
        $additionalTexts = ViisonCommonUtil::getVariantAdditionalTexts($articleDetailIds);
        $mappedPropertyTypes = $this->get('models')->getRepository(ArticleDetailItemProperty::class)->getAssignedItemPropertiesAsArrays(
            $articleDetailIds
        );
        $images = PickwareUtil::getRestApiConformingVariantImages(
            $this->get('viison_common.image_service')->getVariantImages($articleDetailIds)
        );

        // Add the article image URL as well as additional text and bin locations to all order items
        foreach ($orders as &$order) {
            foreach ($order['items'] as &$article) {
                $article['articleDetail']['pickwareImages'] = $images[$article['articleDetail']['id']];
                $article['articleDetail']['binLocationMappings'] = ($binLocationMappings[$article['articleDetail']['id']]) ?: [];
                $article['articleDetail']['additionalText'] = ($additionalTexts[$article['articleDetail']['id']]) ?: '';
                $article['articleDetail']['pickwareItemProperties'] = ($mappedPropertyTypes[$article['articleDetail']['id']]) ?: [];

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

        // Add results to response
        $this->View()->assign([
            'success' => true,
            'data' => $orders,
            'total' => $totalResult,
        ]);
    }

    /**
     * Updates the requested supplier order with the posted data.
     *
     * PUT /api/supplierOrders/{id}
     */
    public function putAction()
    {
        // Get the order ID and try to find the order
        $orderId = $this->Request()->getParam('id');
        $order = $this->getOne($orderId);

        // Try to get the order data
        $orderData = $this->Request()->getPost();
        if (empty($orderData)) {
            throw new ApiException\ParameterMissingException('Order data');
        }

        // Update the order
        $changedEntities = [
            $order
        ];
        if (isset($orderData['statusId'])) {
            // Find and set status
            $orderStatus = $this->get('models')->find(SupplierOrderStatus::class, $orderData['statusId']);
            if (!$orderStatus) {
                throw new ApiException\CustomValidationException('Invalid order status');
            }
            $order->setStatus($orderStatus);

            if ($orderStatus->getName() === SupplierOrderStatus::COMPLETELY_RECEIVED) {
                // Set the order's delivery date
                $order->setDeliveryDate(new DateTime());
            }
        }
        if (array_key_exists('comment', $orderData)) {
            $order->setComment($orderData['comment']);
        }
        if (is_array($orderData['attachments'])) {
            foreach ($orderData['attachments'] as $attachmentData) {
                if (empty($attachmentData['mediaId'])) {
                    throw new ApiException\CustomValidationException('Attachment parameter "mediaId" must not be empty.');
                }

                // Check for existing attachment with given media ID
                $existingAttachment = array_filter($order->getAttachments()->toArray(), function ($attachment) use ($attachmentData) {
                    return $attachment->getMedia()->getId() == $attachmentData['mediaId'];
                });
                if (count($existingAttachment) > 0) {
                    continue;
                }

                // Try to find the media item
                $media = $this->get('models')->find(Media::class, $attachmentData['mediaId']);
                if (!$media) {
                    throw new ApiException\NotFoundException(
                        sprintf('Media with ID %d not found.', $attachmentData['mediaId'])
                    );
                }

                // Create new attachment
                $attachment = new SupplierOrderAttachment($order, $media);
                $this->get('models')->persist($attachment);
                $attachment->setFilename($media->getFileName());
                $attachment->setDate(new DateTime());
                $changedEntities[] = $attachment;
            }
        }

        // Save the changes
        $this->get('models')->flush($changedEntities);

        $this->View()->success = true;
    }

    /**
     * Logs a new 'purchase' stock entry for the updated supplier order item using the POSTed 'quantity', 'binLocationId'
     * and 'price'. The item's price, delivered quantity and status are updated as well. Finally the updated
     * item values as well as the updated article detail's bin location mappings are responded.
     *
     * POST /api/supplierOrders/{id}/items/{items}/purchases
     */
    public function postItemsPurchasesAction()
    {
        // Try to find the requested supplier order
        $supplierOrderId = $this->Request()->getParam('id');
        /** @var SupplierOrder $supplierOrder */
        $supplierOrder = $this->get('models')->find(SupplierOrder::class, $supplierOrderId);
        if (!$supplierOrder) {
            throw new ApiException\NotFoundException(
                sprintf('Supplier order with ID %d not found.', $supplierOrderId)
            );
        }

        // Try to find the requested item in the supplier order
        $supplierOrderItemId = intval($this->Request()->getParam('items'));
        $supplierOrderItem = $supplierOrder->getItems()->filter(function (SupplierOrderItem $item) use ($supplierOrderItemId) {
            return $item->getId() === $supplierOrderItemId;
        })->first();
        if (!$supplierOrderItem) {
            throw new ApiException\NotFoundException(sprintf(
                'Supplier order item with ID %d not found in supplier order with ID %d.',
                $supplierOrderItemId,
                $supplierOrderId
            ));
        }

        // Check the provided parameters
        $quantity = intval($this->Request()->getParam('quantity'));
        if ($quantity <= 0) {
            throw new ApiException\CustomValidationException('The parameter "quantity" must be greater zero.');
        }
        $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)
            );
        }
        if (!$binLocation->getWarehouse()->equals($supplierOrder->getWarehouse())) {
            throw new ApiException\CustomValidationException(sprintf(
                'Bin location "%s" with ID %d is not part of the designated warehouse "%s" of supplier order with ID %d.',
                $binLocation->getCode(),
                $binLocation->getId(),
                $supplierOrder->getWarehouse()->getDisplayName(),
                $supplierOrder->getId()
            ));
        }
        $price = $this->Request()->getParam('price', $supplierOrderItem->getPrice());
        $markForBarcodeLabelPrinting = $this->Request()->getParam('markForBarcodeLabelPrinting', false);

        // Convert the purchase price of the order item, so that all stock entries are in the default currency.
        $defaultCurrency = CurrencyUtil::getDefaultCurrency();
        if ($supplierOrder->getCurrency()->getId() !== $defaultCurrency->getId()) {
            $stockEntryPrice = CurrencyUtil::convertAmountBetweenCurrencies(
                $price,
                $supplierOrder->getCurrency(),
                $defaultCurrency
            );
            $namespace = $this->get('snippets')->getNamespace('backend/viison_pickware_erp_supplier_orders/main');
            $message = sprintf(
                $namespace->get('currency/conversion/stock_entry_comment'),
                CurrencyUtil::getFormattedPriceStringByCurrency($price, $supplierOrder->getCurrency()),
                CurrencyUtil::getFormattedPriceStringByCurrency($stockEntryPrice, $defaultCurrency)
            );
            $this->recordPurchasedStockInStockLedger(
                $supplierOrderItem,
                $quantity,
                $stockEntryPrice,
                $binLocation,
                $markForBarcodeLabelPrinting,
                $message
            );
        } else {
            $this->recordPurchasedStockInStockLedger(
                $supplierOrderItem,
                $quantity,
                $price,
                $binLocation,
                $markForBarcodeLabelPrinting
            );
        }
        $this->autoUpdateItemStatus($supplierOrderItem);
        $changedEntities = [$supplierOrderItem];

        if ($price !== $supplierOrderItem->getPrice()) {
            // Save the price in the item and update the order total. This price is not necessarily in the default currency.
            $supplierOrderItem->setPrice($price);
            $supplierOrder->recomputeTotal();
            $changedEntities[] = $supplierOrder;
        }

        $this->get('models')->flush($changedEntities);

        // Prepare the response data
        $articleDetailId = $supplierOrderItem->getArticleDetail()->getId();
        $binLocationMappings = PickwareUtil::getVariantBinLocationMappingArrays([$articleDetailId]);
        $responseData = [
            'statusId' => $supplierOrderItem->getStatus()->getId(),
            'deliveredQuantity' => $supplierOrderItem->getDeliveredQuantity(),
            'articleDetail' => [
                'binLocationMappings' => ($binLocationMappings[$articleDetailId]) ?: [],
                'purchasePrice' => $supplierOrderItem->getArticleDetail()->getPurchasePrice(),
            ],
        ];

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

    /**
     * @return int[]
     */
    private function validPickwareFilterStatusIds()
    {
        $validStates = $this->get('models')->getRepository(SupplierOrderStatus::class)->findBy([
            'name' => [
                SupplierOrderStatus::OPEN,
                SupplierOrderStatus::SENT_TO_SUPPLIER,
                SupplierOrderStatus::DISPATCHED_BY_SUPPLIER,
                SupplierOrderStatus::PARTLY_RECEIVED,
            ],
        ]);

        return array_map(function ($status) {
            return $status->getId();
        }, $validStates);
    }

    /**
     * Tries to find a supplier order using the given id.
     *
     * @param int $id
     * @return SupplierOrder
     * @throws ApiException\NotFoundException If the no supplier order with the given id exists.
     */
    private function getOne($id)
    {
        // Get the supplier order by id
        $supplierOrder = $this->get('models')->find(SupplierOrder::class, $id);
        if (!$supplierOrder) {
            throw new ApiException\NotFoundException(
                sprintf('Supplier order by ID %d not found', $id)
            );
        }

        return $supplierOrder;
    }

    /**
     * @param SupplierOrderItem $orderItem
     * @param int $quantity
     * @param float $price
     * @param BinLocation|null $binLocation (optional)
     * @param string|null $message (optional)
     * @param boolean $printBarcodeLabels (optional)
     */
    private function recordPurchasedStockInStockLedger(
        SupplierOrderItem $orderItem,
        $quantity,
        $price,
        BinLocation $binLocation = null,
        $printBarcodeLabels = false,
        $message = null
    ) {
        // Determine the stock changes
        if ($binLocation) {
            $stockChanges = $this->get('pickware.erp.stock_change_list_factory_service')->createSingleBinLocationStockChangeList(
                $binLocation,
                $quantity
            );
        } else {
            $stockChanges = $this->get('pickware.erp.stock_change_list_factory_service')->createStockChangeList(
                $orderItem->getSupplierOrder()->getWarehouse(),
                $orderItem->getArticleDetail(),
                $quantity
            );
        }

        // Log the stock changes
        $this->get('pickware.erp.stock_ledger_service')->recordPurchasedStock(
            $orderItem->getArticleDetail(),
            $stockChanges,
            $price,
            $message,
            $orderItem
        );
        $orderItem->setDeliveredQuantity($orderItem->getDeliveredQuantity() + $quantity);

        // Mark the changed quantity for barcode label printing if required
        if ($printBarcodeLabels && $orderItem->getArticleDetail()) {
            $articleBarcodeLabelType = $this->get('pickware.erp.barcode_label_service')->getBarcodeLabelTypeByName(
                ArticleBarcodeLabelType::IDENTIFIER
            );
            $articleBarcodeLabelType->enqueueForPrinting($orderItem->getArticleDetail()->getNumber(), $quantity);
        }
    }

    /**
     * @param SupplierOrderItem $item
     */
    protected function autoUpdateItemStatus(SupplierOrderItem $item)
    {
        // Only update the status if its not already 'cancelled', 'refund' or 'missing'
        $manualStatus = [
            SupplierOrderItemStatus::CANCELED,
            SupplierOrderItemStatus::REFUND,
            SupplierOrderItemStatus::MISSING,
        ];
        if (in_array($item->getStatus()->getName(), $manualStatus)) {
            return;
        }

        // Update the item's status based on the updated delivered quantity
        $newStatusName = SupplierOrderItemStatus::OPEN;
        if ($item->getDeliveredQuantity() >= $item->getOrderedQuantity()) {
            $newStatusName = SupplierOrderItemStatus::COMPLETELY_RECEIVED;
        } elseif ($item->getDeliveredQuantity() > 0) {
            $newStatusName = SupplierOrderItemStatus::PARTLY_RECEIVED;
        }
        $newStatus = $this->get('models')->getRepository(SupplierOrderItemStatus::class)->findOneBy([
            'name' => $newStatusName,
        ]);
        $item->setStatus($newStatus);
    }
}
