<?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\ORM\Mapping as ORM;
use Ferienpass\CoreBundle\Emoji\EmojiDictionary;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Exception\AmbiguousHolidayTaskException;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\Uid\Uuid;

#[ORM\Entity(repositoryClass: EditionRepository::class)]
class Edition implements TenantAwareInterface
{
    use TenantAwareTrait;

    #[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: 'datetime_immutable', options: ['default' => 'CURRENT_TIMESTAMP'])]
    private \DateTimeInterface $createdAt;

    #[ORM\Column(type: 'string', nullable: true)]
    #[Groups(['notification', 'api:offer:read'])]
    private ?string $name = null;

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

    /**
     * A GitHub-style emoji short code (e.g. ":tada:") used as a visual marker to
     * tell editions apart at a glance. Stored as a short code so it stays plain
     * ASCII in the database; rendered to its glyph in the admin templates.
     */
    #[ORM\Column(type: 'string', length: 64, nullable: true)]
    private ?string $emoji = null;

    #[ORM\OneToMany(mappedBy: 'edition', targetEntity: EditionTask::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $tasks;

    #[ORM\OneToMany(mappedBy: 'edition', targetEntity: OfferInterface::class, cascade: ['remove'])]
    private Collection $offers;

    #[ORM\OneToMany(mappedBy: 'edition', targetEntity: ConsentForm::class, cascade: ['persist'])]
    private Collection $consentForms;

    #[ORM\Column(type: 'boolean')]
    private bool $archived = false;

    #[ORM\ManyToMany(targetEntity: Host::class)]
    #[ORM\JoinTable(name: 'EditionToHost', )]
    #[ORM\JoinColumn(name: 'edition_id', referencedColumnName: 'id')]
    #[ORM\InverseJoinColumn(name: 'host_id', referencedColumnName: 'id')]
    private Collection $hosts;

    #[ORM\Column(type: 'boolean')]
    private bool $hostsCanAssign = false;

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

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

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

    /**
     * Whether this season collects fees at all. Off means the season has no pay
     * status: no "bezahlt/unbezahlt" column in the participant lists, and none
     * of its attendances turn up in the payment lists (ausstehende Zahlungen,
     * Überzahlungen, Debitoren).
     */
    #[ORM\Column(type: 'boolean', options: ['default' => 1])]
    private bool $usePayments = true;

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

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

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

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

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

    public function __construct()
    {
        $this->uuid = Uuid::v7();
        $this->tasks = new ArrayCollection();
        $this->offers = new ArrayCollection();
        $this->hosts = new ArrayCollection();
        $this->activity = new ArrayCollection();
        $this->conversation = new ArrayCollection();
        $this->createdAt = new \DateTimeImmutable();
        $this->emoji = EmojiDictionary::random();
    }

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

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

    public function setId(int $id): void
    {
        $this->id = $id;
    }

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

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getAlias(): ?string
    {
        return $this->alias;
    }

    public function setAlias(string $alias): void
    {
        $this->alias = $alias;
    }

    public function getEmoji(): ?string
    {
        return $this->emoji;
    }

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

    public function isAddToNavigation(): bool
    {
        return $this->addToNavigation;
    }

    public function setAddToNavigation(bool $addToNavigation): void
    {
        $this->addToNavigation = $addToNavigation;
    }

    public function getNavigationPositionAfter(): ?int
    {
        return $this->navigationPositionAfter;
    }

    public function setNavigationPositionAfter(?int $navigationPositionAfter): void
    {
        $this->navigationPositionAfter = $navigationPositionAfter;
    }

    public function getWelcomeText(): ?string
    {
        return $this->welcomeText;
    }

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

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

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

    public function setTasks(Collection $tasks): void
    {
        $this->tasks = $tasks;
    }

    public function addTask(EditionTask $editionTask): void
    {
        $editionTask->setEdition($this);

        $this->tasks->add($editionTask);
    }

    public function removeTask(EditionTask $editionTask): void
    {
        $this->tasks->removeElement($editionTask);
    }

