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

use Chill\CalendarBundle\Entity\Calendar;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodStepHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ConfidentialCourseMustHaveReferrer;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\LocationValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ResourceDuplicateCheck;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\GroupSequenceProviderInterface;
use UnexpectedValueException;

/**
 * AccompanyingPeriod Class.
 */
#[DiscriminatorMap(typeProperty: 'type', mapping: ['accompanying_period' => AccompanyingPeriod::class])]
#[Assert\GroupSequenceProvider]
#[ORM\Entity]
#[ORM\Table(name: 'chill_person_accompanying_period')]
#[AccompanyingPeriodValidity(groups: [AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED])]
#[LocationValidity(groups: [AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED])]
#[ConfidentialCourseMustHaveReferrer(groups: [AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED])]
class AccompanyingPeriod implements
    GroupSequenceProviderInterface,
    HasCentersInterface,
    HasScopesInterface,
    TrackCreationInterface,
    TrackUpdateInterface
{
    final public const INTENSITIES = [self::INTENSITY_OCCASIONAL, self::INTENSITY_REGULAR];

    /**
     * Mark an accompanying period as "occasional".
     *
     * used in INTENSITY
     */
    final public const INTENSITY_OCCASIONAL = 'occasional';

    /**
     * Mark an accompanying period as "regular".
     *
     * used in INTENSITY
     */
    final public const INTENSITY_REGULAR = 'regular';

    /**
     * Mark an accompanying period as "closed".
     *
     * This means that the accompanying period **is**
     * closed by the creator
     */
    final public const STEP_CLOSED = 'CLOSED';

    /**
     * Mark an accompanying period as "confirmed".
     *
     * This means that the accompanying period **is**
     * confirmed by the creator
     */
    final public const STEP_CONFIRMED = 'CONFIRMED';

    /**
     * Mark an accompanying period as confirmed, but inactive.
     *
     * this means that the accompanying period **is**
     * confirmed, but no activity (Activity, AccompanyingPeriod, ...)
     * has been associated, or updated, within this accompanying period.
     */
    final public const STEP_CONFIRMED_INACTIVE_SHORT = 'CONFIRMED_INACTIVE_SHORT';

    /**
     * Mark an accompanying period as confirmed, but inactive.
     *
     * this means that the accompanying period **is**
     * confirmed, but no activity (Activity, AccompanyingPeriod, ...)
     * has been associated, or updated, within this accompanying period.
     */
    final public const STEP_CONFIRMED_INACTIVE_LONG = 'CONFIRMED_INACTIVE_LONG';

    /**
     * Mark an accompanying period as "draft".
     *
     * This means that the accompanying period is not yet
     * confirmed by the creator
     */
    final public const STEP_DRAFT = 'DRAFT';

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

    #[Groups(['read', 'write'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[ORM\ManyToOne(targetEntity: Location::class)]
    private ?Location $administrativeLocation = null;

    /**
     * @var Collection<int, Calendar>&Selectable
     */
    #[ORM\OneToMany(mappedBy: 'accompanyingPeriod', targetEntity: Calendar::class)]
    private Collection&Selectable $calendars;

    #[Groups(['read', 'write', 'docgen:read'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CLOSED])]
    #[Assert\GreaterThanOrEqual(propertyPath: 'openingDate', message: 'The closing date must be later than the date of creation', groups: [AccompanyingPeriod::STEP_CLOSED])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE, nullable: true)]
    private ?\DateTime $closingDate = null;

    #[Groups(['read', 'write'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CLOSED])]
    #[ORM\ManyToOne(targetEntity: ClosingMotive::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?ClosingMotive $closingMotive = null;

    /**
     * @var Collection<int, Comment>
     */
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_DRAFT])]
    #[ORM\OneToMany(mappedBy: 'accompanyingPeriod', targetEntity: Comment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['createdAt' => Criteria::DESC, 'id' => 'DESC'])]
    private Collection $comments;

    #[Groups(['read', 'write', 'docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $confidential = false;

    #[Groups(['docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])]
    private ?\DateTimeInterface $createdAt = null;

    #[Groups(['read', 'docgen:read'])]
    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?User $createdBy = null;

    #[Groups(['read', 'write', 'docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $emergency = false;

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

    #[Groups(['read'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, nullable: true)]
    private ?string $intensity = self::INTENSITY_OCCASIONAL;

    #[Groups(['read', 'write'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[ORM\ManyToOne(targetEntity: UserJob::class)]
    private ?UserJob $job = null;

    /**
     * @var Collection<int, AccompanyingPeriodLocationHistory>
     */
    #[ORM\OneToMany(mappedBy: 'period', targetEntity: AccompanyingPeriodLocationHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $locationHistories;

    #[Groups(['read', 'write', 'docgen:read'])]
    #[Assert\LessThan(value: 'tomorrow', groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[Assert\LessThanOrEqual(propertyPath: 'closingDate', groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE)]
    private ?\DateTime $openingDate = null;

    #[Groups(['read', 'write'])]
    #[Assert\NotBlank(groups: [AccompanyingPeriod::STEP_CONFIRMED])]
    #[ORM\ManyToOne(targetEntity: Origin::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?Origin $origin = null;

    /**
     * @var Collection<int, AccompanyingPeriodParticipation>
     */
    #[Groups(['read', 'docgen:read'])]
    #[ORM\OneToMany(mappedBy: 'accompanyingPeriod', targetEntity: AccompanyingPeriodParticipation::class, cascade: ['persist', 'refresh', 'remove', 'merge', 'detach'], orphanRemoval: true)]
    #[ParticipationOverlap(groups: [AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED])]
    private Collection $participations;

    #[ORM\ManyToOne(targetEntity: Person::class, inversedBy: 'periodLocatedOn')]
    private ?Person $personLocation = null;

    #[Groups(['read'])]
    #[ORM\ManyToOne(targetEntity: Comment::class, cascade: ['persist'])]
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
    private ?Comment $pinnedComment = null;

    private bool $preventUserIsChangedNotification = false;

    #[Groups(['read', 'write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
    private string $remark = '';

    #[Groups(['read', 'write', 'docgen:read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $requestorAnonymous = false;

    #[ORM\ManyToOne(targetEntity: Person::class, inversedBy: 'accompanyingPeriodRequested')]
    #[ORM\JoinColumn(nullable: true)]
    private ?Person $requestorPerson = null;

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

    /**
     * @var Collection<int, AccompanyingPeriod\Resource>
     */
    #[Groups(['read', 'docgen:read'])]
    #[ORM\OneToMany(targetEntity: AccompanyingPeriod\Resource::class, mappedBy: 'accompanyingPeriod', cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ResourceDuplicateCheck(groups: [AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED, 'Default', 'default'])]
    private Collection $resources;

    /**
     * @var Collection<int, Scope>
     */
    #[Groups(['read', 'docgen:read'])]
    #[Assert\Count(min: 1, groups: [AccompanyingPeriod::STEP_CONFIRMED], minMessage: 'A course must be associated to at least one scope')]
    #[ORM\ManyToMany(targetEntity: Scope::class, cascade: [])]
    #[ORM\JoinTable(name: 'accompanying_periods_scopes', joinColumns: [new ORM\JoinColumn(name: 'accompanying_period_id', referencedColumnName: 'id')], inverseJoinColumns: [new ORM\JoinColumn(name: 'scope_id', referencedColumnName: 'id')])]
    private Collection $scopes;

    /**
     * @var Collection<int, SocialIssue>
     */
    #[Groups(['read', 'docgen:read'])]
    #[Assert\Count(min: 1, groups: [AccompanyingPeriod::STEP_CONFIRMED], minMessage: 'A course must contains at least one social issue')]
    #[ORM\ManyToMany(targetEntity: SocialIssue::class)]
    #[ORM\JoinTable(name: 'chill_person_accompanying_period_social_issues')]
    private Collection $socialIssues;

    /**
     * @var AccompanyingPeriod::STEP_*
     */
    #[Groups(['read'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 32, nullable: true)]
    private ?string $step = self::STEP_DRAFT;

    /**
     * @var Collection<int, AccompanyingPeriodStepHistory>
     */
    #[ORM\OneToMany(mappedBy: 'period', targetEntity: AccompanyingPeriodStepHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $stepHistories;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])]
    private ?\DateTimeInterface $updatedAt = null;

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

    #[Groups(['read', 'write', 'docgen:read'])]
    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?User $user = null;

    /**
     * @var Collection<int, UserHistory>
     */
    #[ORM\OneToMany(targetEntity: UserHistory::class, mappedBy: 'accompanyingPeriod', orphanRemoval: true, cascade: ['persist', 'remove'])]
    private Collection $userHistories;

    private bool $userIsChanged = false;

    /**
     * Temporary field, which is filled when the user is changed.
     *
     * Used internally for listener when user change
     */
    private ?User $userPrevious = null;

    /**
     * @var Collection<int, AccompanyingPeriodWork>
     */
    #[Assert\Valid(traverse: true)]
    #[ORM\OneToMany(mappedBy: 'accompanyingPeriod', targetEntity: AccompanyingPeriodWork::class)]
    private Collection $works;

    /**
     * AccompanyingPeriod constructor.
     *
     * @uses AccompanyingPeriod::setClosingDate()
     */
    public function __construct(?\DateTime $dateOpening = null)
    {
        $this->calendars = new ArrayCollection(); // TODO we cannot add a dependency between AccompanyingPeriod and calendars
        $this->participations = new ArrayCollection();
        $this->scopes = new ArrayCollection();
        $this->socialIssues = new ArrayCollection();
        $this->comments = new ArrayCollection();
        $this->works = new ArrayCollection();
        $this->resources = new ArrayCollection();
        $this->userHistories = new ArrayCollection();
        $this->locationHistories = new ArrayCollection();
        $this->stepHistories = new ArrayCollection();
        $this->setOpeningDate($dateOpening ?? new \DateTime('now'));
    }

    /**
     *  Return an array with open participations sorted by household
     * [
     *   [
     *     "household" => Household x,
     *     "members" => [
     *        Participation y , Participation z, ...
     *      ]
     *   ],
     * ].
     */
    public function actualParticipationsByHousehold(): array
    {
        $participations = $this->getOpenParticipations()->toArray();

        $households = [];

        foreach ($participations as $p) {
            $households[] = $p->getPerson()->getCurrentHousehold();
        }
        $households = array_unique($households, \SORT_REGULAR);

        $array = [];

        foreach ($households as $household) {
            $members = [];

            foreach ($participations as $p) {
                if ($p->getPerson()->getCurrentHousehold() === $household) {
                    $members[] = array_shift($participations);
                } else {
                    $participations[] = array_shift($participations);
                }
            }
            $array[] = ['household' => $household, 'members' => $members];
        }

        return $array;
    }

    public function addComment(Comment $comment): self
    {
        $this->comments[] = $comment;
        $comment->setAccompanyingPeriod($this);

        return $this;
    }

    public function addLocationHistory(AccompanyingPeriodLocationHistory $history): self
    {
        if (self::STEP_DRAFT === $this->getStep()) {
            return $this;
        }

        if (!$this->locationHistories->contains($history)) {
            $this->locationHistories[] = $history;
            $history->setPeriod($this);
        }

        // ensure continuity of histories
        $criteria = new Criteria();
        $criteria->orderBy(['startDate' => Order::Ascending, 'id' => Order::Ascending]);

        /** @var \Iterator $locations */
        $locations = $this->getLocationHistories()->matching($criteria)->getIterator();
        $locations->rewind();

        do {
            /** @var AccompanyingPeriodLocationHistory $current */
            $current = $locations->current();
            $locations->next();

            if ($locations->valid()) {
                $next = $locations->current();
                $current->setEndDate($next->getStartDate());
            }
        } while ($locations->valid());

        return $this;
    }

    public function addPerson(?Person $person = null): self
    {
        if (null !== $person) {
            $this->createParticipationFor($person);
        }

        return $this;
    }

    public function addResource(AccompanyingPeriod\Resource $resource): self
    {
        $resource->setAccompanyingPeriod($this);
        $this->resources[] = $resource;

        return $this;
    }

    public function addScope(Scope $scope): self
    {
        if (!$this->scopes->contains($scope)) {
            $this->scopes[] = $scope;
        }

        return $this;
    }

    public function addSocialIssue(SocialIssue $socialIssue): self
    {
        if (!$this->socialIssues->contains($socialIssue)) {
            $this->socialIssues[] = $socialIssue;
        }

        return $this;
    }

    public function addWork(AccompanyingPeriodWork $work): self
    {
        $this->works[] = $work;
        $work->setAccompanyingPeriod($this);

        return $this;
    }

    /**
     * If the period can be reopened.
     *
     * This function test if the period is closed and if the period is the last
     * for the given person
     */
    public function canBeReOpened(Person $person): bool
    {
        if (true === $this->isOpen()) {
            return false;
        }

        $participation = $this->getOpenParticipationContainsPerson($person);

        if (null === $participation) {
            return false;
        }

        $periods = $participation->getPerson()->getAccompanyingPeriodsOrdered();

        return end($periods) === $this;
    }

    /**
     * Close a participation for a person.
     *
     * Search for the person's participation and set the end date at
     * 'now'.
     */
    public function closeParticipationFor(mixed $person): ?AccompanyingPeriodParticipation
    {
        $participation = $this->getOpenParticipationContainsPerson($person);

        if ($participation instanceof AccompanyingPeriodParticipation) {
            $participation->setEndDate(new \DateTime('now'));
        }

        return $participation;
    }

    /**
     * Return true if the accompanying period contains a person.
     *
     * **Note**: this participation can be opened or not.
     */
    public function containsPerson(Person $person): bool
    {
        return $this->getParticipationsContainsPerson($person)->count() > 0;
    }

    public function getOpenWorks(): Collection
    {
        return $this->getWorks()->filter(
            static fn (AccompanyingPeriodWork $work): bool => null === $work->getEndDate()
                    or $work->getEndDate() > new \DateTimeImmutable('today')
        );
    }

    /**
     * Open a new participation for a person.
     */
    public function createParticipationFor(Person $person): AccompanyingPeriodParticipation
    {
        $participation = new AccompanyingPeriodParticipation($this, $person);
        $this->participations[] = $participation;

        return $participation;
    }

    public function getAddressLocation(): ?Address
    {
        return $this->addressLocation;
    }

    public function getAdministrativeLocation(): ?Location
    {
        return $this->administrativeLocation;
    }

    /**
     * Get a list of person which have an adresse available for a valid location.
     *
     * @return ReadableCollection<(int|string), Person>
     */
    public function getAvailablePersonLocation(): ReadableCollection
    {
        return $this->getOpenParticipations()
            ->filter(
                static fn (AccompanyingPeriodParticipation $p): bool => $p->getPerson()->hasCurrentHouseholdAddress()
            )
            ->map(
                static fn (AccompanyingPeriodParticipation $p): ?Person => $p->getPerson()
            );
    }

    public function getCalendars(): Collection
    {
        return $this->calendars;
    }

    public function getCenter(): ?Center
    {
        if (0 === $this->getPersons()->count()) {
            return null;
        }

        return $this->getPersons()->first()->getCenter();
    }

    public function getCenters(): ?iterable
    {
        $centers = [];

        foreach ($this->getPersons() as $person) {
            if (
                null !== $person->getCenter()
                && !\array_key_exists(spl_object_hash($person->getCenter()), $centers)
            ) {
                $centers[spl_object_hash($person->getCenter())] = $person->getCenter();
            }
        }

        return array_values($centers);
    }

    /**
     * Get closingDate.
     */
    public function getClosingDate(): ?\DateTime
    {
        return $this->closingDate;
    }

    public function getClosingMotive(): ?ClosingMotive
    {
        return $this->closingMotive;
    }

    /**
     * @return ReadableCollection<int, Comment>
     */
    #[Groups(['read'])]
    public function getComments(): ReadableCollection
    {
        $pinnedComment = $this->pinnedComment;

        return $this
            ->comments
            ->filter(
                static fn (Comment $c): bool => $c !== $pinnedComment
            )
        ;
    }

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

    public function getCreatedBy(): ?User
    {
        return $this->createdBy;
    }

    #[Groups(['docgen:read'])]
    public function getCurrentParticipations(): ReadableCollection
    {
        return $this->getOpenParticipations();
    }

    public function getGroupSequence()
    {
        if (self::STEP_DRAFT === $this->getStep()) {
            return [[self::STEP_DRAFT]];
        }
        if (str_starts_with((string) $this->getStep(), 'CONFIRM')) {
            return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
        }
        if (self::STEP_CLOSED === $this->getStep()) {
            return [[self::STEP_DRAFT, self::STEP_CONFIRMED, self::STEP_CLOSED]];
        }

        throw new \LogicException('no validation group permitted with this step: '.$this->getStep());
    }

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

    public function getIntensity(): ?string
    {
        return $this->intensity;
    }

    public function getJob(): ?UserJob
    {
        return $this->job;
    }

    public function getLastLocationHistory(): ?AccompanyingPeriodLocationHistory
    {
        foreach ($this->getLocationHistories() as $locationHistory) {
            if (null === $locationHistory->getEndDate()) {
                return $locationHistory;
            }
        }

        return null;
    }

    /**
     * Get the location, taking precedence into account.
     */
    #[Groups(['read'])]
    public function getLocation(?\DateTimeImmutable $at = null): ?Address
    {
        if ($this->getPersonLocation() instanceof Person) {
            return $this->getPersonLocation()->getCurrentPersonAddress();
        }

        return $this->getAddressLocation();
    }

    /**
     * @return Collection<AccompanyingPeriodLocationHistory>
     */
    public function getLocationHistories(): Collection
    {
        return $this->locationHistories;
    }

    /**
     * Get where the location is.
     *
     * @return 'person'|'address'|'none'
     */
    #[Groups(['read'])]
    public function getLocationStatus(): string
    {
        if ($this->getPersonLocation() instanceof Person) {
            return 'person';
        }

        if ($this->getAddressLocation() instanceof Address) {
            return 'address';
        }

        return 'none';
    }

    public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection
    {
        $today = new \DateTimeImmutable('today');

        $criteria = Criteria::create();
        $expr = Criteria::expr();

        $criteria
            ->where(
                $expr->gte('startDate', $today),
            )
            ->orderBy(['startDate' => 'ASC']);

        $criteriaByPerson = Criteria::create();
        $criteriaByPerson
            ->where(
                $expr->memberOf('persons', $person)
            )
            ->setMaxResults($limit);

        return $this->calendars->matching($criteria)->matching($criteriaByPerson);
    }

    /**
     * Get openingDate.
     */
    public function getOpeningDate(): ?\DateTime
    {
        return $this->openingDate;
    }

    /**
     * Get the opened participation containing a person.
     *
     * "Open" means that the closed date is NULL
     */
    public function getOpenParticipationContainsPerson(Person $person): ?AccompanyingPeriodParticipation
    {
        $collection = $this
            ->getParticipationsContainsPerson($person)
            ->filter(
                static fn (AccompanyingPeriodParticipation $participation): bool => null === $participation->getEndDate()
            );

        return $collection->count() > 0 ? $collection->first() : null;
    }

    /**
     * @return ReadableCollection<(int|string), AccompanyingPeriodParticipation>
     */
    public function getOpenParticipations(): ReadableCollection
    {
        return $this
            ->getParticipations()
            ->filter(
                static fn (AccompanyingPeriodParticipation $participation): bool => null === $participation->getEndDate()
            );
    }

    public function getOrigin(): ?Origin
    {
        return $this->origin;
    }

    /**
     * Get Participations Collection.
     *
     * @return Collection<AccompanyingPeriodParticipation>
     */
    public function getParticipations(): Collection
    {
        return $this->participations;
    }

    /**
     * Get the participation containing a person.
     *
     * @return ReadableCollection<(int|string), AccompanyingPeriodParticipation>
     */
    public function getParticipationsContainsPerson(Person $person): ReadableCollection
    {
        return $this
            ->getParticipations()
            ->filter(
                static fn (AccompanyingPeriodParticipation $participation): bool => $participation->getPerson() === $person
            );
    }

    #[Groups(['read'])]
    public function getPersonLocation(): ?Person
    {
        return $this->personLocation;
    }

    /**
     * Get a list of all persons which are participating to this course.
     *
     * @psalm-return Collection<int, Person|null>
     */
    public function getPersons(): Collection
    {
        return $this
            ->participations
            ->map(
                static fn (AccompanyingPeriodParticipation $participation): ?Person => $participation->getPerson()
            );
    }

    #[Groups(['read'])]
    public function getPinnedComment(): ?Comment
    {
        return $this->pinnedComment;
    }

    public function getPreviousUser(): ?User
    {
        return $this->userPrevious;
    }

    /**
     * @return Collection|SocialAction[] All the descendant social actions of all
     *                                   the descendants of the entity
     */
    public function getRecursiveSocialActions(): Collection
    {
        $recursiveSocialActions = new ArrayCollection();

        foreach ($this->socialIssues as $socialIssue) {
            foreach ($socialIssue->getRecursiveSocialActions() as $descendant) {
                if (!$recursiveSocialActions->contains($descendant)) {
                    $recursiveSocialActions->add($descendant);
                }
            }
        }

        return $recursiveSocialActions;
    }

    /**
     * @return Collection|SocialIssues[] All social issues and their descendants
     */
    public function getRecursiveSocialIssues(): Collection
    {
        $recursiveSocialIssues = new ArrayCollection();

        foreach ($this->socialIssues as $socialIssue) {
            foreach ($socialIssue->getDescendantsWithThis() as $descendant) {
                if (!$recursiveSocialIssues->contains($descendant)) {
                    $recursiveSocialIssues->add($descendant);
                }
            }
        }

        return $recursiveSocialIssues;
    }

    public function getRemark(): string
    {
        return $this->remark;
    }

    #[Groups(['read'])]
    public function getRequestor(): Person|ThirdParty|null
    {
        return $this->requestorPerson ?? $this->requestorThirdParty;
    }

    /**
     * @return string 'person' if requestor is an instanceof @see{Person::class}, 'thirdparty' if this is an instanceof @see{ThirdParty::class}, or 'none'
     */
    public function getRequestorKind(): string
    {
        if ($this->getRequestor() instanceof ThirdParty) {
            return 'thirdparty';
        }

        if ($this->getRequestor() instanceof Person) {
            return 'person';
        }

        return 'none';
    }

    public function getRequestorPerson(): ?Person
    {
        return $this->requestorPerson;
    }

    public function getRequestorThirdParty(): ?ThirdParty
    {
        return $this->requestorThirdParty;
    }

    /**
     * @return Collection<int, AccompanyingPeriod\Resource>
     */
    public function getResources(): Collection
    {
        return $this->resources;
    }

    /**
     * @return Collection|iterable
     */
    public function getScopes(): Collection
    {
        return $this->scopes;
    }

    public function getSocialIssues(): Collection
    {
        return $this->socialIssues;
    }

    public function getStep(): ?string
    {
        return $this->step;
    }

    public function getStepHistories(): Collection
    {
        return $this->stepHistories;
    }

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

    /**
     * @return AccompanyingPeriodWork[]
     */
    public function getWorks(): Collection
    {
        return $this->works;
    }

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

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

    public function isChangedUser(): bool
    {
        return $this->userIsChanged && $this->user !== $this->userPrevious;
    }

    /**
     * Returns true if the closing date is after the opening date.
     */
    public function isClosingAfterOpening(): bool
    {
        if (null === $this->getClosingDate()) {
            return false;
        }
        $diff = $this->getOpeningDate()->diff($this->getClosingDate());

        if (0 === $diff->invert) {
            return true;
        }

        return false;
    }

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

    /**
     * Validation functions.
     */
    public function isDateConsistent(ExecutionContextInterface $context)
    {
        if ($this->isOpen()) {
            return;
        }

        if (!$this->isClosingAfterOpening()) {
            $context->buildViolation('The date of closing is before the date of opening')
                ->atPath('dateClosing')
                ->addViolation();
        }
    }

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

    public function isOpen(): bool
    {
        if ($this->getOpeningDate() > new \DateTimeImmutable('now')) {
            return false;
        }

        if (null === $this->getClosingDate()) {
            return true;
        }

        return false;
    }

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

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

    public function removeComment(Comment $comment): void
    {
        $comment->setAccompanyingPeriod(null);
        $this->comments->removeElement($comment);
    }

    public function removeLocationHistory(AccompanyingPeriodLocationHistory $history): self
    {
        if ($this->locationHistories->removeElement($history)) {
            $history->setPeriod(null);
        }

        return $this;
    }

    /**
     * Remove Participation.
     */
    public function removeParticipation(AccompanyingPeriodParticipation $participation)
    {
        $participation->setAccompanyingPeriod(null);
    }

    /**
     * Remove Person.
     */
    public function removePerson(Person $person): self
    {
        $this->closeParticipationFor($person);

        return $this;
    }

    public function removeResource(Resource $resource): void
    {
        $resource->setAccompanyingPeriod(null);
        $this->resources->removeElement($resource);
    }

    public function removeScope(Scope $scope): void
    {
        $this->scopes->removeElement($scope);
    }

    public function removeSocialIssue(SocialIssue $socialIssue): void
    {
        $this->socialIssues->removeElement($socialIssue);
    }

    public function removeWork(AccompanyingPeriodWork $work): self
    {
        $this->works->removeElement($work);
        $work->setAccompanyingPeriod(null);

        return $this;
    }

    public function reOpen(): void
    {
        $this->setClosingDate(null);
        $this->setClosingMotive(null);
        $this->setStep(AccompanyingPeriod::STEP_CONFIRMED);
    }

    public function resetPreviousUser(): self
    {
        $this->userPrevious = null;
        $this->userIsChanged = false;

        return $this;
    }

    #[Groups(['write'])]
    public function setAddressLocation(?Address $addressLocation = null): self
    {
        if ($this->addressLocation !== $addressLocation) {
            $this->addressLocation = $addressLocation;

            if (null !== $addressLocation) {
                $this->setPersonLocation(null);
                $locationHistory = new AccompanyingPeriodLocationHistory();
                $locationHistory
                    ->setStartDate(new \DateTimeImmutable('now'))
                    ->setAddressLocation($addressLocation);

                $this->addLocationHistory($locationHistory);
            }
        }

        return $this;
    }

    public function setAdministrativeLocation(?Location $administrativeLocation): AccompanyingPeriod
    {
        $this->administrativeLocation = $administrativeLocation;

        return $this;
    }

    /**
     * Set closingDate.
     *
     * For closing a Person file, you should use Person::setClosed instead.
     *
     * @return AccompanyingPeriod
     */
    public function setClosingDate(mixed $closingDate)
    {
        $this->closingDate = $closingDate;

        return $this;
    }

    public function setClosingMotive(?ClosingMotive $closingMotive = null): self
    {
        $this->closingMotive = $closingMotive;

        return $this;
    }

    public function setConfidential(bool $confidential): self
    {
        $this->confidential = $confidential;

        return $this;
    }

    public function setCreatedAt(\DateTimeInterface $datetime): self
    {
        $this->createdAt = $datetime;

        return $this;
    }

    public function setCreatedBy(User $createdBy): self
    {
        $this->createdBy = $createdBy;

        return $this;
    }

    public function setEmergency(bool $emergency): self
    {
        $this->emergency = $emergency;

        return $this;
    }

    public function setIntensity(string $intensity): self
    {
        $this->intensity = $intensity;

        return $this;
    }

    public function setJob(?UserJob $job): self
    {
        $this->job = $job;

        return $this;
    }

    /**
     * Set openingDate.
     *
     * @return AccompanyingPeriod
     */
    public function setOpeningDate(mixed $openingDate)
    {
        if ($this->openingDate !== $openingDate) {
            $this->openingDate = $openingDate;

            $this->ensureStepContinuity();
        }

        return $this;
    }

    public function setOrigin(Origin $origin): self
    {
        $this->origin = $origin;

        return $this;
    }

    #[Groups(['write'])]
    public function setPersonLocation(?Person $person = null): self
    {
        if ($this->personLocation !== $person) {
            $this->personLocation = $person;

            if (null !== $person) {
                $this->setAddressLocation(null);
                $locationHistory = new AccompanyingPeriodLocationHistory();
                $locationHistory
                    ->setStartDate(new \DateTimeImmutable('now'))
                    ->setPersonLocation($person);

                $this->addLocationHistory($locationHistory);
            }
        }

        return $this;
    }

    #[Groups(['write'])]
    public function setPinnedComment(?Comment $comment = null): self
    {
        if (null !== $this->pinnedComment) {
            $this->addComment($this->pinnedComment);
        }

        $this->pinnedComment = $comment;

        return $this;
    }

    public function setRemark(?string $remark = null): self
    {
        $this->remark = (string) $remark;

        return $this;
    }

    /**
     * Set a requestor.
     *
     * The requestor is either an instance of ThirdParty, or an
     * instance of Person
     *
     * @param $requestor Person|ThirdParty
     *
     * @throw UnexpectedValueException if the requestor is not a Person or ThirdParty
     */
    #[Groups(['write'])]
    public function setRequestor($requestor): self
    {
        if ($requestor instanceof Person) {
            $this->setRequestorThirdParty(null);
            $this->setRequestorPerson($requestor);
        } elseif ($requestor instanceof ThirdParty) {
            $this->setRequestorThirdParty($requestor);
            $this->setRequestorPerson(null);
        } elseif (null === $requestor) {
            $this->setRequestorPerson(null);
            $this->setRequestorThirdParty(null);
        } else {
            throw new \UnexpectedValueException('requestor is not an instance of Person or ThirdParty');
        }

        return $this;
    }

    public function setRequestorAnonymous(bool $requestorAnonymous): self
    {
        $this->requestorAnonymous = $requestorAnonymous;

        return $this;
    }

    public function setStep(string $step, array $context = []): self
    {
        $previous = $this->step;

        $this->step = $step;

        if (self::STEP_DRAFT === $previous && self::STEP_DRAFT !== $step) {
            $this->bootPeriod();
        }

        if (self::STEP_DRAFT !== $this->step && $previous !== $step) {
            // we create a new history
            $history = new AccompanyingPeriodStepHistory();
            $history->setStep($this->step)->setStartDate(new \DateTimeImmutable('now'));

            $this->addStepHistory($history, $context);
        }

        return $this;
    }

    public function setUpdatedAt(\DateTimeInterface $datetime): self
    {
        $this->updatedAt = $datetime;

        return $this;
    }

    public function setUpdatedBy(User $user): self
    {
        $this->updatedBy = $user;

        return $this;
    }

    public function setUser(?User $user, bool $preventNotification = false): self
    {
        if ($this->user !== $user) {
            $this->userPrevious = $this->user;
            $this->userIsChanged = true;
            $this->preventUserIsChangedNotification = $preventNotification;

            foreach ($this->userHistories as $history) {
                if (null === $history->getEndDate()) {
                    $history->setEndDate(new \DateTimeImmutable('now'));
                }
            }

            if (null !== $user) {
                $this->userHistories->add(new UserHistory($this, $user));
            }
        }

        $this->user = $user;

        return $this;
    }

    public function getUserHistories(): ReadableCollection
    {
        return $this->userHistories;
    }

    public function getCurrentUserHistory(): ?UserHistory
    {
        return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate());
    }

    private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self
    {
        if (!$this->stepHistories->contains($stepHistory)) {
            $this->stepHistories[] = $stepHistory;
            $stepHistory->setPeriod($this);
            if (($context['closing_motive'] ?? null) instanceof ClosingMotive) {
                $stepHistory->setClosingMotive($context['closing_motive']);
            }
            $this->ensureStepContinuity();
        }

        return $this;
    }

    private function bootPeriod(): void
    {
        // first location history
        $locationHistory = new AccompanyingPeriodLocationHistory();
        $locationHistory
            ->setStartDate(new \DateTimeImmutable('now'))
            ->setPersonLocation($this->getPersonLocation())
            ->setAddressLocation($this->getAddressLocation());
        $this->addLocationHistory($locationHistory);
    }

    private function ensureStepContinuity(): void
    {
        // ensure continuity of histories
        $criteria = new Criteria();
        $criteria->orderBy(['startDate' => Order::Ascending, 'id' => Order::Ascending]);

        /** @var \Iterator $steps */
        $steps = $this->getStepHistories()->matching($criteria)->getIterator();
        $steps->rewind();

        // we set the start date of the first step as the opening date, only if it is
        // not greater than the end date
        /** @var AccompanyingPeriodStepHistory $current */
        $current = $steps->current();

        if (null === $current) {
            return;
        }

        if ($this->getOpeningDate()->format('Y-m-d') !== $current->getStartDate()->format('Y-m-d')
            && ($this->getOpeningDate() <= $current->getEndDate() || null === $current->getEndDate())) {
            $current->setStartDate(\DateTimeImmutable::createFromMutable($this->getOpeningDate()));
        }

        // then we set all the end date to the start date of the next one
        do {
            /** @var AccompanyingPeriodStepHistory $current */
            $current = $steps->current();
            $steps->next();

            if ($steps->valid()) {
                $next = $steps->current();
                $current->setEndDate($next->getStartDate());
            }
        } while ($steps->valid());
    }

    private function setRequestorPerson(?Person $requestorPerson = null): self
    {
        $this->requestorPerson = $requestorPerson;

        return $this;
    }

    private function setRequestorThirdParty(?ThirdParty $requestorThirdParty = null): self
    {
        $this->requestorThirdParty = $requestorThirdParty;

        return $this;
    }
}
