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

use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class MembersEditor
{
    final public const VALIDATION_GROUP_AFFECTED = 'household_memberships';

    final public const VALIDATION_GROUP_COMPOSITION = 'household_composition';

    final public const VALIDATION_GROUP_CREATED = 'household_memberships_created';

    private array $events = [];

    private array $membershipsAffected = [];

    private array $oldMembershipsHashes = [];

    private array $persistables = [];

    public function __construct(
        private readonly ValidatorInterface $validator,
        private readonly ?Household $household,
        private readonly EventDispatcherInterface $eventDispatcher,
    ) {}

    /**
     * Add a person to the household.
     *
     * The person is added to the household associated with this editor's instance.
     *
     * If the person is also a member of another household, or the same household at the same position, the person
     * is not associated any more with the previous household.
     */
    public function addMovement(\DateTimeImmutable $date, Person $person, ?Position $position, ?bool $holder = false, ?string $comment = null): self
    {
        if (null === $this->household) {
            throw new \LogicException('You must define a household first');
        }

        $membership = (new HouseholdMember())
            ->setStartDate($date)
            ->setPerson($person)
            ->setPosition($position)
            ->setHolder($holder)
            ->setComment($comment);
        $this->household->addMember($membership);

        if ($membership->getShareHousehold()) {
            // launch event only if moving to a "share household" position,
            // and if the destination household is different than the previous one
            $event = new PersonAddressMoveEvent($person);
            $event->setNextMembership($membership);

            $counter = 0;

            foreach ($person->getHouseholdParticipationsShareHousehold() as $participation) {
                if ($participation === $membership) {
                    continue;
                }

                if ($participation->getStartDate() > $membership->getStartDate()) {
                    continue;
                }

                ++$counter;

                if (null === $participation->getEndDate() || $participation->getEndDate() > $date) {
                    $participation->setEndDate($date);
                    $this->membershipsAffected[] = $participation;
                    $this->oldMembershipsHashes[] = \spl_object_hash($participation);

                    if ($participation->getHousehold() !== $this->household) {
                        $event->setPreviousMembership($participation);
                        $this->events[] = $event;
                    }
                }
            }

            // send also the event if there was no participation before
            if (0 === $counter) {
                $this->events[] = $event;
            }

            foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) {
                if ($participation->getHousehold() === $this->household
                    && null === $participation->getEndDate() || $participation->getEndDate() > $membership->getStartDate()
                    && $participation->getStartDate() <= $membership->getStartDate()
                ) {
                    $participation->setEndDate($membership->getStartDate());
                }
            }
        } else {
            // if there are multiple belongings not sharing household, close the others
            foreach ($person->getHouseholdParticipations() as $participation) {
                if ($participation === $membership) {
                    continue;
                }

                if ($participation->getHousehold() === $this->household
                    && (null === $participation->getEndDate() || $participation->getEndDate() > $membership->getStartDate())
                    && $participation->getStartDate() <= $membership->getStartDate()
                ) {
                    if ($participation->getShareHousehold()) {
                        // if a members is moved to the same household than the one he belongs to,
                        // we should make it leave the household
                        $this->leaveMovement($date, $person);
                    } else {
                        $participation->setEndDate($membership->getStartDate());
                    }
                }
            }
        }

        $this->membershipsAffected[] = $membership;
        $this->persistables[] = $membership;

        return $this;
    }

    public function getHousehold(): ?Household
    {
        return $this->household;
    }

    public function getPersistable(): array
    {
        return $this->persistables;
    }

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

    /**
     * Makes a person leave the household.
     *
     * Makes a person leave the household **associated with this editor**.
     *
     * @return $this
     */
    public function leaveMovement(
        \DateTimeImmutable $date,
        Person $person,
    ): self {
        $criteria = new Criteria();
        $expr = Criteria::expr();

        $criteria->where(
            $expr->andX(
                $expr->lte('startDate', $date),
                $expr->isNull('endDate'),
                $expr->eq('shareHousehold', true)
            )
        );

        $participations = $person->getHouseholdParticipations()
            ->matching($criteria);

        foreach ($participations as $participation) {
            $this->events[] = $event = new PersonAddressMoveEvent($person);
            $event->setPreviousMembership($participation);
            $participation->setEndDate($date);
            $this->membershipsAffected[] = $participation;
        }

        return $this;
    }

    public function postMove(): void
    {
        foreach ($this->events as $event) {
            $this->eventDispatcher->dispatch($event);
        }
    }

    public function validate(): ConstraintViolationListInterface
    {
        if ($this->hasHousehold()) {
            $list = $this->validator
                ->validate($this->getHousehold(), null, [self::VALIDATION_GROUP_AFFECTED, self::VALIDATION_GROUP_COMPOSITION]);
        } else {
            $list = new ConstraintViolationList();
        }

        foreach ($this->membershipsAffected as $m) {
            if (\in_array(\spl_object_hash($m), $this->oldMembershipsHashes, true)) {
                $list->addAll($this->validator->validate($m, null, [self::VALIDATION_GROUP_AFFECTED]));
            } else {
                $list->addAll($this->validator->validate($m, null, [self::VALIDATION_GROUP_CREATED,
                    self::VALIDATION_GROUP_AFFECTED, ]));
            }
        }

        return $list;
    }
}
