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

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity]
#[ORM\UniqueConstraint(columns: ['offer_id', 'participant_id'])]
#[ORM\HasLifecycleCallbacks]
#[ORM\Index(columns: ['offer_id', 'status', 'sorting'], name: 'offer_id_status_sorting_idx')]
class Attendance implements \Stringable
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
    private ?int $id = null;

    #[ORM\Column(type: UuidType::NAME)]
    private Uuid $uuid;

    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
    private int $sorting = 0;

    #[ORM\Column(type: 'string', length: 32, nullable: true)]
    private ?string $status = AttendanceStatus::WAITING;

    #[ORM\Column(type: 'boolean', options: ['default' => 0])]
    private bool $paid = false;

    #[ORM\Column(type: 'boolean', options: ['default' => 0])]
    private bool $payable = false;

    #[ORM\Column(type: 'datetime_immutable', options: ['default' => 'CURRENT_TIMESTAMP'])]
    #[Groups('notification')]
    private \DateTimeInterface $createdAt;

    #[ORM\Column(type: 'datetime', options: ['default' => 'CURRENT_TIMESTAMP'])]
    private \DateTimeInterface $modifiedAt;

    #[ORM\ManyToOne(targetEntity: EditionTask::class)]
    #[ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
    private ?EditionTask $task = null;

    #[ORM\OneToMany(mappedBy: 'attendance', targetEntity: ParticipantLog::class, cascade: ['persist'], orphanRemoval: true)]
    private Collection $activity;

    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
    private int $userPriority = 0;

    #[ORM\Column(type: 'boolean', options: ['default' => true])]
    private bool $allAlternateDates = true;

    #[ORM\Column(type: 'integer', length: 3, nullable: true, options: ['unsigned' => true])]
    private ?int $age = null;

    // Only used for data retention.
    #[ORM\Column(name: 'participant_id_original', type: 'string', length: 10, nullable: true)]
    private ?string $participantPseudonym = null;

    #[ORM\OneToMany(mappedBy: 'attendance', targetEntity: PaymentItem::class, fetch: 'EXTRA_LAZY')]
    private Collection $paymentItems;

    #[ORM\ManyToMany(targetEntity: MessengerLog::class, mappedBy: 'attendances')]
    private Collection $messengerLogs;

    #[ORM\Column(type: 'json', nullable: true)]
    private ?array $extra = null;

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $consentOutstanding = false;

    #[ORM\Column(type: 'string', length: 36, nullable: true)]
    private ?string $consentToken = null;

    #[ORM\ManyToOne(targetEntity: AttendanceStash::class, cascade: ['persist'], inversedBy: 'attendances')]
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
    private ?AttendanceStash $stash = null;

    /**
     * Postal address captured for this specific application. Populated for
     * anonymous applications when the system requires an address; signed-in
     * members reference their account's address instead (User.postalAddresses).
     */
    #[ORM\OneToOne(targetEntity: PostalAddress::class, cascade: ['persist'])]
    #[ORM\JoinColumn(name: 'postal_address_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
    private ?PostalAddress $postalAddress = null;

    public function __construct(#[ORM\ManyToOne(targetEntity: OfferInterface::class, inversedBy: 'attendances')]
        #[ORM\JoinColumn(name: 'offer_id', referencedColumnName: 'id')]
        private OfferInterface $offer, #[ORM\ManyToOne(targetEntity: ParticipantInterface::class, inversedBy: 'attendances')]
        #[ORM\JoinColumn(name: 'participant_id', referencedColumnName: 'id', nullable: true)]
        private ?ParticipantInterface $participant)
    {
        $this->uuid = Uuid::v7();
        $this->createdAt = new \DateTimeImmutable();
        $this->activity = new ArrayCollection();
        $this->messengerLogs = new ArrayCollection();
        $this->paymentItems = new ArrayCollection();

        $this->setModifiedAt();
    }

    public function __toString(): string
    {
        return (string) $this->getId();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUuid(): Uuid
    {
        return $this->uuid;
    }

    public function getSorting(): int
    {
        return $this->sorting;
    }

    public function setSorting(int $sorting): void
    {
        $this->sorting = $sorting;
    }

    #[Groups('notification')]
    public function getStatus(): ?string
    {
        return $this->status;
    }

    public function resetStatus()
    {
        $this->status = AttendanceStatus::WAITING;
    }

    public function setStatus(string $status): void
    {
        $this->status = $status;

        $this->setModifiedAt();
    }

    public function getActivity(): Collection
    {
        return $this->activity;
    }

    public function getComments(): Collection
    {
        return $this->activity->filter(static fn (ParticipantLog $l) => $l->isComment());
    }

    public function setPaid($paid = true): void
    {
        $this->paid = $paid;
    }

    public function isPaid(): bool
    {
        return $this->paid;
    }

    public function isPayable(): bool
    {
        return $this->payable;
    }

    public function setPayable(bool $payable): void
    {
        $this->payable = $payable;
    }

    public function isConfirmed(): bool
    {
        return AttendanceStatus::CONFIRMED === $this->status;
    }

    public function isParticipated(): bool
    {
        return AttendanceStatus::PARTICIPATED === $this->status;
    }

    public function isWithdrawn(): bool
    {
        return AttendanceStatus::WITHDRAWN === $this->status;
    }

    public function isHidden(): bool
    {
        return $this->isWithdrawn() || $this->isUnfulfilled();
    }

    public function isUnfulfilled(): bool
    {
        return AttendanceStatus::UNFULFILLED === $this->status;
    }

    public function isWaitlisted(): bool
    {
        return AttendanceStatus::WAITLISTED === $this->status;
    }

    public function isWaiting(): bool
    {
        return AttendanceStatus::WAITING === $this->status;
    }

    public function isRejected(): bool
    {
        return AttendanceStatus::REJECTED === $this->status;
    }

    public function isEmailUnconfirmed(): bool
    {
        return AttendanceStatus::EMAIL_UNCONFIRMED === $this->status;
    }

    /**
     * Held until the legal guardian signed the consents via the shared link
     * (status-based; not to be confused with the {@see $consentOutstanding} flag,
     * which also covers allocated attendances with unsigned consents).
     */
    public function isAwaitingConsents(): bool
    {
        return AttendanceStatus::CONSENTS_OUTSTANDING === $this->status;
    }

    public function getCreatedAt(): \DateTimeInterface
    {
        return $this->createdAt;
    }

    public function getOffer(): OfferInterface
    {
        return $this->offer;
    }

    public function setOffer(OfferInterface $offer): void
    {
        $this->offer = $offer;
    }

    public function getParticipant(): ?ParticipantInterface
    {
        return $this->participant;
    }

    public function getTask(): ?EditionTask
    {
        return $this->task;
    }

    public function setTask(?EditionTask $task): void
    {
        $this->task = $task;
    }

    public function setModifiedAt(?\DateTimeInterface $modifiedAt = null): void
    {
        if (!$modifiedAt instanceof \DateTimeInterface) {
            $modifiedAt = new \DateTimeImmutable();
        }

        $this->modifiedAt = $modifiedAt;
    }

    public function getModifiedAt(): \DateTimeInterface
    {
        return $this->modifiedAt;
    }

    public function getUserPriority(): int
    {
        return $this->userPriority;
    }

    public function isAllAlternateDates(): bool
    {
        return $this->allAlternateDates;
    }

    public function setAllAlternateDates(bool $allAlternateDates): void
    {
        $this->allAlternateDates = $allAlternateDates;
    }

    public function setUserPriority(int $userPriority): void
    {
        $this->userPriority = $userPriority;
    }

    public function getAge(): ?int
    {
        return $this->age;
    }

    public function setExtra(array $values): void
    {
        $this->extra = $values;
    }

    public function getExtra(string $key): string|bool|int|null
    {
        return $this->extra[$key] ?? null;
    }

    public function isConsentOutstanding(): bool
    {
        return $this->consentOutstanding;
    }

    public function setConsentOutstanding(bool $consentOutstanding): void
    {
        $this->consentOutstanding = $consentOutstanding;
    }

    public function getConsentToken(): ?string
    {
        return $this->consentToken;
    }

    public function setConsentToken(?string $consentToken): void
    {
        $this->consentToken = $consentToken;
    }

    public function getPostalAddress(): ?PostalAddress
    {
        return $this->postalAddress;
    }

    public function setPostalAddress(?PostalAddress $postalAddress): void
    {
        $this->postalAddress = $postalAddress;
    }

    public function getStash(): ?AttendanceStash
    {
        return $this->stash;
    }

    public function setStash(?AttendanceStash $stash): void
    {
        if ($this->stash && $this->stash !== $stash) {
            $this->stash->removeAttendance($this);
        }

        $this->stash = $stash;

        if ($stash) {
            $stash->addAttendance($this);
        }
    }

    public function getName(): string
    {
        return $this->participant?->getName() ?? '';
    }

    #[Groups('docx_export')]
    public function getPhone(): string
    {
        return $this->participant?->getPhone() ?? '';
    }

    #[Groups('docx_export')]
    public function getEmail(): string
    {
        return $this->participant?->getEmail() ?? '';
    }

    #[Groups('docx_export')]
    public function getFee(): string
    {
        $fee = $this->offer->getFee();
        if (!$fee) {
            return '';
        }

        return \sprintf('%s €', number_format($fee / 100, 2, ',', '.'));
    }

    /**
     * @return Collection|PaymentItem[]
     *
     * @psalm-return Collection<int, PaymentItem>
     */
    public function getPaymentItems(): Collection
    {
        return $this->paymentItems;
    }

    /**
     * @return Collection|PaymentItem[]
     *
     * @psalm-return Collection<int, PaymentItem>
     */
    public function getMessengerLogs(): Collection
    {
        return $this->messengerLogs;
    }

    public function getParticipantPseudonym(): ?string
    {
        return $this->participantPseudonym;
    }

    public function setParticipantPseudonym(string $participantPseudonym): void
    {
        $this->participantPseudonym = $participantPseudonym;
    }

    public function setParticipant(?ParticipantInterface $participant): void
    {
        $this->participant = $participant;
    }

    public function unsetParticipant(): void
    {
        $this->setParticipant(null);
    }

    public function getFriendCode(): ?FriendCode
    {
        return $this->participant?->getFriendCodes()->findFirst(fn (int $i, FriendCode $c) => $c->getOffer() === $this->offer);
    }

    public function getFriendedAttendances(): array
    {
        $code = $this->getFriendCode();
        if (!$code instanceof FriendCode) {
            return [];
        }

        $attendances = [];
        foreach ($code->getParticipants() as $participant) {
            if ($participant === $this->participant) {
                continue;
            }
            $attendances[] = $participant->getAttendancesNotHidden()->filter(fn (Attendance $a) => $a->getOffer() === $this->offer)->toArray();
        }

        return array_merge(...$attendances);
    }

    public function getDateVariants(): Collection
    {
        return $this->getParticipant()->getAttendancesNotWithdrawn()->filter(fn (Attendance $a) => $this->offer === $a->getOffer() || $a->getOffer()->isVariantOf($this->offer));
    }

    #[ORM\PrePersist]
    #[ORM\PreUpdate]
    public function updateSorting(LifecycleEventArgs $eventArgs): void
    {
        // Only called when new entity is created with explicit status (this is not the case in the front end)
        if ($eventArgs instanceof PrePersistEventArgs && null === $this->status) {
            return;
        }

        // Only called when status was updated (e.g., by an application system, or by an admin) and no explicit sorting was given
        if ($eventArgs instanceof PreUpdateEventArgs && (!$eventArgs->hasChangedField('status') || $eventArgs->hasChangedField('sorting'))) {
            return;
        }

        $lastAttendance = $this->offer
            ->getAttendancesWithStatus($this->status)
            ->filter(static fn (Attendance $attendance) => $attendance->getSorting() > 0)->last() ?: null;

        $sorting = $lastAttendance?->getSorting() ?? 0;
        $sorting += 128;

        $this->setSorting($sorting);

        if ($eventArgs instanceof PrePersistEventArgs && (null === $this->status || $this->isWaiting())) {
            /** @var Attendance|false $lastAttendanceParticipant */
            $lastAttendanceParticipant = $this->participant
                ?->getAttendancesWaiting()
                ?->matching(Criteria::create()->orderBy(['userPriority' => Order::Ascending]))
                ?->last()
            ;

            $priority = $lastAttendanceParticipant ? $lastAttendanceParticipant->getUserPriority() + 1 : 1;
            if ($maxApplications = $this->task?->getMaxApplications()) {
                $priority = min($maxApplications + 1, $priority);
            }

            $this->setUserPriority($priority);
        }
    }
}
