<?php

namespace KupShop\POSBundle\Util;

use KupShop\ContentBundle\View\Exception\ValidationException;
use KupShop\GraphQLBundle\ApiPos\Types\Warehouse\PosWarehouseCollection;
use KupShop\GraphQLBundle\ApiPos\Types\Warehouse\PosWarehouseItem;
use KupShop\KupShopBundle\Context\PosContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\LoggingContext;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\WarehouseBundle\Entity\StoreItem;
use KupShop\WarehouseBundle\Util\StoreItemWorker;
use Query\Operator;
use Query\QueryBuilder;

class PosWarehouseUtil
{
    public function __construct(
        protected LoggingContext $loggingContext,
        protected ?StoreItemWorker $storeItemWorker = null,
    ) {
    }

    public function getWarehouseItem(PosEntity $pos, \Product|\Variation $product, ?int $forceVariationId = null): ?PosWarehouseCollection
    {
        if (!findModule(\Modules::WAREHOUSE)) {
            return null;
        }

        $posWarehouseCollection = [];
        $storePositions = $this->storeItemWorker->getProductPositions(function (QueryBuilder $qb) use ($product, $pos, $forceVariationId) {
            $qb->addSelect('wpos.id AS id_position');
            $qb->andWhere(Operator::inStringArray($pos->getWarehouseLocations(), 'wl.id'));

            if ($forceVariationId) {
                $qb->andWhere(Operator::equals(['wp.id_product' => $product->id, 'wp.id_variation' => $forceVariationId]));
            } elseif ($product instanceof \Variation) {
                $qb->andWhere(Operator::equals(['wp.id_product' => $product->id, 'wp.id_variation' => $product->variationId]));
            } else {
                $qb->andWhere(Operator::equals(['wp.id_product' => $product->id]));
            }
        });

        foreach ($storePositions as $position) {
            $posWarehouseCollection[] = new PosWarehouseItem($position['code'], $position['pieces'], null, $position['id_position']);
        }

        return new PosWarehouseCollection($posWarehouseCollection);
    }

    public function checkPurchaseStateItemsStockIn(PurchaseState $purchaseState, PosEntity $pos, ?PurchaseState $previousPurchaseState = null): array
    {
        $productsInPositions = [];
        foreach ($this->getPurchaseItemsFromPurchaseState($purchaseState) as $purchaseItem) {
            // Textové položky se nekontrolují
            if (!$purchaseItem->getIdProduct()) {
                continue;
            }

            // Pokud mám produkt již v objednávce, tak kontroluju pouze počet přidaných kusů
            // Od kusů z purchase statu odečítám kusy z vytvořené objednávky (x > 0 = kontroluju rozdíl, x < 0 - odebírám a chci aby kontrola prošla)
            $checkPiecesCount = $purchaseItem->getPieces();
            if ($previousPurchaseState && key_exists($purchaseItem->getId(), $previousPurchaseState->getProducts())) {
                $checkPiecesCount = $checkPiecesCount - ($previousPurchaseState->getProducts()[$purchaseItem->getId()]->getPieces() ?? 0);
            }

            // Pokud jde o dárek, tak se stejně tak musi kontrolovat rozdílná skladovost při editaci
            if ($previousPurchaseState && key_exists($purchaseItem->getId(), $previousPurchaseState->getDiscounts())) {
                $checkPiecesCount = $checkPiecesCount - ($previousPurchaseState->getDiscounts()[$purchaseItem->getId()]->getPieces() ?? 0);
            }

            // Pokud kontroluju na produktu 0 kusů, tak vůbec neprovádím kontroly -> přeskakuji
            if ($checkPiecesCount == 0) {
                continue;
            }

            // Kontrola jestli jsou kusy dostupné na skladě/skladech -
            // in_store na produktu kontroluju jen v pripade ze neni modul stores - protoze pokladna muze byt na externim sklade a pak by to tu nikdy neproslo
            if (findModule(\Modules::STORES)) {
                if ($checkPiecesCount > $this->loadOverallInStoreForPos($purchaseItem->getProduct(), $pos)) {
                    throw new ValidationException("Produkt {$purchaseItem->getName()} není v požadovaném množství na skladu.");
                }
            } else {
                // Kontrola jestli je celkem k dispozici více kusů než je v obj.
                if ($checkPiecesCount > $purchaseItem->getProduct()->inStore) {
                    throw new ValidationException("Produkt {$purchaseItem->getName()} není dostupný v požadovaném množství.");
                }
            }

            // Kontrola jestli jsou kusy na přiřazených pozicích
            if (findModule(\Modules::WAREHOUSE)) {
                $piecesSum = 0;

                $productsInPositions[$purchaseItem->getId()] = $this->searchProductInLocations($purchaseItem->id_product, $purchaseItem->id_variation, $pos);
                $piecesSum += array_sum(array_column($productsInPositions[$purchaseItem->getId()], 'pieces'));

                if ($checkPiecesCount > $piecesSum) {
                    throw new ValidationException("Produkt {$purchaseItem->getName()} není v požadovaném množství na pozicích.");
                }
            }
        }

        return $productsInPositions;
    }

