<?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\Notification;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\ParticipantLog;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * The roster changes on one offer since a point in time, as the Veranstalter is
 * allowed to see them. Shared by the cron (which only asks "is there anything?")
 * and {@see \Ferienpass\CoreBundle\NotificationHandler\SendAttendanceChanges}, so
 * the window and the visibility rule are defined exactly once.
 */
final readonly class RosterChangeCollector
{
    /** Transitions that change the roster. Everything else is internal churn. */
    private const array REPORTED_STATUSES = [
        AttendanceStatus::CONFIRMED,
        AttendanceStatus::WAITLISTED,
        AttendanceStatus::WITHDRAWN,
        AttendanceStatus::REJECTED,
        AttendanceStatus::UNFULFILLED,
    ];

    public function __construct(
        private EntityManagerInterface $em,
        #[Autowire(param: 'ferienpass_admin.participant_list.hosts_can_edit')]
        private bool $hostsCanEdit = false,
    ) {
    }

    /**
     * @return list<array{attendance: Attendance, status: string, changedAt: \DateTimeInterface}>
     *                                                                                            Net change per attendance, oldest first — an attendance that flipped twice
     *                                                                                            inside the window is reported once, with where it ended up
     */
    public function collect(OfferInterface $offer, \DateTimeInterface $since): array
    {
        $statuses = self::REPORTED_STATUSES;
        if (!$this->hostsCanEdit) {
            // Same rule as the participant list: a Veranstalter who is only shown the
            // confirmed roster must not learn about applicants they never saw. A freed
            // seat still has to reach them, so it is reported once the seat had in fact
            // been confirmed at some point — see the query's EXISTS below.
            $statuses = [AttendanceStatus::CONFIRMED, ...AttendanceChangesNotification::FREED_STATUSES];
        }

        $qb = $this->em->createQueryBuilder()
            ->select('pl', 'a')
            ->from(ParticipantLog::class, 'pl')
            ->innerJoin('pl.attendance', 'a')
            ->where('a.offer = :offer')
            ->andWhere('pl.createdAt > :since')
            ->andWhere('pl.transitionTo IN (:statuses)')
            ->setParameter('offer', $offer)
            ->setParameter('since', $since)
            ->setParameter('statuses', $statuses)
            ->orderBy('pl.createdAt', 'ASC')
        ;

        if (!$this->hostsCanEdit) {
            $qb
                ->andWhere('pl.transitionTo = :confirmed OR EXISTS (SELECT prior.id FROM '.ParticipantLog::class.' prior WHERE prior.attendance = a AND prior.transitionTo = :confirmed)')
                ->setParameter('confirmed', AttendanceStatus::CONFIRMED)
            ;
        }

        $changes = [];
        /** @var ParticipantLog $log */
        foreach ($qb->getQuery()->getResult() as $log) {
            $attendance = $log->getAttendance();
            if (!$attendance instanceof Attendance) {
                continue;
            }

            // Keyed by attendance, overwritten as we walk forward in time, so the last
            // write wins and the net result survives.
            $changes[(string) $attendance->getId()] = [
                'attendance' => $attendance,
                'status' => (string) $log->getTransitionTo(),
                'changedAt' => $log->getCreatedAt(),
            ];
        }

        // Drop anything the attendance has since moved on from — the log we kept is
        // only the latest inside the window, and a change made outside it (or by a
        // path that writes no log) would otherwise be reported as current.
        $changes = array_filter($changes, static fn (array $c): bool => $c['status'] === $c['attendance']->getStatus());

        return array_values($changes);
    }
}
