<?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\AdminBundle\Components;

use Contao\StringUtil;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Ferienpass\AdminBundle\Menu\ActionsBuilder;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\OfferStatus;
use Ferienpass\CoreBundle\Notification\MailingNotification;
use Ferienpass\CoreBundle\Notifier\Notifier;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\Attribute\PostMount;
use Twig\Environment;

#[AsLiveComponent(route: 'live_component_admin')]
class OffersAssign extends AbstractController
{
    use ComponentToolsTrait;
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    #[LiveProp]
    public ?Edition $edition = null;

    #[LiveProp(onUpdated: 'onOfferUpdated', url: new UrlMapping(as: 'angebot'))]
    public ?OfferInterface $offer = null;

    #[LiveProp]
    public ?ParticipantInterface $participant = null;

    #[LiveProp(writable: true)]
    public string $query = '';

    public function __construct(private readonly EditionRepository $editions, private readonly OfferRepositoryInterface $offers, private readonly ParticipantRepositoryInterface $participants, private readonly UserRepository $users, private readonly HostRepository $hosts, private readonly Environment $twig, private readonly RequestStack $requestStack, private readonly NormalizerInterface $normalizer, private readonly Notifier $notifier, private readonly MailingNotification $mailingNotification, private readonly ActionsBuilder $actionsBuilder, #[Autowire(param: 'ferienpass.preferred_postal_codes')] #[ExposeInTemplate] public array $preferredPostalCodes)
    {
    }

    public function getMenu(OfferInterface $offer): \Knp\Menu\ItemInterface
    {
        return $this->actionsBuilder->actions([
            'item' => $offer,
            'view' => 'admin_offers_assign',
        ]);
    }

    #[ExposeInTemplate]
    public function demand(): ?int
    {
        if (!$this->offer || !$this->offer->getMaxParticipants()) {
            return null;
        }

        return (int) round($this->offer->getAttendances()->filter(static fn (Attendance $a) => !$a->isWithdrawn())->count() / $this->offer->getMaxParticipants() * 100);
    }

    #[ExposeInTemplate]
    public function offers(): iterable
    {
        $cancelled = OfferStatus::CANCELLED;

        /** @var QueryBuilder $qb */
        $qb = $this->offers->createQueryBuilder('o');
        $qb
            ->addSelect('(SELECT COUNT(a1.id) FROM '.Attendance::class.' a1 WHERE a1.offer = o.id AND a1.status = :status_waiting) AS HIDDEN countWaiting')
            ->addSelect('CASE WHEN o.maxParticipants > 0 AND (SELECT COUNT(a2.id) FROM '.Attendance::class.' a2 WHERE a2.offer = o.id AND a2.status = :status_confirmed) < o.maxParticipants AND (SELECT COUNT(a3.id) FROM '.Attendance::class.' a3 WHERE a3.offer = o.id AND a3.status = :status_waitlisted) > 0 THEN 1 ELSE 0 END AS HIDDEN falseWaitlisted')
            ->leftJoin('o.hosts', 'h')
            ->leftJoin('o.dates', 'd')
            ->andWhere('o.requiresApplication = 1')
            ->andWhere('o.onlineApplication = 1')
            ->addOrderBy("CASE WHEN JSON_CONTAINS_PATH(o.status, 'one', '$.$cancelled') = 1 THEN 0 ELSE 1 END", 'DESC')
            ->addOrderBy('countWaiting', 'DESC')
            ->addOrderBy('falseWaitlisted', 'DESC')
            ->addOrderBy('d.begin')
            ->addGroupBy('o.id')
            ->addGroupBy('d.begin')
            ->setParameter('status_waiting', AttendanceStatus::WAITING)
            ->setParameter('status_confirmed', AttendanceStatus::CONFIRMED)
            ->setParameter('status_waitlisted', AttendanceStatus::WAITLISTED)
        ;

        $this->addQueryBuilderSearch($qb);

        if ($this->edition instanceof Edition) {
            $qb->andWhere('o.edition = :edition')->setParameter('edition', $this->edition);
        }

        if ($this->participant instanceof ParticipantInterface) {
            $qb
                ->innerJoin('o.attendances', 'a', Join::WITH, 'a.participant = :participant')
                ->setParameter('participant', $this->participant->getId())
            ;
        }

        $offers = $qb->getQuery()->getResult();
        if (empty($offers)) {
            return [];
        }

        $this->offers->createQueryBuilder('o')
            ->addSelect('o', 'h', 'd', 'v', 'activity', 'createdBy')
            ->leftJoin('o.hosts', 'h')
            ->leftJoin('o.dates', 'd')
            ->leftJoin('o.variants', 'v')
            ->leftJoin('o.activity', 'activity')
            ->leftJoin('activity.createdBy', 'createdBy')
            ->where('o IN (:offers)')
            ->setParameter('offers', $offers)
            ->getQuery()
            ->getResult();

        $this->offers->createQueryBuilder('o')
            ->addSelect('o', 'a')
            ->leftJoin('o.attendances', 'a')
            ->where('o IN (:offers)')
            ->setParameter('offers', $offers)
            ->getQuery()
            ->getResult();

        return $offers;
    }

    #[ExposeInTemplate]
    public function participants(): iterable
    {
        /** @var QueryBuilder $qb */
        $qb = $this->participants->createQueryBuilder('p');
        $qb
            ->addSelect('p')
            ->innerJoin('p.attendances', 'a')
            ->addGroupBy('p')
        ;

        if ($this->offer instanceof OfferInterface) {
            $qb
                ->innerJoin('a.offer', 'o', Join::WITH, 'o.id = :offer')
                ->addGroupBy('a')
                ->setParameter('offer', $this->offer)
            ;
        } else {
            $qb
                ->innerJoin('a.offer', 'o')
            ;
        }

        if ($this->offer instanceof OfferInterface) {
            $qb
                ->addOrderBy('a.userPriority', 'ASC')
                ->addOrderBy('a.status')
                ->addOrderBy('a.createdAt', 'ASC')
            ;
        }

        if (!$this->offer instanceof OfferInterface) {
            $cancelled = OfferStatus::CANCELLED;

            $qb
                ->addSelect("sum(CASE WHEN JSON_CONTAINS_PATH(o.status, 'one', '$.$cancelled') = 1 AND a.status = :status_waiting THEN 1 ELSE 0 END) AS HIDDEN countWaiting")
                ->addSelect('sum(CASE WHEN a.status = :status_confirmed THEN 1 ELSE 0 END) AS HIDDEN countConfirmed')
                ->addOrderBy('countWaiting', 'DESC')
                ->addOrderBy('countConfirmed', 'DESC')
                ->setParameter('status_waiting', AttendanceStatus::WAITING)
                ->setParameter('status_confirmed', AttendanceStatus::CONFIRMED)
            ;
        }

        if ($this->edition instanceof Edition) {
            $qb->innerJoin('o.edition', 'e', Join::WITH, 'e = :edition')->setParameter('edition', $this->edition);
        }

        $participants = $qb->getQuery()->getResult();

        if (empty($participants)) {
            return [];
        }

        $this->participants->createQueryBuilder('p')
            ->addSelect('p', 'duplicates', 'u', 'u_blocked', 'address')
            ->leftJoin('p.duplicates', 'duplicates')
            ->leftJoin('p.user', 'u')
            ->leftJoin('u.blockedContact', 'u_blocked')
            ->leftJoin('p.postalAddress', 'address')
            ->where('p IN (:participants)')
            ->setParameter('participants', $participants)
            ->getQuery()
            ->getResult();

        $this->participants->createQueryBuilder('p')
            ->addSelect('p', 'a', 'o', 'e', 'd')
            ->leftJoin('p.attendances', 'a')
            ->leftJoin('a.offer', 'o')
            ->leftJoin('o.edition', 'e')
            ->leftJoin('o.dates', 'd')
            ->where('p IN (:participants)')
            ->setParameter('participants', $participants)
            ->getQuery()
            ->getResult();

        $this->participants->createQueryBuilder('p')
            ->addSelect('p', 'activity', 'createdBy')
            ->leftJoin('p.activity', 'activity')
            ->leftJoin('activity.createdBy', 'createdBy')
            ->where('p IN (:participants)')
            ->setParameter('participants', $participants)
            ->getQuery()
            ->getResult();

        return $participants;
    }

    #[ExposeInTemplate]
    public function maxApplications(): int
    {
        /** @var QueryBuilder $qb */
        $qb = $this->participants->createQueryBuilder('p');

        $qb
            ->innerJoin('p.attendances', 'a')
            ->innerJoin('a.offer', 'o')
            ->select('sum(CASE WHEN a.status = :status_withdrawn OR a.status = :status_unfulfilled THEN 0 ELSE 1 END) AS countAttendances')
            ->addGroupBy('p')
            ->addOrderBy('countAttendances', 'DESC')
            ->andWhere('a.status <> :status_withdrawn')
            ->andWhere('a.status <> :status_unfulfilled')
            ->setParameter('status_withdrawn', AttendanceStatus::WITHDRAWN)
            ->setParameter('status_unfulfilled', AttendanceStatus::UNFULFILLED)
        ;

        if ($this->edition instanceof Edition) {
            $qb->innerJoin('o.edition', 'e', Join::WITH, 'e.id = :edition')->setParameter('edition', $this->edition->getId());
        }

        try {
            $count = $qb->getQuery()->setMaxResults(1)->getSingleScalarResult();
        } catch (NoResultException) {
            return 0;
        }

        return max((int) $count, 1);
    }

    #[LiveListener('open')]
    public function openOffer(#[LiveArg('offer')] int $offerId): void
    {
        $this->offer = $this->offers->createQueryBuilder('o')
            ->addSelect('o', 'h', 'd', 'v', 'activity', 'createdBy')
            ->leftJoin('o.hosts', 'h')
            ->leftJoin('o.dates', 'd')
            ->leftJoin('o.variants', 'v')
            ->leftJoin('o.activity', 'activity')
            ->leftJoin('activity.createdBy', 'createdBy')
            ->where('o.id = :id')
            ->setParameter('id', $offerId)
            ->getQuery()
            ->getOneOrNullResult();

        if (null === $this->offer) {
            return;
        }

        $attendances = $this->offers->createQueryBuilder('o')
            ->addSelect('o', 'a', 'p')
            ->leftJoin('o.attendances', 'a')
            ->leftJoin('a.participant', 'p')
            ->where('o.id = :id')
            ->setParameter('id', $offerId)
            ->getQuery()
            ->getSingleResult()
            ->getAttendances();

        $participants = $attendances->map(static fn ($a) => $a->getParticipant())->filter(static fn ($p) => null !== $p)->toArray();

        if (!empty($participants)) {
            $this->participants->createQueryBuilder('p')
                ->addSelect('p', 'u', 'u_blocked', 'duplicates', 'activity', 'createdBy')
                ->leftJoin('p.user', 'u')
                ->leftJoin('u.blockedContact', 'u_blocked')
                ->leftJoin('p.duplicates', 'duplicates')
                ->leftJoin('p.activity', 'activity')
                ->leftJoin('activity.createdBy', 'createdBy')
                ->where('p IN (:participants)')
                ->setParameter('participants', $participants)
                ->getQuery()
                ->getResult();

            $this->participants->createQueryBuilder('p')
                ->addSelect('p', 'a', 'o', 'e', 'd')
                ->leftJoin('p.attendances', 'a')
                ->leftJoin('a.offer', 'o')
                ->leftJoin('o.edition', 'e')
                ->leftJoin('o.dates', 'd')
                ->where('p IN (:participants)')
                ->setParameter('participants', $participants)
                ->getQuery()
                ->getResult();
        }
    }

    #[LiveListener('selectParticipant')]
    public function selectParticipant(#[LiveArg('participant')] int $participantId): void
    {
        if (!$this->participant instanceof ParticipantInterface || $participantId !== $this->participant->getId()) {
            $this->participant = $this->participants->createQueryBuilder('p')
                ->addSelect('p', 'u', 'u_blocked', 'address', 'duplicates', 'activity', 'createdBy')
                ->leftJoin('p.user', 'u')
                ->leftJoin('u.blockedContact', 'u_blocked')
                ->leftJoin('p.postalAddress', 'address')
                ->leftJoin('p.duplicates', 'duplicates')
                ->leftJoin('p.activity', 'activity')
                ->leftJoin('activity.createdBy', 'createdBy')
                ->where('p.id = :id')
                ->setParameter('id', $participantId)
                ->getQuery()
                ->getOneOrNullResult();

            if ($this->participant) {
                $this->participants->createQueryBuilder('p')
                    ->addSelect('p', 'a', 'o', 'e', 'd')
                    ->leftJoin('p.attendances', 'a')
                    ->leftJoin('a.offer', 'o')
                    ->leftJoin('o.edition', 'e')
                    ->leftJoin('o.dates', 'd')
                    ->where('p.id = :id')
                    ->setParameter('id', $participantId)
                    ->getQuery()
                    ->getResult();
            }
        } else {
            $this->participant = null;
        }
    }

    #[ExposeInTemplate]
    public function showPostalCode(): bool
    {
        return (bool) $this->preferredPostalCodes;
    }

    public function onOfferUpdated($previous): void
    {
    }

    #[PostMount]
    public function postMount(): void
    {
        // Fix invalid entity when ?angebot=
        if (null === $this->offer?->getId()) {
            $this->offer = null;
        }
    }

    private function addQueryBuilderSearch(QueryBuilder $qb): void
    {
        $where = $qb->expr()->orX();
        foreach (array_filter(StringUtil::trimsplit(' ', $this->query)) as $j => $token) {
            foreach (['o.name', 'h.name'] as $field) {
                $where->add("$field LIKE :query_$j");
            }

            $qb->setParameter("query_$j", "%{$token}%");
        }

        if ($where->count()) {
            $qb->andWhere($where);
        }
    }
}
