<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\CalendarBundle\Entity;

use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Mapping\ClassMetadata;

#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['chill_calendar_calendar' => Calendar::class])]
#[ORM\Entity]
#[ORM\Table(name: 'chill_calendar.calendar')]
#[ORM\UniqueConstraint(name: 'idx_calendar_remote', columns: ['remoteId'], options: ['where' => "remoteId <> ''"])]
class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCentersInterface
{
    use RemoteCalendarTrait;

    use TrackCreationTrait;

    use TrackUpdateTrait;

    final public const SMS_CANCEL_PENDING = 'sms_cancel_pending';

    final public const SMS_PENDING = 'sms_pending';

    final public const SMS_SENT = 'sms_sent';

    final public const STATUS_CANCELED = 'canceled';

    /**
     * @deprecated
     */
    final public const STATUS_MOVED = 'moved';

    final public const STATUS_VALID = 'valid';

    /**
     * a list of invite which have been added during this session.
     *
     * @var array|Invite[]
     */
    public array $newInvites = [];

    /**
     * a list of invite which have been removed during this session.
     *
     * @var array|Invite[]
     */
    public array $oldInvites = [];

    public ?CalendarRange $previousCalendarRange = null;

    public ?User $previousMainUser = null;

    #[Serializer\Groups(['calendar:read', 'read'])]
    #[ORM\ManyToOne(targetEntity: AccompanyingPeriod::class, inversedBy: 'calendars')]
    private ?AccompanyingPeriod $accompanyingPeriod = null;

    #[ORM\ManyToOne(targetEntity: Activity::class)]
    private ?Activity $activity = null;

    #[Serializer\Groups(['calendar:read', 'read'])]
    #[ORM\OneToOne(targetEntity: CalendarRange::class, inversedBy: 'calendar')]
    private ?CalendarRange $calendarRange = null;

    #[ORM\ManyToOne(targetEntity: CancelReason::class)]
    private ?CancelReason $cancelReason = null;