    public function isArchived(): bool
    {
        return $this->archived;
    }

    public function setArchived(bool $archived = true): void
    {
        $this->archived = $archived;
    }

    public function getOffers(): Collection
    {
        return $this->offers;
    }

    public function getHosts(): Collection
    {
        return $this->hosts;
    }

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

    public function setHostsCanAssign(bool $hostsCanAssign): void
    {
        $this->hostsCanAssign = $hostsCanAssign;
    }

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

    public function setHostsCanPublish(bool $hostsCanPublish): void
    {
        $this->hostsCanPublish = $hostsCanPublish;
    }

    public function getSurveyWithApplication(): ?string
    {
        return $this->surveyWithApplication;
    }

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

    public function getSurveyWithoutApplication(): ?string
    {
        return $this->surveyWithoutApplication;
    }

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

    /**
     * The participant/account consent declarations. Excludes the host
     * declaration ({@see getHostConsentForm()}) so the participant editor and
     * the frontend application form never pick it up.
     */
    public function getConsentForms(): Collection
    {
        return $this->consentForms->filter(static fn (ConsentForm $f): bool => !$f->isHost());
    }

    /**
     * The single "Einverständnis der Veranstaltenden" declaration, if configured.
     */
    public function getHostConsentForm(): ?ConsentForm
    {
        return $this->consentForms->filter(static fn (ConsentForm $f): bool => $f->isHost())->first() ?: null;
    }

    /**
     * Whether hosts must sign a confidentiality declaration for this edition: a
     * host declaration exists and actually carries a text to sign.
     */
    public function isHostAgreementActive(): bool
    {
        $form = $this->getHostConsentForm();

        return null !== $form && null !== $form->getText() && '' !== trim($form->getText());
    }

    public function addConsentForm(ConsentForm $consentForm): void
    {
        $consentForm->setEdition($this);

        $this->consentForms->add($consentForm);
    }

    public function removeConsentForm(ConsentForm $consentForm): void
    {
        $this->consentForms->removeElement($consentForm);
    }

    public function getHoliday(): ?EditionTask
    {
        $tasks = $this->getTasks()->filter(static fn (EditionTask $element) => 'holiday' === $element->getType());

        if ($tasks->count() > 1) {
            throw new AmbiguousHolidayTaskException('More than one holiday found for the pass edition ID '.($this->getId() ?? 0));
        }

        if (false === $task = $tasks->current()) {
            return null;
        }

        return $task;
    }

    public function getHostEditingStages(): Collection
    {
        return $this->getTasks()->filter(static fn (EditionTask $element) => 'host_editing_stage' === $element->getType());
    }

    /**
     * @return Collection<int, EditionTask>
     */
    public function getTasksOfType(string $type): Collection
    {
        return $this->getTasks()->filter(static fn (EditionTask $element) => $type === $element->getType());
    }

    /**
     * Get the host editing stages for this pass edition.
     */
    public function getCurrentHostEditingStage(): ?EditionTask
    {
        $now = new \DateTimeImmutable();
        $tasks = $this->getTasks()->filter(
            static fn (EditionTask $element) => 'host_editing_stage' === $element->getType()
                && $now >= $element->getPeriodBegin()
                && $now < $element->getPeriodEnd()
        );

        if ($tasks->count() > 1) {
            return $tasks->first();
        }

        if ($tasks->isEmpty()) {
            return null;
        }

        /** @var EditionTask $task */
        $task = $tasks->current();

        return $task;
    }

    public function isParticipantListReleased(): bool
    {
        if ($this->tasks->filter(static fn (EditionTask $element) => 'publish_lists' === $element->getType())->isEmpty()) {
            return true;
        }

        $time = new \DateTimeImmutable();
        $tasks = $this->tasks->filter(static fn (EditionTask $element) => 'publish_lists' === $element->getType() && $time >= $element->getPeriodBegin() && $time < $element->getPeriodEnd());

        return !$tasks->isEmpty();
    }

