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

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Ferienpass\CoreBundle\Entity\AccessCode;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\FriendCode;
use Ferienpass\CoreBundle\Entity\ParticipantDuplicate;
use Ferienpass\CoreBundle\Entity\ParticipantLog;
use Ferienpass\CoreBundle\Entity\PaymentItem;
use Ferienpass\CoreBundle\Entity\PostalAddress;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\MappedSuperclass]
#[ORM\Index(columns: ['createdAt'])]
#[ORM\Index(columns: ['lastname'])]
class BaseParticipant implements ParticipantInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
    #[Groups(['list:export:admin', 'list:export:default'])]
    private ?int $id = null;

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

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeInterface $createdAt;

    #[ORM\Column(type: 'string', length: 255, nullable: false, options: ['default' => ''])]
    #[Groups(['list:export:admin', 'list:export:default', 'notification'])]
    #[Assert\NotBlank]
    private ?string $firstname = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true, options: ['default' => ''])]
    #[Groups(['list:export:admin', 'list:export:default', 'notification'])]
    #[Assert\NotBlank]
    private ?string $lastname = null;

    #[ORM\Column(type: 'date', nullable: true)]
    #[Groups(['list:export:admin', 'list:export:default', 'notification'])]
    #[Assert\LessThan('today', message: 'Das Geburtsdatum muss in der Vergangenheit liegen')]
    // #[Assert\NotBlank]
    private ?\DateTimeInterface $dateOfBirth = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    #[PhoneNumber(defaultRegion: 'DE')]
    #[Groups(['list:export:admin', 'list:export:default'])]
    private ?string $phone = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    #[PhoneNumber(type: PhoneNumber::MOBILE, defaultRegion: 'DE')]
    private ?string $mobile = null;

    #[ORM\OneToOne(targetEntity: PostalAddress::class, cascade: ['persist'])]
    #[ORM\JoinColumn(name: 'address_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
    private ?PostalAddress $postalAddress = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    #[Assert\Email]
    #[Assert\NotBlank(groups: ['no_parent'])]
    private ?string $email = null;

    #[ORM\ManyToMany(targetEntity: AccessCode::class, mappedBy: 'participants', cascade: ['persist'])]
    private Collection $accessCodes;

    #[ORM\ManyToMany(targetEntity: FriendCode::class, mappedBy: 'participants', cascade: ['persist'])]
    private Collection $friendCodes;

    #[ORM\OneToMany(mappedBy: 'participant', targetEntity: Consent::class, orphanRemoval: true)]
    #[ORM\OrderBy(['signedAt' => 'DESC'])]
    private Collection $consents;

    /**
     * Use FETCH_MODE = EAGER here because it means the collection will not be re-fetched (we use a join condition in the ParticipantsController).
     *
     * @psalm-var Collection<int, Attendance>
     */
    #[ORM\OneToMany(mappedBy: 'participant', targetEntity: Attendance::class, cascade: ['remove'], fetch: 'EXTRA_LAZY')]
    private Collection $attendances;

    #[ORM\OneToMany(mappedBy: 'participant', targetEntity: ParticipantLog::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['createdAt' => 'DESC'])]
    private Collection $activity;

    #[ORM\OneToMany(mappedBy: 'participant', targetEntity: ConversationItem::class, fetch: 'EXTRA_LAZY')]
    #[ORM\OrderBy(['createdAt' => 'ASC'])]
    private Collection $conversation;

    #[ORM\OneToMany(mappedBy: 'participant', targetEntity: ParticipantDuplicate::class, cascade: ['all'], orphanRemoval: true)]
    private Collection $duplicates;

    public function __construct(
        #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'participants')]
        #[ORM\JoinColumn(name: 'member_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
        private ?User $user = null
    ) {
        $this->uuid = Uuid::v7();

        $this->createdAt = new \DateTimeImmutable();
        $this->attendances = new ArrayCollection();
        $this->activity = new ArrayCollection();
        $this->conversation = new ArrayCollection();
        $this->accessCodes = new ArrayCollection();
        $this->friendCodes = new ArrayCollection();
        $this->consents = new ArrayCollection();
        $this->duplicates = new ArrayCollection();
    }

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

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

    public function getFirstname(): ?string
    {
        return $this->firstname;
    }

    public function getLastname(): ?string
    {
        return $this->lastname;
    }

    public function getName(): string
    {
        return trim(\sprintf('%s %s', $this->getFirstname(), $this->getLastname()));
    }

    public function getDateOfBirth(): ?\DateTimeInterface
    {
        return $this->dateOfBirth;
    }

    public function getOwnPhone(): ?string
    {
        return $this->phone;
    }

    public function getPhone(): ?string
    {
        if ($this->phone) {
            return $this->phone;
        }

        if (!($user = $this->getUser()) instanceof User) {
            return null;
        }

        return $user->getPhone();
    }

    public function getOwnMobile(): ?string
    {
        return $this->mobile;
    }

    #[Groups(['list:export:admin', 'list:export:default', 'notification'])]
    public function getMobile(): ?string
    {
        if ($this->mobile) {
            return $this->mobile;
        }

        if (!($user = $this->getUser()) instanceof User) {
            return null;
        }

        return $user->getMobile();
    }

    public function getOwnEmail(): ?string
    {
        return $this->email;
    }

    #[Groups(['list:export:admin', 'list:export:default', 'notification'])]
    public function getEmail(): ?string
    {
        if ($this->email) {
            return $this->email;
        }

        if (!($user = $this->getUser()) instanceof User) {
            return null;
        }

        return $user->getEmail();
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): void
    {
        $this->user = $user;
    }

    public function getAge(?\DateTimeInterface $atDate = null): ?int
    {
        if (!$this->dateOfBirth instanceof \DateTimeInterface) {
            return null;
        }

        return $this->dateOfBirth->diff($atDate ?? new \DateTimeImmutable())->y;
    }

    /**
     * @return Collection|Attendance[]
     *
     * @psalm-return Collection<int, Attendance>
     */
    public function getAttendances(?string $orderBy = null): Collection
    {
        return $this->attendances;
    }

    public function addAttendance(Attendance $attendance): void
    {
        if (!$this->attendances->contains($attendance)) {
            $this->attendances->add($attendance);
            $attendance->setParticipant($this);
        }
    }

    public function removeAttendance(Attendance $attendance): void
    {
        if ($this->attendances->removeElement($attendance)) {
            if ($attendance->getParticipant() === $this) {
                $attendance->setParticipant(null);
            }
        }
    }

    public function addActivity(ParticipantLog $log): void
    {
        if (!$this->activity->contains($log)) {
            $this->activity->add($log);
            $log->setParticipant($this);
        }
    }

    public function removeActivity(ParticipantLog $log): void
    {
        if ($this->activity->removeElement($log)) {
            if ($log->getParticipant() === $this) {
                // ParticipantLog has non-nullable participant in constructor but onDelete=SET NULL in JoinColumn
                // However, our setParticipant allows changing it.
            }
        }
    }

    public function addConsent(Consent $consent): void
    {
        if (!$this->consents->contains($consent)) {
            $this->consents->add($consent);
            $consent->setParticipant($this);
        }
    }

    public function removeConsent(Consent $consent): void
    {
        if ($this->consents->removeElement($consent)) {
            if ($consent->getParticipant() === $this) {
                $consent->setParticipant(null);
            }
        }
    }

    /**
     * @return Collection|Attendance[]
     *
     * @psalm-return Collection<int, Attendance>
     */
    public function getAttendancesNotWithdrawn(?Edition $edition = null, $includedCancelled = true): Collection
    {
        $attendances = $this->getAllAttendances()->filter(static fn (Attendance $attendance) => AttendanceStatus::WITHDRAWN !== $attendance->getStatus() && (!$edition instanceof Edition || $edition === $attendance->getOffer()->getEdition()));

        if (false === $includedCancelled) {
            $attendances = $attendances->filter(static fn (Attendance $a) => false === $a->getOffer()->isCancelled());
        }

        return $attendances;
    }

    /**
     * @return Collection|Attendance[]
     *
     * @psalm-return Collection<int, Attendance>
     */
    public function getAttendancesNotHidden(?Edition $edition = null, $includedCancelled = true): Collection
    {
        $attendances = $this->getAllAttendances()->filter(static fn (Attendance $attendance) => AttendanceStatus::WITHDRAWN !== $attendance->getStatus() && AttendanceStatus::UNFULFILLED !== $attendance->getStatus() && (!$edition instanceof Edition || $edition === $attendance->getOffer()->getEdition()));

        if (false === $includedCancelled) {
            $attendances = $attendances->filter(static fn (Attendance $a) => false === $a->getOffer()->isCancelled());
        }

        return $attendances;
    }

    public function getAttendancesConfirmed(bool $cancelled = true): Collection
    {
        $attendances = $this->getAttendancesByStatus(AttendanceStatus::CONFIRMED);

        if (false === $cancelled) {
            $attendances = $attendances->filter(static fn (Attendance $a) => false === $a->getOffer()->isCancelled());
        }

        return $attendances;
    }

    public function getAttendancesWaitlisted(bool $cancelled = true): Collection
    {
        $attendances = $this->getAttendancesByStatus(AttendanceStatus::WAITLISTED);

        if (false === $cancelled) {
            $attendances = $attendances->filter(static fn (Attendance $a) => false === $a->getOffer()->isCancelled());
        }

        return $attendances;
    }

    public function getAttendancesWaiting(bool $cancelled = true): Collection
    {
        $attendances = $this->getAttendancesByStatus(AttendanceStatus::WAITING);

        if (false === $cancelled) {
            $attendances = $attendances->filter(static fn (Attendance $a) => false === $a->getOffer()->isCancelled());
        }

        return $attendances;
    }

    public function getAttendancesRejected(): Collection
    {
        return $this->getAttendancesByStatus(AttendanceStatus::REJECTED);
    }

    public function getAttendancesUnfulfilled(): Collection
    {
        return $this->getAttendancesByStatus(AttendanceStatus::UNFULFILLED);
    }

    /**
     * @return ArrayCollection|Attendance[]
     *
     * @psalm-return ArrayCollection<int, Attendance>
     */
    public function getAttendancesByStatus(string $status): Collection
    {
        if (!\in_array($status, [AttendanceStatus::CONFIRMED, AttendanceStatus::WAITLISTED, AttendanceStatus::WAITING, AttendanceStatus::WITHDRAWN, AttendanceStatus::REJECTED, AttendanceStatus::UNFULFILLED], true)) {
            throw new \InvalidArgumentException("Status \"$status\" is unknown to the application.");
        }

        return $this->getAllAttendances()->filter(static fn (Attendance $attendance) => $status === $attendance->getStatus());
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getAttendancesConfirmedCount(): int
    {
        return $this->getAttendancesConfirmed()->count();
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getAttendancesWaitlistedCount(): int
    {
        return $this->getAttendancesWaitlisted()->count();
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getAttendancesWaitingCount(): int
    {
        return $this->getAttendancesWaiting()->count();
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getAttendancesRejectedCount(): int
    {
        return $this->getAttendancesRejected()->count();
    }

    /**
     * @return ArrayCollection|Attendance[]
     *
     * @psalm-return ArrayCollection<int, Attendance>
     */
    public function getAttendancesPaid(): Collection
    {
        return $this->getAllAttendances()->filter(static fn (Attendance $attendance) => $attendance->isPaid());
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getAttendancesPaidCount(): int
    {
        return $this->getAttendancesPaid()->count();
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getUnpaidSum(): float
    {
        $fees = $this->getAttendancesConfirmed(false)
            ->filter(static fn (Attendance $a) => !$a->isPaid())
            ->map(static fn (Attendance $a) => $a->getOffer()->getFeePayable($a->getParticipant()))
            ->toArray()
        ;

        return (float) (array_sum(array_values($fees)) / 100);
    }

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function getPaidSum(): float
    {
        $fees = $this->getAttendancesConfirmed(false)
            ->map(static fn (Attendance $a) => $a->getPaymentItems()->map(static fn (PaymentItem $pi) => $pi->getAmount())->toArray())
            ->toArray()
        ;

        $fees = array_merge(...$fees);

        return (float) (array_sum($fees) / 100);
    }

    public function getLastAttendance(): ?Attendance
    {
        /** @var ArrayCollection $this->attendances */
        $criteria = Criteria::create()
            ->orderBy(['modifiedAt' => Criteria::DESC])
        ;

        $attendances = $this->attendances->matching($criteria);

        return $attendances->first() ?: null;
    }

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

    public function getConversation(): Collection
    {
        return $this->conversation;
    }

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

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

    public function setDateOfBirth(?\DateTimeInterface $dateOfBirth): void
    {
        $this->dateOfBirth = $dateOfBirth;
    }

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

    public function setOwnPhone(?string $phone): void
    {
        $this->setPhone($phone);
    }

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

    public function setOwnMobile(?string $mobile): void
    {
        $this->setMobile($mobile);
    }

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

    public function setOwnEmail(?string $email): void
    {
        $this->setEmail($email);
    }

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

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

    #[Groups(['list:export:admin', 'list:export:default'])]
    public function hasUnpaidAttendances(): bool
    {
        return !$this->getAttendancesConfirmed(false)->filter(static fn (Attendance $a) => !$a->isPaid())->isEmpty();
    }

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

    public function getAccessCodes(): Collection
    {
        return $this->accessCodes;
    }

    public function addAccessCode(?AccessCode $code): void
    {
        if (!$code instanceof AccessCode) {
            return;
        }

        if ($this->accessCodes->contains($code)) {
            return;
        }

        $this->accessCodes->add($code);
        $code->addParticipant($this);
    }

    public function removeAccessCode(AccessCode $code): void
    {
        if ($this->accessCodes->contains($code)) {
            $this->accessCodes->removeElement($code);
            $code->removeParticipant($this);
        }
    }

    public function getFriendCodes(): Collection
    {
        return $this->friendCodes;
    }

    public function addFriendCode(?FriendCode $code): void
    {
        if (!$code instanceof FriendCode) {
            return;
        }

        if ($this->friendCodes->contains($code)) {
            return;
        }

        $this->friendCodes->add($code);
        $code->addParticipant($this);
    }

    public function removeFriendCode(FriendCode $code): void
    {
        if ($this->friendCodes->contains($code)) {
            $this->friendCodes->removeElement($code);
            $code->removeParticipant($this);
        }
    }

    public function getConsents(): Collection
    {
        return $this->consents;
    }

    public function getConsent(string $type): ?Consent
    {
        return $this->consents->findFirst(static fn (int $i, Consent $consent) => $consent->getType() === $type);
    }

    public function getPostalCode(): ?string
    {
        if ($this->getPostalAddress() instanceof PostalAddress) {
            return $this->getPostalAddress()->getPostalCode();
        }

        return ($this->getUser()?->getPostalAddresses()->first() ?: null)?->getPostalCode();
    }

    /**
     * @return Collection<int, ParticipantDuplicate>
     */
    public function getDuplicates(): Collection
    {
        return $this->duplicates;
    }

    public function addDuplicate(ParticipantInterface $duplicate, string $type): void
    {
        foreach ($this->duplicates as $existing) {
            if ($existing->getDuplicate() === $duplicate) {
                $existing->setType($type);

                return;
            }
        }

        $this->duplicates->add(new ParticipantDuplicate($this, $duplicate, $type));
    }

    private function getAllAttendances(): Collection
    {
        $attendances = new ArrayCollection($this->attendances->toArray());
        foreach ($this->duplicates as $duplicate) {
            if (ParticipantDuplicate::TYPE_TOLERATED === $duplicate->getType()) {
                foreach ($duplicate->getDuplicate()->getAttendances() as $attendance) {
                    if (!$attendances->contains($attendance)) {
                        $attendances->add($attendance);
                    }
                }
            }
        }

        return $attendances;
    }
}