    private function getPurchaseItemsFromPurchaseState(PurchaseState $purchaseState): array
    {
        return QueryHint::withRouteToMaster(function () use ($purchaseState) {
            $purchaseState->getProductList()
                ->fetchVariations(true, false)
                ->getProducts()
                ->fetchStoresInStore(findModule(\Modules::PRODUCTS_VARIATIONS));

            return $purchaseState->getProducts();
        });
    }

    public function loadOverallInStoreForPos(\Product|\Variation $product, PosEntity $pos, ?int $forceVariationId = null): float|int
    {
        if (!findModule(\Modules::STORES)) {
            return $product->variations[$forceVariationId]['in_store'] ?? $product->inStore ?? 0;
        }

        if ($forceVariationId) {
            $storesInStores = $product->storesInStoreByVariations[$forceVariationId] ?? [];
        } elseif ($product instanceof \Variation) {
            $storesInStores = $product->storesInStoreByVariations[$product->variationId] ?? [];
        } else {
            $storesInStores = $product->storesInStore ?? [];
        }

        $searchStores = $pos->getStores();
        $posStores = array_filter($storesInStores, function ($key) use ($searchStores) {
            return in_array($key, $searchStores);
        }, ARRAY_FILTER_USE_KEY);

        return array_sum(array_column($posStores, 'in_store'));
    }

