<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Repository;

use DeepCopy\DeepCopy;
use DeepCopy\Filter\ChainableFilter;
use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Filter\KeepFilter;
use DeepCopy\Filter\ReplaceFilter;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
use DeepCopy\Matcher\PropertyMatcher;
use DeepCopy\Matcher\PropertyTypeMatcher;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\ConsentText;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferAttachment;
use Ferienpass\CoreBundle\Entity\OfferDate;
use Ferienpass\CoreBundle\Entity\OfferMedia;
use Ferienpass\CoreBundle\Entity\OfferMemberAssociation;
use Symfony\Component\Uid\Uuid;

class OfferRepository extends EntityRepository implements OfferRepositoryInterface
{
    public function findByEditionAndStatus(Edition $edition, string $status): array
    {
        return $this->createQueryBuilder('o')
            ->where('o.edition = :edition')
            ->setParameter('edition', $edition)
            ->andWhere("JSON_CONTAINS_PATH(o.status, 'one', CONCAT('$.', :status)) = 1")
            ->setParameter('status', $status)
            ->getQuery()
            ->getResult();
    }

    public function countByEditionAndStatus(Edition $edition, string $status): int
    {
        return (int) $this->createQueryBuilder('o')
            ->select('COUNT(o.id)')
            ->where('o.edition = :edition')
            ->setParameter('edition', $edition)
            ->andWhere("JSON_CONTAINS_PATH(o.status, 'one', CONCAT('$.', :status)) = 1")
            ->setParameter('status', $status)
            ->getQuery()
            ->getSingleScalarResult();
    }

    public function findByAlias(string $alias): ?OfferInterface
    {
        return $this->findOneBy(['alias' => $alias]);
    }

    public function findByUuid(string|Uuid $uuid): ?OfferInterface
    {
        return $this->findOneBy(['uuid' => $uuid]);
    }

    public function createCopy(OfferInterface $original, bool $withdrawDates = false): OfferInterface
    {
        $copier = new DeepCopy();

        // For associations to entities the offer merely references (rather than owns), the
        // copy must point at the very same rows. KeepFilter alone would do that, but it
        // leaves the copy holding the *original's* PersistentCollection instance: the copy
        // goes straight into an edit form, and every add/remove there would then be flushed
        // onto the original offer as well. Hand the copy an own collection over the same
        // referenced entities instead.
        $referenced = static fn (mixed $value): mixed => $value instanceof Collection ? new ArrayCollection($value->getValues()) : $value;

        $copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferInterface::class, 'id'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(OfferInterface::class, 'uuid'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferDate::class, 'id'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(OfferDate::class, 'uuid'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferAttachment::class, 'id'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(OfferAttachment::class, 'uuid'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferMedia::class, 'id'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(OfferMedia::class, 'uuid'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferMemberAssociation::class, 'id'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(OfferMemberAssociation::class, 'uuid'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(ConsentForm::class, 'id'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(ConsentForm::class, 'signatures'));
        $copier->addFilter(new ReplaceFilter(static fn () => Uuid::v7()), new PropertyMatcher(ConsentText::class, 'id'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferInterface::class, 'alias'));
        $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferInterface::class, 'contactUser'));
        $copier->addFilter(new KeepFilter(), new PropertyMatcher(OfferInterface::class, 'edition'));
        $copier->addFilter(new ReplaceFilter($referenced), new PropertyMatcher(OfferInterface::class, 'hosts'));
        $copier->addFilter(new KeepFilter(), new PropertyMatcher(OfferInterface::class, 'variantBase'));
        $copier->addFilter(new ReplaceFilter($referenced), new PropertyMatcher(OfferInterface::class, 'categories'));
        // The recipients are existing accounts, so they belong to the referenced group
        // above. Without a filter of their own they fall through to the catch-all
        // collection filter below, which deep-copies every element: flushing the copy then
        // fails with "A new entity was found through the relationship
        // Offer#participantListRecipients".
        $copier->addFilter(new ReplaceFilter($referenced), new PropertyMatcher(OfferInterface::class, 'participantListRecipients'));
        $copier->addFilter(new KeepFilter(), new PropertyMatcher(OfferAttachment::class, 'attachment'));
        $copier->addFilter(new KeepFilter(), new PropertyMatcher(OfferMedia::class, 'media'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'attendances'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'variants'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'activity'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'messengerLogs'));
        $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'friendCodes'));

        if ($withdrawDates) {
            $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher(OfferInterface::class, 'dates'));
            $copier->addFilter(new SetNullFilter(), new PropertyMatcher(OfferInterface::class, 'applicationDeadline'));
        }

        $copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher(Collection::class));

        /** @var OfferInterface $new */
        $new = $copier->copy($original);

        $new->resetStatus();

        $new->setVariantBase(null);

        return $new;
    }

    public function createVariant(OfferInterface $original): OfferInterface
    {
        $new = $this->createCopy($original);

        $new->setVariantBase($original->getVariantBase() ?? $original);

        return $new;
    }

    public function variantFields(): array
    {
        return [
            'name',
            'description',
            'meetingPoint',
            'meetingAddress',
            'meetingLat',
            'meetingLng',
            'bring',
            'minAge',
            'maxAge',
            'requiresApplication',
            'onlineApplication',
            'collectDateOfBirth',
            'applyText',
            'publish',
            'contactUser',
            'fee',
            'image',
            'edition',
            'hosts',
            'categories',
            'wheelchairAccessible',
            'requiresAgreementLetter',
            'applicationExtra',
            'media',
            'attachments',

            // Not real fields
            'contact',
            'uploadMedia',
        ];
    }

    public function findUpcoming(): array
    {
        $qb = $this->createQueryBuilder('offer')
            ->innerJoin('offer.dates', 'date')
            ->where('date.begin > CURRENT_TIMESTAMP()')
            ->addSelect('MIN(date.begin) AS HIDDEN minBegin')
            ->groupBy('offer.id')
            ->orderBy('minBegin')
        ;

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