<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (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\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductCollection;
use CpCreation\VitiCore\Repository\BaseRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;

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

    public function findOneBySlug(string $slug): ?Product
    {
        return $this->createQueryBuilder('product')
            ->addSelect('translation', 'category', 'categoryTranslation')
            ->innerJoin('product.translations', 'matchingTranslation', Join::WITH, 'matchingTranslation.translatable = product.id')
            ->leftJoin('product.translations', 'translation', Join::WITH, 'translation.translatable = product.id')
            ->leftJoin('product.category', 'category')
            ->leftJoin('category.translations', 'categoryTranslation')
            ->andWhere('matchingTranslation.slug = :slug')
            ->setParameter('slug', $slug)
            ->getQuery()
            ->getOneOrNullResult();
    }

    /**
     * @return Product[]
     */
    public function findAllNotDeleted(): array
    {
        return $this->createQueryBuilder('product')
            ->addSelect('va')
            ->innerJoin('product.variants', 'va', Join::WITH, 'va.deletedAt IS NULL')
            ->orderBy('product.position', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return Product[]
     */
    public function findAllPublish($group = null, $category = null): array
    {
        $qb = $this->createQueryBuilder('product')
            ->addSelect('tr, im, im2, ca, va, ty, tytr, pr')
            ->innerJoin('product.translations', 'tr', Join::WITH, 'tr.translatable = product.id')
            ->leftJoin('product.category', 'ca', Join::WITH, 'ca.id = product.category')
            ->innerJoin('product.image', 'im', Join::WITH, 'im.id = product.image')
            ->leftJoin('product.secondImage', 'im2', Join::WITH, 'im2.id = product.secondImage')
            ->innerJoin('product.variants', 'va', Join::WITH, '(va.unPublished = FALSE and va.deletedAt IS NULL)')
            ->innerJoin('va.prices', 'pr')
            ->innerJoin('va.type', 'ty')
            ->innerJoin('ty.translations', 'tytr')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->orderBy('product.position', 'ASC');

        if (null !== $category) {
            $qb
                ->andWhere('product.category = :category')
                ->setParameter('category', $category);
        }

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

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

    /**
     * @return Product[]
     */
    public function findOnlyCuvee($category = null): array
    {
        if (null !== $category) {
            return $this->createQueryBuilder('product')
                ->addSelect('tr, im')
                ->innerJoin('product.translations', 'tr', Join::WITH, 'tr.translatable = product.id')
                ->innerJoin('product.image', 'im', Join::WITH, 'im.id = product.image')
                ->andWhere('product.category = :category')
                ->andWhere('product.disabled = FALSE')
                ->andWhere('product.deletedAt IS NULL')
                ->andWhere('product.cuvee = TRUE')
                ->orderBy('product.position', 'ASC')
                ->setParameter('category', $category)
                ->getQuery()
                ->getResult();
        }

        return $this->createQueryBuilder('product')
            ->addSelect('tr, im')
            ->innerJoin('product.translations', 'tr')
            ->innerJoin('product.image', 'im')
            ->innerJoin('product.category', 'cat')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->andWhere('cat.pageCuvee = TRUE')
            ->andWhere('product.cuvee = TRUE')
            ->addOrderBy('cat.position', 'ASC')
            ->addOrderBy('product.position', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return Product[]
     */
    public function findOnlyShop($group = null, $category = null): array
    {
        $qb = $this->createQueryBuilder('product')
            ->addSelect('tr, im')
            ->innerJoin('product.translations', 'tr', Join::WITH, 'tr.translatable = product.id')
            ->innerJoin('product.image', 'im', Join::WITH, 'im.id = product.image')
            ->innerJoin('product.variants', 'va', Join::WITH, '(va.unPublished = FALSE and va.deletedAt IS NULL)')
            ->innerJoin('va.type', 'type', Join::WITH, 'type.id = va.type')
            ->innerJoin('product.category', 'cat')
            ->andWhere('cat.pageShop = TRUE')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->andWhere('product.shop = TRUE')
            ->orderBy('product.positionShop', 'ASC');

        if (null !== $category) {
            $qb
                ->andWhere('product.category = :category')
                ->setParameter('category', $category);
        }

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

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

    /**
     * Catalogue brut destine a la projection Commerce.
     *
     * Contrairement a findOnlyShop(), cette lecture n'impose pas la presence
     * d'une image, d'une categorie, d'une variante ou d'un type. Le projecteur
     * peut ainsi exposer une fiche incomplete avec des erreurs de validation
     * exploitables par le hub central.
     *
     * @return Product[]
     */
    public function findForCommerceCatalog(): array
    {
        return $this->createQueryBuilder('product')
            ->addSelect(
                'translations',
                'mainImage',
                'secondImage',
                'category',
                'categoryTranslations',
                'productImages',
                'productImageMedia',
                'variants',
                'types',
                'typeTranslations',
                'prices',
                'priceGroups',
                'productGroups',
                'variantImages',
                'variantImageMedia',
                'options',
                'optionTranslations',
                'optionImages',
                'optionImageMedia',
            )
            ->leftJoin('product.translations', 'translations')
            ->leftJoin('product.image', 'mainImage')
            ->leftJoin('product.secondImage', 'secondImage')
            ->leftJoin('product.category', 'category')
            ->leftJoin('category.translations', 'categoryTranslations')
            ->leftJoin('product.images', 'productImages')
            ->leftJoin('productImages.image', 'productImageMedia')
            ->leftJoin('product.variants', 'variants')
            ->leftJoin('variants.type', 'types')
            ->leftJoin('types.translations', 'typeTranslations')
            ->leftJoin('variants.prices', 'prices')
            ->leftJoin('prices.group', 'priceGroups')
            ->leftJoin('product.groups', 'productGroups')
            ->leftJoin('variants.images', 'variantImages')
            ->leftJoin('variantImages.image', 'variantImageMedia')
            ->leftJoin('variants.options', 'options')
            ->leftJoin('options.translations', 'optionTranslations')
            ->leftJoin('options.images', 'optionImages')
            ->leftJoin('optionImages.image', 'optionImageMedia')
            ->andWhere('product.shop = TRUE')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->addOrderBy('product.positionShop', 'ASC')
            ->addOrderBy('product.position', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return Product[]
     */
    public function findPublishedForProductCollection(ProductCollection $collection, ?string $group = null, bool $shopOnly = false, ?int $limit = null): array
    {
        $qb = $this->createQueryBuilder('product')
            ->select('DISTINCT product')
            ->addSelect('tr, im, cat, va, type')
            ->innerJoin('product.translations', 'tr', Join::WITH, 'tr.translatable = product.id')
            ->innerJoin('product.image', 'im', Join::WITH, 'im.id = product.image')
            ->innerJoin('product.variants', 'va', Join::WITH, '(va.unPublished = FALSE and va.deletedAt IS NULL)')
            ->innerJoin('va.type', 'type', Join::WITH, 'type.id = va.type')
            ->leftJoin('product.category', 'cat')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->addOrderBy('cat.position', 'ASC')
            ->addOrderBy($shopOnly ? 'product.positionShop' : 'product.position', 'ASC');

        if ($shopOnly) {
            $qb
                ->andWhere('product.shop = TRUE')
                ->andWhere('cat.pageShop = TRUE');
        }

        if ($collection->isAutomaticOnlyCuvee()) {
            $qb->andWhere('product.cuvee = TRUE');
        }

        if (null !== $collection->getAutomaticCategory()) {
            $qb
                ->andWhere('product.category = :automaticCategory')
                ->setParameter('automaticCategory', $collection->getAutomaticCategory());
        }

        if ($collection->isAutomaticOnlyNew()) {
            $qb->andWhere('product.new = TRUE');
        }

        if ($collection->isAutomaticExcludeSoldOut()) {
            $qb
                ->andWhere('product.soldOut = FALSE')
                ->andWhere('va.soldOut = FALSE')
                ->andWhere('(va.stock IS NULL OR va.stock > 0)');
        }

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

        $resolvedLimit = $limit ?? $collection->getAutomaticLimit();
        if (null !== $resolvedLimit && $resolvedLimit > 0) {
            $qb->setMaxResults($resolvedLimit);
        }

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

    /**
     * @param array{
     *     category?: string|null,
     *     variant?: string|null,
     *     group?: string|null,
     *     cuvee?: bool|null,
     *     shop?: bool|null
     * } $filters
     *
     * @return Product[]
     */
    public function findByAdminFilters(array $filters = []): array
    {
        $qb = $this->createQueryBuilder('product')
            ->select('DISTINCT product, im, ca, gr, va, ty')
            ->leftJoin('product.image', 'im')
            ->leftJoin('product.category', 'ca')
            ->leftJoin('product.groups', 'gr')
            ->leftJoin('product.variants', 'va', Join::WITH, 'va.deletedAt IS NULL')
            ->leftJoin('va.type', 'ty')
            ->andWhere('product.deletedAt IS NULL')
            ->orderBy('product.position', 'ASC');

        if (!empty($filters['category'])) {
            $qb
                ->andWhere('ca.id = :categoryId')
                ->setParameter('categoryId', $filters['category']);
        }

        if (!empty($filters['variant'])) {
            $qb
                ->andWhere('ty.id = :variantId')
                ->setParameter('variantId', $filters['variant']);
        }

        if (!empty($filters['group'])) {
            $qb
                ->andWhere('gr.id = :groupId')
                ->setParameter('groupId', $filters['group']);
        }

        if (\array_key_exists('cuvee', $filters) && null !== $filters['cuvee']) {
            $qb
                ->andWhere('product.cuvee = :cuvee')
                ->setParameter('cuvee', $filters['cuvee']);
        }

        if (\array_key_exists('shop', $filters) && null !== $filters['shop']) {
            $qb
                ->andWhere('product.shop = :shop')
                ->setParameter('shop', $filters['shop']);
        }

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

    /**
     * @return Product[]
     */
    public function findByPositionShopOrderAdmin(): array
    {
        return $this->createQueryBuilder('r')
            ->andWhere('r.deletedAt IS NULL')
            ->orderBy('r.positionShop', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * @return Product[]
     */
    public function findByPositionShopOrder(): array
    {
        return $this->createQueryBuilder('r')
            ->andWhere('r.deletedAt IS NULL')
            ->andWhere('r.disabled = FALSE')
            ->orderBy('r.positionShop', 'ASC')
            ->getQuery()
            ->getResult();
    }

    public function moveToShopPosition(Product $product, int $targetPosition): void
    {
        $products = $this->createQueryBuilder('product')
            ->andWhere('product.deletedAt IS NULL')
            ->orderBy('product.positionShop', 'ASC')
            ->addOrderBy('product.createdAt', 'ASC')
            ->getQuery()
            ->getResult();

        $orderedProducts = [];
        $productId = $product->getId()->toString();

        foreach ($products as $item) {
            if (!$item instanceof Product || $item->getId()->toString() === $productId) {
                continue;
            }

            $orderedProducts[] = $item;
        }

        $targetPosition = max(0, min($targetPosition, \count($orderedProducts)));
        array_splice($orderedProducts, $targetPosition, 0, [$product]);

        $entityManager = $this->getEntityManager();
        $hasChanges = false;

        foreach ($orderedProducts as $index => $item) {
            if (!$item instanceof Product || $item->getPositionShop() === $index) {
                continue;
            }

            $item->setPositionShop($index);
            $entityManager->persist($item);
            $hasChanges = true;
        }

        if ($hasChanges) {
            $entityManager->flush();
        }
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findNextCuvee(int $position): ?Product
    {
        return $this->createQueryBuilder('product')
            ->innerJoin('product.category', 'cat')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->andWhere('cat.pageCuvee = TRUE')
            ->andWhere('product.cuvee = TRUE')
            ->andWhere('product.position > :position')
            ->addOrderBy('cat.position', 'ASC')
            ->addOrderBy('product.position', 'ASC')
            ->setMaxResults(1)
            ->setParameter('position', $position)
            ->getQuery()
            ->getOneOrNullResult();
    }

    /**
     * @throws NonUniqueResultException
     */
    public function findPreviousCuvee(int $position): ?Product
    {
        return $this->createQueryBuilder('product')
            ->innerJoin('product.category', 'cat')
            ->andWhere('product.disabled = FALSE')
            ->andWhere('product.deletedAt IS NULL')
            ->andWhere('cat.pageCuvee = TRUE')
            ->andWhere('product.cuvee = TRUE')
            ->andWhere('product.position < :position')
            ->addOrderBy('product.position', 'DESC')
            ->setMaxResults(1)
            ->setParameter('position', $position)
            ->getQuery()
            ->getOneOrNullResult();
    }
}