    #[Serializer\Groups(['calendar:read', 'read', 'docgen:read'])]
    #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')]
    private CommentEmbeddable $comment;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false, options: ['default' => 0])]
    private int $dateTimeVersion = 0;

    /**
     * @var Collection<int, CalendarDoc>
     */
    #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
    private Collection $documents;

    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[Assert\NotNull(message: 'calendar.An end date is required')]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
    private ?\DateTimeImmutable $endDate = null;

    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
    private ?int $id = null;

    /**
     * @var Collection<int, Invite>&Selectable
     */
    #[Serializer\Groups(['read', 'docgen:read'])]
    #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
    #[ORM\JoinTable(name: 'chill_calendar.calendar_to_invites')]
    private Collection&Selectable $invites;

    #[Serializer\Groups(['read', 'docgen:read'])]
    #[Assert\NotNull(message: 'calendar.A location is required')]
    #[ORM\ManyToOne(targetEntity: Location::class)]
    private ?Location $location = null;

    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[Assert\NotNull(message: 'calendar.A main user is mandatory')]
    #[ORM\ManyToOne(targetEntity: User::class)]
    #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])]
    private ?User $mainUser = null;

    #[ORM\ManyToOne(targetEntity: Person::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?Person $person = null;

    /**
     * @var Collection<int, Person>
     */
    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
    #[ORM\ManyToMany(targetEntity: Person::class, inversedBy: 'calendars')]
    #[ORM\JoinTable(name: 'chill_calendar.calendar_to_persons')]
    #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])]
    private Collection $persons;

    #[Serializer\Groups(['calendar:read'])]
    #[ORM\Embedded(class: PrivateCommentEmbeddable::class, columnPrefix: 'privateComment_')]
    private PrivateCommentEmbeddable $privateComment;

    /**
     * @var Collection<int, ThirdParty>
     */
    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[ORM\ManyToMany(targetEntity: ThirdParty::class)]
    #[ORM\JoinTable(name: 'chill_calendar.calendar_to_thirdparties')]
    #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])]
    private Collection $professionals;

    #[Serializer\Groups(['docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
    private ?bool $sendSMS = false;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => Calendar::SMS_PENDING])]
    private string $smsStatus = self::SMS_PENDING;

    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
    #[Assert\NotNull(message: 'calendar.A start date is required')]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
    #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])]
    private ?\DateTimeImmutable $startDate = null;

    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: false, options: ['default' => 'valid'])]
    #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])]
    private string $status = self::STATUS_VALID;

    #[Serializer\Groups(['docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
    private ?bool $urgent = false;

    public function __construct()
    {
        $this->comment = new CommentEmbeddable();
        $this->documents = new ArrayCollection();
        $this->privateComment = new PrivateCommentEmbeddable();
        $this->persons = new ArrayCollection();
        $this->professionals = new ArrayCollection();
        $this->invites = new ArrayCollection();
    }

    /**
     * @internal use @{CalendarDoc::__construct} instead
     */
    public function addDocument(CalendarDoc $calendarDoc): self
    {
        if ($this->documents->contains($calendarDoc)) {
            $this->documents[] = $calendarDoc;
        }

        return $this;
    }

    /**
     * @internal Use {@link (Calendar::addUser)} instead
     */
    public function addInvite(Invite $invite): self
    {
        if ($invite->getCalendar() instanceof Calendar && $invite->getCalendar() !== $this) {
            throw new \LogicException('Not allowed to move an invitation to another Calendar');
        }

        $this->invites[] = $invite;
        $this->newInvites[] = $invite;

        $invite->setCalendar($this);

        return $this;
    }

    public function addPerson(Person $person): self
    {
        $this->persons[] = $person;

        return $this;
    }

    public function addProfessional(ThirdParty $professional): self
    {
        $this->professionals[] = $professional;

        return $this;
    }

    public function addUser(User $user): self
    {
        if (!$this->getUsers()->contains($user) && $this->getMainUser() !== $user) {
            $this->addInvite((new Invite())->setUser($user));
        }

        return $this;
    }

    public function getAccompanyingPeriod(): ?AccompanyingPeriod
    {
        return $this->accompanyingPeriod;
    }

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

    public function getCalendarRange(): ?CalendarRange
    {
        return $this->calendarRange;
    }

    public function getCancelReason(): ?CancelReason
    {
        return $this->cancelReason;
    }

    public function isCanceled(): bool
    {
        return null !== $this->cancelReason;
    }

    public function getCenters(): ?iterable
    {
        return match ($this->getContext()) {
            'person' => [$this->getPerson()->getCenter()],
            'accompanying_period' => $this->getAccompanyingPeriod()->getCenters(),
            default => throw new \LogicException('context not supported: '.$this->getContext()),
        };
    }

    public function getComment(): CommentEmbeddable
    {
        return $this->comment;
    }

    /**
     * @return 'person'|'accompanying_period'|null
     */
    public function getContext(): ?string
    {
        if (null !== $this->getAccompanyingPeriod()) {
            return 'accompanying_period';
        }

        if (null !== $this->getPerson()) {
            return 'person';
        }

        return null;
    }

    /**
     * Each time the date and time is update, this version is incremented.
     */
    public function getDateTimeVersion(): int
    {
        return $this->dateTimeVersion;
    }

    public function getDocuments(): Collection
    {
        return $this->documents;
    }

    #[Serializer\Groups(['docgen:read'])]
    public function getDuration(): ?\DateInterval
    {
        if (null === $this->getStartDate() || null === $this->getEndDate()) {
            return null;
        }

        return $this->getStartDate()->diff($this->getEndDate());
    }

    public function getEndDate(): ?\DateTimeImmutable
    {
        return $this->endDate;
    }

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

    public function getInviteForUser(User $user): ?Invite
    {
        $criteria = Criteria::create();
        $criteria->where(Criteria::expr()->eq('user', $user));

        $matchings = $this->invites
            ->matching($criteria);

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

        return null;
    }

    /**
     * @return Collection|Invite[]
     */
    public function getInvites(): Collection
    {
        return $this->invites;
    }

    public function getLocation(): ?Location
    {
        return $this->location;
    }

    public function getMainUser(): ?User
    {
        return $this->mainUser;
    }

    public function getPerson(): ?Person
    {
        return $this->person;
    }

    /**
     * @return Collection|Person[]
     */
    public function getPersons(): Collection
    {
        return $this->persons;
    }

    public function getPersonsAssociated(): array
    {
        if (null !== $this->accompanyingPeriod) {
            $personsAssociated = [];

            foreach ($this->accompanyingPeriod->getParticipations() as $participation) {
                if ($this->persons->contains($participation->getPerson())) {
                    $personsAssociated[] = $participation->getPerson();
                }
            }

            return $personsAssociated;
        }

        return [];
    }

    public function getPersonsNotAssociated(): array
    {
        if (null !== $this->accompanyingPeriod) {
            $personsNotAssociated = [];

            foreach ($this->persons as $person) {
                if (!\in_array($person, $this->getPersonsAssociated(), true)) {
                    $personsNotAssociated[] = $person;
                }
            }

            return $personsNotAssociated;
        }

        return [];
    }

    public function getPrivateComment(): PrivateCommentEmbeddable
    {
        return $this->privateComment;
    }

    /**
     * @return Collection|ThirdParty[]
     */
    public function getProfessionals(): Collection
    {
        return $this->professionals;
    }

    public function getSendSMS(): ?bool
    {
        return $this->sendSMS;
    }

    public function getSmsStatus(): string
    {
        return $this->smsStatus;
    }

    public function getStartDate(): ?\DateTimeImmutable
    {
        return $this->startDate;
    }

    /**
     * get the date of the calendar.
     *
     * Useful for showing the date of the calendar event, required by twig in some places.
     */
    public function getDate(): ?\DateTimeImmutable
    {
        return $this->getStartDate();
    }

    public function getStatus(): ?string
    {
        return $this->status;
    }

    public function getThirdParties(): Collection
    {
        return $this->getProfessionals();
    }

    public function getUrgent(): ?bool
    {
        return $this->urgent;
    }

    /**
     * @return ReadableCollection<(int|string), User>
     */
    #[Serializer\Groups(['calendar:read', 'read'])]
    public function getUsers(): ReadableCollection
    {
        return $this->getInvites()->map(static fn (Invite $i) => $i->getUser());
    }

    public function hasCalendarRange(): bool
    {
        return null !== $this->calendarRange;
    }

    public function hasLocation(): bool
    {
        return null !== $this->getLocation();
    }

    /**
     * return true if the user is invited.
     */
    public function isInvited(User $user): bool
    {
        if ($this->getMainUser() === $user) {
            return false;
        }

        return $this->getUsers()->contains($user);
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    {
        $metadata->addPropertyConstraint('startDate', new NotBlank());
        $metadata->addPropertyConstraint('startDate', new Range([
            'min' => '2 years ago',
            'max' => '+ 2 years',
        ]));
        $metadata->addPropertyConstraint('endDate', new NotBlank());
        $metadata->addPropertyConstraint('endDate', new Range([
            'min' => '2 years ago',
            'max' => '+ 2 years',
        ]));
    }

    /**
     * @internal use @{CalendarDoc::setCalendar} with null instead
     */
    public function removeDocument(CalendarDoc $calendarDoc): self
    {
        if ($calendarDoc->getCalendar() !== $this) {
            throw new \LogicException('cannot remove document of another calendar');
        }

        return $this;
    }

    /**
     * @internal Use {@link (Calendar::removeUser)} instead
     */
    public function removeInvite(Invite $invite): self
    {
        if ($this->invites->removeElement($invite)) {
            $invite->setCalendar(null);
            $this->oldInvites[] = $invite;
        }

        return $this;
    }

    public function removePerson(Person $person): self
    {
        $this->persons->removeElement($person);

        return $this;
    }

    public function removeProfessional(ThirdParty $professional): self
    {
        $this->professionals->removeElement($professional);

        return $this;
    }

    public function removeUser(User $user): self
    {
        if (!$this->getUsers()->contains($user)) {
            return $this;
        }

        $invite = $this->invites
            ->filter(static fn (Invite $invite) => $invite->getUser() === $user)
            ->first();
        $this->removeInvite($invite);

        return $this;
    }

    public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self
    {
        $this->accompanyingPeriod = $accompanyingPeriod;

        return $this;
    }

    public function setActivity(?Activity $activity): self
    {
        $this->activity = $activity;

        return $this;
    }

    public function setCalendarRange(?CalendarRange $calendarRange): self
    {
        if ($this->calendarRange !== $calendarRange) {
            $this->previousCalendarRange = $this->calendarRange;

            if (null !== $this->previousCalendarRange) {
                $this->previousCalendarRange->setCalendar(null);
            }
        }

        $this->calendarRange = $calendarRange;

        if ($this->calendarRange instanceof CalendarRange) {
            $this->calendarRange->setCalendar($this);
        }

        return $this;
    }

    public function setCancelReason(?CancelReason $cancelReason): self
    {
        $this->cancelReason = $cancelReason;

        return $this;
    }

    public function setComment(CommentEmbeddable $comment): self
    {
        $this->comment = $comment;

        return $this;
    }

    public function setEndDate(\DateTimeImmutable $endDate): self
    {
        if (null === $this->endDate || $this->endDate->getTimestamp() !== $endDate->getTimestamp()) {
            $this->increaseaDatetimeVersion();
        }

        $this->endDate = $endDate;

        return $this;
    }

    public function setLocation(?Location $location): Calendar
    {
        $this->location = $location;

        return $this;
    }

    public function setMainUser(?User $mainUser): self
    {
        if ($this->mainUser !== $mainUser) {
            $this->previousMainUser = $this->mainUser;
        }

        $this->mainUser = $mainUser;
        $this->removeUser($mainUser);

        return $this;
    }

    public function setPerson(?Person $person): Calendar
    {
        $this->person = $person;

        return $this;
    }

    public function setPrivateComment(PrivateCommentEmbeddable $privateComment): self
    {
        $this->privateComment = $privateComment;

        return $this;
    }

    public function setSendSMS(?bool $sendSMS): self
    {
        $this->sendSMS = $sendSMS;

        return $this;
    }

    public function setSmsStatus(string $smsStatus): self
    {
        $this->smsStatus = $smsStatus;

        return $this;
    }

    public function setStartDate(\DateTimeImmutable $startDate): self
    {
        if (null === $this->startDate || $this->startDate->getTimestamp() !== $startDate->getTimestamp()) {
            $this->increaseaDatetimeVersion();
        }

        $this->startDate = $startDate;

        return $this;
    }

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

        if (self::STATUS_CANCELED === $status && self::SMS_SENT === $this->getSmsStatus()) {
            $this->setSmsStatus(self::SMS_CANCEL_PENDING);
        }

        return $this;
    }

    public function setUrgent(bool $urgent): self
    {
        $this->urgent = $urgent;

        return $this;
    }

    private function increaseaDatetimeVersion(): void
    {
        ++$this->dateTimeVersion;
    }
}
