<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Shop\Repository;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Repository\BaseRepository;
use CpCreation\VitiCore\Shop\Model\InformationBlock;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\Persistence\ManagerRegistry;

final class InformationBlockRepository extends BaseRepository
{
    /**
     * @throws \ReflectionException
     */
    public function __construct(private readonly ManagerRegistry $registry)
    {
        parent::__construct($this->registry, InformationBlock::class);
    }

    /**
     * @return list<InformationBlock>
     */
    public function findActiveOrdered(?string $zone = null, int $limit = 0): array
    {
        $groupId = GroupContext::$groupId;
        $now = new \DateTimeImmutable('now');
        $queryBuilder = $this->createQueryBuilder('b')
            ->distinct()
            ->andWhere('b.disabled = FALSE')
            ->andWhere('(b.publishedAt <= :now OR b.publishedAt IS NULL)')
            ->andWhere('(b.unPublishedAt >= :now OR b.unPublishedAt IS NULL)')
            ->orderBy('b.position', 'ASC')
            ->addOrderBy('b.createdAt', 'ASC')
            ->setParameter('now', $now);

        if (null !== $zone) {
            $queryBuilder
                ->andWhere('b.displayZone = :zone')
                ->setParameter('zone', $zone);
        }

        if (\is_string($groupId) && '' !== $groupId) {
            $queryBuilder
                ->leftJoin('b.groups', 'groups')
                ->andWhere('(groups.id = :groupId OR groups.id IS NULL)')
                ->setParameter('groupId', $groupId);
        }

        if ($limit > 0) {
            $queryBuilder->setMaxResults($limit);
        }

        /** @var list<InformationBlock> $result */
        $result = $queryBuilder->getQuery()->getResult();

        return $result;
    }

    /**
     * @return list<InformationBlock>
     */
    public function findByPositionOrderAdmin(): array
    {
        /** @var list<InformationBlock> $result */
        $result = $this->createQueryBuilder('b')
            ->leftJoin('b.groups', 'groups')
            ->addSelect('groups')
            ->addSelect(
                "CASE WHEN b.displayZone = '".InformationBlock::ZONE_TOP."' THEN 0 ELSE 1 END AS HIDDEN zoneOrder"
            )
            ->orderBy('zoneOrder', 'ASC')
            ->addOrderBy('b.position', 'ASC')
            ->addOrderBy('b.createdAt', 'ASC')
            ->getQuery()
            ->getResult();

        return $result;
    }

    public function countActiveInZone(string $zone, ?InformationBlock $excludedBlock = null): int
    {
        $now = new \DateTimeImmutable('now');
        $queryBuilder = $this->createQueryBuilder('b')
            ->select('COUNT(b.id)')
            ->andWhere('b.disabled = FALSE')
            ->andWhere('b.displayZone = :zone')
            ->andWhere('(b.publishedAt <= :now OR b.publishedAt IS NULL)')
            ->andWhere('(b.unPublishedAt >= :now OR b.unPublishedAt IS NULL)')
            ->setParameter('zone', $zone)
            ->setParameter('now', $now);

        if ($excludedBlock instanceof InformationBlock) {
            $queryBuilder
                ->andWhere('b.id != :excludedId')
                ->setParameter('excludedId', $excludedBlock->getId());
        }

        return (int) $queryBuilder->getQuery()->getSingleScalarResult();
    }

    public function prepareForSave(InformationBlock $informationBlock, ?string $previousZone = null): void
    {
        if (null === $previousZone || $previousZone !== $informationBlock->getDisplayZone()) {
            $informationBlock->setPosition($this->getNextPositionForZone($informationBlock->getDisplayZone()));
        }

        if (null !== $previousZone && $previousZone !== $informationBlock->getDisplayZone()) {
            $this->normalizeZonePositions($previousZone);
        }
    }

    public function moveUp(object $object): object|false
    {
        if (!$object instanceof InformationBlock) {
            return false;
        }

        $this->normalizeZonePositions($object->getDisplayZone());
        $this->getEntityManager()->refresh($object);

        $previous = $this->createQueryBuilder('b')
            ->andWhere('b.displayZone = :zone')
            ->andWhere('b.position < :position')
            ->setParameter('zone', $object->getDisplayZone())
            ->setParameter('position', $object->getPosition())
            ->orderBy('b.position', 'DESC')
            ->addOrderBy('b.createdAt', 'DESC')
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();

        if (!$previous instanceof InformationBlock) {
            return false;
        }

        $this->swapPositions($object, $previous);

        return $object;
    }

    public function moveDown(object $object): object|false
    {
        if (!$object instanceof InformationBlock) {
            return false;
        }

        $this->normalizeZonePositions($object->getDisplayZone());
        $this->getEntityManager()->refresh($object);

        $next = $this->createQueryBuilder('b')
            ->andWhere('b.displayZone = :zone')
            ->andWhere('b.position > :position')
            ->setParameter('zone', $object->getDisplayZone())
            ->setParameter('position', $object->getPosition())
            ->orderBy('b.position', 'ASC')
            ->addOrderBy('b.createdAt', 'ASC')
            ->setMaxResults(1)
            ->getQuery()
            ->getOneOrNullResult();

        if (!$next instanceof InformationBlock) {
            return false;
        }

        $this->swapPositions($object, $next);

        return $object;
    }

    private function swapPositions(InformationBlock $first, InformationBlock $second): void
    {
        $firstPosition = $first->getPosition();
        $secondPosition = $second->getPosition();

        $first->setPosition($secondPosition);
        $second->setPosition($firstPosition);

        $entityManager = $this->getEntityManager();
        $entityManager->persist($first);
        $entityManager->persist($second);
        $entityManager->flush();
    }

    private function getNextPositionForZone(string $zone): int
    {
        try {
            $maxPosition = $this->createQueryBuilder('b')
                ->select('MAX(b.position)')
                ->andWhere('b.displayZone = :zone')
                ->setParameter('zone', $zone)
                ->getQuery()
                ->getSingleScalarResult();
        } catch (NonUniqueResultException) {
            $maxPosition = null;
        }

        return null === $maxPosition ? 0 : ((int) $maxPosition) + 1;
    }

    private function normalizeZonePositions(string $zone): void
    {
        $blocks = $this->createQueryBuilder('b')
            ->andWhere('b.displayZone = :zone')
            ->setParameter('zone', $zone)
            ->orderBy('b.position', 'ASC')
            ->addOrderBy('b.createdAt', 'ASC')
            ->getQuery()
            ->getResult();

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

        foreach ($blocks as $index => $block) {
            if (!$block instanceof InformationBlock || $block->getPosition() === $index) {
                continue;
            }

            $block->setPosition($index);
            $entityManager->persist($block);
            $hasChanges = true;
        }

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