    public function isOnline(): bool
    {
        if ($this->tasks->filter(static fn (EditionTask $element) => 'show_offers' === $element->getType())->isEmpty()) {
            return true;
        }

        $time = new \DateTimeImmutable();
        $tasks = $this->tasks->filter(static fn (EditionTask $element) => 'show_offers' === $element->getType() && $time >= $element->getPeriodBegin() && $time < $element->getPeriodEnd());

        return !$tasks->isEmpty();
    }

    /**
     * Check whether the host can edit the offers of this pass edition.
     */
    public function isEditableForHosts(): bool
    {
        $hasCurrentHostEditingStage = $this->getCurrentHostEditingStage() instanceof EditionTask;
        $hasHostEditingStages = $this->getHostEditingStages()->count() > 0;

        return $hasCurrentHostEditingStage || !$hasHostEditingStages;
    }

    public function isCompleted(): bool
    {
        if ($this->isArchived()) {
            return true;
        }

        if (!$this->getHoliday() instanceof EditionTask) {
            return false;
        }

        return $this->getHoliday()->getPeriodEnd() < new \DateTimeImmutable();
    }

    /**
     * @return Collection<EditionTask>
     */
    public function getActiveTasks(string $taskName): Collection
    {
        $time = new \DateTimeImmutable();

        return $this->getTasks()->filter(
            static fn (EditionTask $element) => $taskName === $element->getType()
                && $time >= $element->getPeriodBegin()
                && $time < $element->getPeriodEnd()
        );
    }

    public function getAccessCodeStrategy(): ?AccessCodeStrategy
    {
        foreach ($this->getActiveTasks('application_system') as $task) {
            if ($strategy = $task->getAccessCodeStrategy()) {
                return $strategy;
            }
        }

        return null;
    }

    public function generateAlias(SluggerInterface $slugger): void
    {
        $this->alias = (string) $slugger->slug($this->getName() ?? uniqid())->lower();
    }

    public function findPreviousTask(string $type, ?string $applicationSystem = null): ?EditionTask
    {
        $criteria = Criteria::create()
            ->andWhere(Criteria::expr()->eq('type', $type))
            ->andWhere(Criteria::expr()->lt('periodEnd', new \DateTimeImmutable()))
            ->orderBy(['periodEnd' => 'DESC'])
            ->setMaxResults(1)
        ;

        if (null !== $applicationSystem) {
            $criteria->andWhere(Criteria::expr()->eq('applicationSystem', $applicationSystem));
        }

        return $this->tasks->matching($criteria)->first() ?: null;
    }

    public function findCurrentTask(string $type, ?string $applicationSystem = null): ?EditionTask
    {
        $criteria = Criteria::create()
            ->andWhere(Criteria::expr()->eq('type', $type))
            ->andWhere(Criteria::expr()->lt('periodBegin', new \DateTimeImmutable()))
            ->andWhere(Criteria::expr()->gt('periodEnd', new \DateTimeImmutable()))
            ->setMaxResults(1)
        ;

        if (null !== $applicationSystem) {
            $criteria->andWhere(Criteria::expr()->eq('applicationSystem', $applicationSystem));
        }

        return $this->tasks->matching($criteria)->first() ?: null;
    }

    public function findUpcomingTask(string $type, ?string $applicationSystem = null): ?EditionTask
    {
        $criteria = Criteria::create()
            ->andWhere(Criteria::expr()->eq('type', $type))
            ->andWhere(Criteria::expr()->gt('periodBegin', new \DateTimeImmutable()))
            ->orderBy(['periodBegin' => 'ASC'])
            ->setMaxResults(1)
        ;

        if (null !== $applicationSystem) {
            $criteria->andWhere(Criteria::expr()->eq('applicationSystem', $applicationSystem));
        }

        return $this->tasks->matching($criteria)->first() ?: null;
    }

    public function getPayments(): ?string
    {
        return $this->payments;
    }

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

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

    public function setUsePayments(bool $usePayments): void
    {
        $this->usePayments = $usePayments;
    }

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

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