<?php

/*
 * This file is part of the VitiCore 2.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Product\Repository;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartItem;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductDegressive;
use CpCreation\VitiCore\Product\Model\ProductPriceGroup;
use CpCreation\VitiCore\Repository\BaseRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;

class ProductDegressiveRepository extends BaseRepository
{
    /**
     * @throws \ReflectionException
     */
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, ProductDegressive::class);
    }

    /**
     * @return ProductDegressive[]
     */
    public function findOrder(): array
    {
        return $this
            ->createQueryBuilder('p')
            ->innerJoin('p.types', 't')
            ->innerJoin('t.translations', 'tr')
            ->addOrderBy('tr.name')
            ->addOrderBy('p.qtyAt')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return ProductDegressive[]
     */
    public function findAllByProductAndByGroup(Product $product, null|string|ProductPriceGroup $group = null): array
    {
        $qb = $this->createQueryBuilder('d');

        $qb
            ->innerJoin('d.products', 'products', Join::WITH, 'products.id = :product')
            ->innerJoin('d.types', 'types')
            ->setParameter('product', $product->getId())
            ->orderBy('d.qtyOf', 'DESC');

        if (null !== $group) {
            $qb
                ->innerJoin('d.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        return $qb->getQuery()->getResult();
    }

    /**
     * @return ProductDegressive[]
     */
    public function findAllApplicableByProductAndByGroup(Product $product, null|string|ProductPriceGroup $group = null): array
    {
        $qb = $this->createQueryBuilder('d');

        $qb
            ->distinct()
            ->leftJoin('d.products', 'products')
            ->innerJoin('d.types', 'types')
            ->andWhere(
                $qb->expr()->orX(
                    $qb->expr()->eq('products.id', ':product'),
                    $qb->expr()->isNull('products.id')
                )
            )
            ->setParameter('product', $product->getId())
            ->orderBy('d.qtyOf', 'ASC')
            ->addOrderBy('d.qtyAt', 'ASC');

        if (null !== $group) {
            $qb
                ->innerJoin('d.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        return $qb->getQuery()->getResult();
    }

    public function findByQuantityAndByItemAndByGroup(int|float $qty, CartItem $item, null|string|ProductPriceGroup $group = null): ?ProductDegressive
    {
        $qb = $this->createQueryBuilder('d');

        $qb
            ->innerJoin('d.products', 'products', Join::WITH, 'products.id = :product')
            ->innerJoin('d.types', 'types', Join::WITH, 'types.id = :type')
            ->setParameter('product', $item->getVariant()->getProduct()->getId())
            ->setParameter('type', $item->getVariant()->getType()->getId())
            ->orderBy('d.qtyOf', 'DESC');

        if (null !== $group) {
            $qb
                ->innerJoin('d.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        foreach ($qb->getQuery()->getResult() as $result) {
            if ($result instanceof ProductDegressive && $this->matchesQuantity($result, $qty)) {
                return $result;
            }
        }

        return null;
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findByQuantityAndByTypeAndByGroup(
        int|float $qty,
        string $type,
        Cart $cart,
        ?string $group = null,
    ): ?ProductDegressive {
        $arrayItem = [];
        /** @var CartItem $item */
        foreach ($cart->getItems() as $item) {
            $arrayItem[] = $item->getVariant()->getProduct()->getId()->toString();
        }

        $qb = $this->createQueryBuilder('p');
        $qb
            ->innerJoin('p.types', 't', Join::WITH, 't.id = :type')
            ->setParameter('type', $type)
            ->orderBy('p.qtyOf', 'DESC');

        $qb->leftJoin('p.products', 'pr');

        if ([] !== $arrayItem) {
            $qb->andWhere(
                $qb->expr()->orX(
                    $qb->expr()->in('pr.id', ':productIds'),
                    $qb->expr()->isNull('pr.id')
                )
            )->setParameter('productIds', $arrayItem);
        } else {
            $qb->andWhere($qb->expr()->isNull('pr.id'));
        }

        if (null !== $group) {
            $qb
                ->innerJoin('p.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        $query = $qb->getQuery();
        $results = $query->getResult();

        if ([] !== $results) {
            /** @var ProductDegressive $result */
            foreach ($results as $result) {
                $quantity = 0 !== $result->getProducts()->count()
                    ? $this->computeCartQuantityByProducts($cart, $result->getProducts(), false)
                    : $qty;

                if ($this->matchesQuantity($result, $quantity)) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findByQuantityAndByTypeAndByGroupAndByEquivalency(
        int|float $qty,
        ?string $type,
        Cart $cart,
        ?string $group = null,
    ): ?ProductDegressive {
        $arrayItem = [];
        /** @var CartItem $item */
        foreach ($cart->getItems() as $item) {
            $arrayItem[] = $item->getVariant()->getProduct()->getId()->toString();
        }

        $qb = $this->createQueryBuilder('p');
        $qb
            ->andWhere('p.equivalency = TRUE')
            ->innerJoin('p.types', 't', Join::WITH, 't.id = :type')
            ->setParameter('type', $type)
            ->orderBy('p.qtyOf', 'DESC');

        $qb->leftJoin('p.products', 'pr');

        if ([] !== $arrayItem) {
            $qb->andWhere(
                $qb->expr()->orX(
                    $qb->expr()->in('pr.id', ':productIds'),
                    $qb->expr()->isNull('pr.id')
                )
            )->setParameter('productIds', $arrayItem);
        } else {
            $qb->andWhere($qb->expr()->isNull('pr.id'));
        }

        if (null !== $group) {
            $qb
                ->innerJoin('p.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        $query = $qb->getQuery();
        $results = $query->getResult();

        if ([] !== $results) {
            /** @var ProductDegressive $result */
            foreach ($results as $result) {
                $quantity = 0 !== $result->getProducts()->count()
                    ? $this->computeCartQuantityByProducts($cart, $result->getProducts(), true)
                    : $qty;

                if ($this->matchesQuantity($result, $quantity)) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * @param Collection<int, mixed> $types
     */
    public function findOneByTypesAndQuantity(Collection $types, int|float $quantity, ?string $group = null): ?ProductDegressive
    {
        $qb = $this->createQueryBuilder('d');

        foreach ($types as $key => $type) {
            $qb->setParameter('type'.$key, $type);
            $qb->innerJoin('d.types', 't'.$key, Join::WITH, 't'.$key.' = :type'.$key);
        }

        if (null !== $group) {
            $qb
                ->innerJoin('d.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        $qb->addOrderBy('d.qtyOf', 'DESC');

        foreach ($qb->getQuery()->getResult() as $result) {
            if ($result instanceof ProductDegressive && $this->matchesQuantity($result, $quantity)) {
                return $result;
            }
        }

        return null;
    }

    /**
     * @param Collection<int, mixed> $types
     * @param Collection<int, mixed> $products
     */
    public function findOneByTypesAndProductsAndQuantity(
        Collection $types,
        Collection $products,
        int|float $quantity,
        ?string $group = null,
    ): ?ProductDegressive {
        $qb = $this->createQueryBuilder('d');

        foreach ($types as $key => $type) {
            $qb->setParameter('type'.$key, $type);
            $qb->innerJoin('d.types', 't'.$key, Join::WITH, 't'.$key.' = :type'.$key);
        }

        foreach ($products as $key => $product) {
            $qb->setParameter('product'.$key, $product);
            $qb->innerJoin('d.products', 'p'.$key, Join::WITH, 'p'.$key.' = :product'.$key);
        }

        if (null !== $group) {
            $qb
                ->innerJoin('d.groups', 'gr', Join::WITH, 'gr.id = :group')
                ->setParameter('group', $group);
        }

        $qb->addOrderBy('d.qtyOf', 'DESC');

        foreach ($qb->getQuery()->getResult() as $result) {
            if ($result instanceof ProductDegressive && $this->matchesQuantity($result, $quantity)) {
                return $result;
            }
        }

        return null;
    }

    private function matchesQuantity(ProductDegressive $rule, int|float $quantity): bool
    {
        $normalizedQuantity = max(0.0, (float) $quantity);
        $qtyOf = (float) ($rule->getQtyOf() ?? 0);
        $qtyAt = (float) ($rule->getQtyAt() ?? \PHP_FLOAT_MAX);

        return $qtyOf < $normalizedQuantity && $normalizedQuantity <= $qtyAt;
    }

    private function computeCartQuantityByProducts(Cart $cart, Collection $products, bool $useDegressiveEquivalency): float
    {
        $quantity = 0.0;

        foreach ($cart->getItems() as $item) {
            $variant = $item->getVariant();

            if (null === $variant || !$products->contains($variant->getProduct())) {
                continue;
            }

            if ($useDegressiveEquivalency) {
                $quantity += $item->getQuantity() * ((float) ($variant->getType()?->getEquivalencyDegressive() ?? 0.0));
                continue;
            }

            $quantity += $item->getQuantity();
        }

        return $quantity;
    }
}