    public function updateBoxToMatchOrder(\Order $order, array $warehouseDiffs, int $idBox): void
    {
        $entity = Contexts::get(PosContext::class)->getActive();
        $this->loggingContext->setIdOrders($order->id);

        // Prochází se všechny rozdíly mezi boxem a objednávkou
        foreach ($warehouseDiffs as $item) {
            // Tahle podmínka by teoreticky neměla nastat, protže by to
            if ($item['pieces'] === $item['pieces_box']) {
                throw new ValidationException('V porovnání se objevil produkt, který nemá rozdíl položek');
            }

            // V objednávce mám méně položek než je v boxu, a proto se zásoba přehodí na stůl pro vrácení
            if ($item['pieces'] < $item['pieces_box']) {
                $idReturnTable = $entity->getIdReturnTable();
                $toRemove = $item['pieces_box'] - $item['pieces'];

                // Pokud mám šarže, tak vyhazuju ty s nejedlším datem expirace, aby se prodaly ty s nejbližším
                if (findModule(\Modules::PRODUCTS_BATCHES)) {
                    // Seřadím šarže podle date expirace
                    usort($item['batches_in_box'], fn ($a, $b) => $b['date_expiry'] <=> $a['date_expiry']);
                    foreach ($item['batches_in_box'] as $batch) {
                        if ($toRemove === 0) {
                            continue;
                        }

                        // Když je na pozici méně kusů než potřebuju přesunout, tak přesunu max možný počet a pokračuju na další dostupnou pozici
                        $item['id_product_batch'] = $batch['id_product_batch'];
                        $this->storeItemWorker->moveBetweenPositions(
                            storeItem: new StoreItem($item),
                            pieces: ($batch['pieces'] < $toRemove) ? $batch['pieces'] : $toRemove,
                            old_position: $idBox,
                            new_position: $idReturnTable
                        );
                        $toRemove -= ($batch['pieces'] < $toRemove) ? $batch['pieces'] : $toRemove;
                    }
                } else {
                    // Nemám šarže, takže přesouvám vše co můžu
                    $this->storeItemWorker->moveBetweenPositions(
                        storeItem: new StoreItem($item),
                        pieces: $toRemove,
                        old_position: $idBox,
                        new_position: $idReturnTable
                    );
                }
            } elseif ($item['pieces'] > $item['pieces_box']) {
                // V objednávce mám více položek než je v boxu, a proto přenesu z pozic do boxu
                // Všechny dostupné pozice pro produkt/variantu
                $availablePositions = $this->searchProductInLocations($item['id_product'], $item['id_variation'], $entity);
                $toMove = $item['pieces'] - $item['pieces_box'];
                $movedPieces = 0;
                foreach ($availablePositions as $position) {
                    // Už je vše přesunuto -> přeskakuju další dostupné pozice
                    if ($movedPieces === $toMove) {
                        continue;
                    }

                    // Pokud mám šarže, tak ji přiřadím
                    if (findModule(\Modules::PRODUCTS_BATCHES)) {
                        $item['id_product_batch'] = $position['id_product_batch'];
                    }

                    // Když je na pozici méně kusů než potřebuju přesunout, tak přesunu max možný počet a pokračuju na další dostupnou pozici
                    $this->storeItemWorker->moveBetweenPositions(
                        storeItem: new StoreItem($item),
                        pieces: ($position['pieces'] < ($toMove - $movedPieces)) ? $position['pieces'] : ($toMove - $movedPieces),
                        old_position: $position['id_position'],
                        new_position: $idBox
                    );
                    $movedPieces += ($position['pieces'] < ($toMove - $movedPieces)) ? $position['pieces'] : ($toMove - $movedPieces);
                }

                if ($toMove != $movedPieces) {
                    throw new ValidationException("Pokladna nebyla schopna zpracovat: {$item['message']}");
                }
            }
        }
    }

    public function createWarehouseOrderRecord(\Order $order): void
    {
        $entity = Contexts::get(PosContext::class)->getActive();
        sqlQueryBuilder()
            ->insert('warehouse_orders')
            ->values([
                'id_order' => $order->id,
                'id_position' => $entity->getVirtualBox(),
                'date_start' => 'NOW()',
                'date_finish' => 'NOW()',
            ])
            ->execute();
    }

    public function searchProductInLocations($id_product, $id_variation, PosEntity $pos)
    {
        $qb = sqlQueryBuilder()
            ->select('wp.*')
            ->from('warehouse_products', 'wp')
            ->join('wp', 'warehouse_positions', 'wpos', 'wpos.id=wp.id_position')
            ->where(Operator::equalsNullable(
                [
                    'wp.id_product' => $id_product,
                    'wp.id_variation' => $id_variation,
                ]))
            ->andWhere('pieces != 0');

        if (findModule(\Modules::PRODUCTS_BATCHES)) {
            $qb->leftJoin('wp', 'products_batches', 'pb', 'wp.id_product_batch = pb.id')
                ->addOrderBy('pb.date_expiry');
        }

        $qb->andWhere('wpos.id IN (:positions)')
        ->setParameter('positions', $pos->getWarehousePositions(), \Doctrine\DBAL\Connection::PARAM_INT_ARRAY)
        ->addOrderBy('FIELD(wpos.id, :positions)');

        return $qb->execute()->fetchAllAssociative();
    }
}
