<?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\Shop\Repository;

use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Repository\BaseRepository;
use CpCreation\VitiCore\Shop\Model\DeliveryMethod;
use CpCreation\VitiCore\Shop\Model\ShippingPrice;
use CpCreation\VitiCore\Shop\Model\ShippingZone;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;

class ShippingPriceRepository extends BaseRepository
{
    /**
     * @var array<string, ?ShippingPrice>
     */
    private array $quantityTypeZoneCache = [];

    /**
     * @var array<string, ?ShippingPrice>
     */
    private array $quantityTypeZoneGroupCache = [];

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

    /**
     * @return list<ShippingPrice>
     */
    public function findTypesOfPrice(): array
    {
        return $this
            ->createQueryBuilder('p')
            ->innerJoin('p.types', 't')
            ->innerJoin('t.translations', 'tr')
            ->groupBy('tr.name')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return list<ShippingPrice>
     */
    public function findOrder(): array
    {
        return $this
            ->createQueryBuilder('p')
            ->addSelect('CASE WHEN p.qtyAt IS NULL THEN 1 ELSE 0 END AS HIDDEN qtyAtIsInfinite')
            ->innerJoin('p.types', 't')
            ->innerJoin('t.translations', 'tr')
            ->addOrderBy('tr.name')
            ->addOrderBy('qtyAtIsInfinite', 'ASC')
            ->addOrderBy('p.qtyAt')
            ->getQuery()
            ->getResult();
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findFreesShipping(): ?ShippingPrice
    {
        return $this
            ->createQueryBuilder('p')
            ->where('p.packagePrice = 0')
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findFreesShippingByGroup(mixed $group, ProductType $type): ?ShippingPrice
    {
        return $this
            ->createQueryBuilder('p')
            ->where('p.packagePrice = 0')
            ->innerJoin('p.groups', 'g', Join::WITH, 'g.id = :group')
            ->innerJoin('p.types', 't', Join::WITH, 't.id = :type')
            ->setParameter('group', $group)
            ->setParameter('type', $type->getId()->toString())
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findByQuantityAndByTypeAndByZone(int $qty, string $type, ShippingZone $zone): ?ShippingPrice
    {
        $cacheKey = implode('|', [
            $qty,
            $type,
            $zone->getId()->toString(),
        ]);

        if (\array_key_exists($cacheKey, $this->quantityTypeZoneCache)) {
            return $this->quantityTypeZoneCache[$cacheKey];
        }

        return $this->quantityTypeZoneCache[$cacheKey] = $this
            ->createQueryBuilder('p')
            ->innerJoin('p.types', 't')
            ->innerJoin('t.translations', 'tr', Join::WITH, 'tr.slug = :type')
            ->innerJoin('p.zones', 'z', Join::WITH, 'z.id = :zone')
            ->andWhere('p.qtyOf < :qty')
            ->andWhere('(p.qtyAt >= :qty OR p.qtyAt IS NULL)')
            ->setParameter('qty', $qty)
            ->setParameter('type', $type)
            ->setParameter('zone', $zone->getId()->toString())
            ->addOrderBy('p.createdAt', 'DESC')
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findByQuantityAndByTypeAndByZoneAndByGroup(
        int $qty,
        string $type,
        ShippingZone $zone,
        mixed $group,
        ?DeliveryMethod $deliveryMethod = null,
    ): ?ShippingPrice {
        $cacheKey = implode('|', [
            $qty,
            $type,
            $zone->getId()->toString(),
            (string) $group,
            $deliveryMethod?->getId()->toString() ?? 'standard',
        ]);

        if (\array_key_exists($cacheKey, $this->quantityTypeZoneGroupCache)) {
            return $this->quantityTypeZoneGroupCache[$cacheKey];
        }

        $qb = $this
            ->createQueryBuilder('p')
            ->innerJoin('p.types', 't')
            ->innerJoin('t.translations', 'tr', Join::WITH, 'tr.slug = :type')
            ->innerJoin('p.zones', 'z', Join::WITH, 'z.id = :zone')
            ->innerJoin('p.groups', 'g', Join::WITH, 'g.id = :group')
            ->andWhere('p.qtyOf < :qty')
            ->andWhere('(p.qtyAt >= :qty OR p.qtyAt IS NULL)')
            ->setParameter('qty', $qty)
            ->setParameter('type', $type)
            ->setParameter('group', $group)
            ->setParameter('zone', $zone->getId()->toString())
            ->addOrderBy('p.createdAt', 'DESC');

        if ($deliveryMethod) {
            $qb->andWhere('p.deliveryMethod = :deliveryMethod')
                ->setParameter('deliveryMethod', $deliveryMethod->getId()->toString());
        } else {
            $qb->andWhere('p.deliveryMethod IS NULL');
        }

        return $this->quantityTypeZoneGroupCache[$cacheKey] = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
    }

    /**
     * @return list<ShippingPrice>
     */
    public function findAvailableForGroup(?string $groupId): array
    {
        if (!\is_string($groupId) || '' === $groupId) {
            return [];
        }

        return $this
            ->createQueryBuilder('p')
            ->distinct()
            ->addSelect('CASE WHEN p.qtyAt IS NULL THEN 1 ELSE 0 END AS HIDDEN qtyAtIsInfinite')
            ->addSelect('dm', 'dmtr', 'z', 't')
            ->innerJoin('p.groups', 'g', Join::WITH, 'g.id = :group')
            ->leftJoin('p.deliveryMethod', 'dm')
            ->leftJoin('dm.translations', 'dmtr')
            ->leftJoin('p.zones', 'z')
            ->leftJoin('p.types', 't')
            ->setParameter('group', $groupId)
            ->addOrderBy('dm.position', 'ASC')
            ->addOrderBy('p.nameCarrier', 'ASC')
            ->addOrderBy('z.name', 'ASC')
            ->addOrderBy('p.qtyOf', 'ASC')
            ->addOrderBy('qtyAtIsInfinite', 'ASC')
            ->addOrderBy('p.qtyAt', 'ASC')
            ->getQuery()
            ->getResult();
    }

}
