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

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\User;
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
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;

#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['entity_workflow' => EntityWorkflow::class])]
#[ORM\Entity]
#[ORM\Table('chill_main_workflow_entity')]
#[EntityWorkflowCreation(groups: ['creation'])]
class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
{
    use TrackCreationTrait;

    use TrackUpdateTrait;

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

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
    private ?int $id = null;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
    private string $relatedEntityClass = '';

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
    private int $relatedEntityId;

    /**
     * @var Collection<int, EntityWorkflowStep>&Selectable<int, EntityWorkflowStep>
     */
    #[Assert\Valid(traverse: true)]
    #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
    private Collection&Selectable $steps;

    /**
     * @var array|EntityWorkflowStep[]|null
     */
    private ?array $stepsChainedCache = null;

    /**
     * @var Collection<int, User>
     */
    #[ORM\ManyToMany(targetEntity: User::class)]
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
    private Collection $subscriberToFinal;

    /**
     * @var Collection<int, User>
     */
    #[ORM\ManyToMany(targetEntity: User::class)]
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')]
    private Collection $subscriberToStep;

    /**
     * a step which will store all the transition data.
     */
    private ?EntityWorkflowStep $transitionningStep = null;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
    private string $workflowName;

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

    public function __construct()
    {
        $this->subscriberToFinal = new ArrayCollection();
        $this->subscriberToStep = new ArrayCollection();
        $this->comments = new ArrayCollection();
        $this->steps = new ArrayCollection();
        $this->attachments = new ArrayCollection();

        $initialStep = new EntityWorkflowStep();
        $initialStep
            ->setCurrentStep('initial');
        $this->addStep($initialStep);
    }

    public function addComment(EntityWorkflowComment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments[] = $comment;
            $comment->setEntityWorkflow($this);
        }

        return $this;
    }

    /**
     * @internal You should prepare a step and run a workflow transition instead of manually adding a step
     */
    public function addStep(EntityWorkflowStep $step): self
    {
        if (!$this->steps->contains($step)) {
            $this->steps[] = $step;
            $step->setEntityWorkflow($this);
            $this->stepsChainedCache = null;
        }

        return $this;
    }

    public function addSubscriberToFinal(User $user): self
    {
        if (!$this->subscriberToFinal->contains($user)) {
            $this->subscriberToFinal[] = $user;
        }

        return $this;
    }

    public function addSubscriberToStep(User $user): self
    {
        if (!$this->subscriberToStep->contains($user)) {
            $this->subscriberToStep[] = $user;
        }

        return $this;
    }

    /**
     * @return $this
     *
     * @internal use @{EntityWorkflowAttachement::__construct} instead
     */
    public function addAttachment(EntityWorkflowAttachment $attachment): self
    {
        if (!$this->attachments->contains($attachment)) {
            $this->attachments[] = $attachment;
        }

        return $this;
    }

    /**
     * @return Collection<int, EntityWorkflowAttachment>
     */
    public function getAttachments(): Collection
    {
        return $this->attachments;
    }

    public function removeAttachment(EntityWorkflowAttachment $attachment): self
    {
        $this->attachments->removeElement($attachment);

        return $this;
    }

    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function getCurrentStep(): ?EntityWorkflowStep
    {
        $step = $this->steps->last();

        if (false !== $step) {
            return $step;
        }

        return null;
    }

    public function getCurrentStepChained(): ?EntityWorkflowStep
    {
        $steps = $this->getStepsChained();
        $currentStep = $this->getCurrentStep();

        foreach ($steps as $step) {
            if ($step === $currentStep) {
                return $step;
            }
        }

        return null;
    }

    public function getCurrentStepCreatedAt(): ?\DateTimeInterface
    {
        if (null !== $previous = $this->getPreviousStepIfAny()) {
            return $previous->getTransitionAt();
        }

        return null;
    }

    public function getCurrentStepCreatedBy(): ?User
    {
        if (null !== $previous = $this->getPreviousStepIfAny()) {
            return $previous->getTransitionBy();
        }

        return null;
    }

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

    public function getRelatedEntityClass(): string
    {
        return $this->relatedEntityClass;
    }

    public function getRelatedEntityId(): int
    {
        return $this->relatedEntityId;
    }

    /**
     * Method used by MarkingStore.
     *
     * get a string representation of the step
     */
    public function getStep(): string
    {
        return $this->getCurrentStep()->getCurrentStep();
    }

    public function getStepAfter(EntityWorkflowStep $step): ?EntityWorkflowStep
    {
        $iterator = $this->steps->getIterator();

        if ($iterator instanceof \Iterator) {
            $iterator->rewind();

            while ($iterator->valid()) {
                $curStep = $iterator->current();

                if ($curStep === $step) {
                    $iterator->next();

                    if ($iterator->valid()) {
                        return $iterator->current();
                    }

                    return null;
                }
                $iterator->next();
            }

            return null;
        }

        throw new \RuntimeException();
    }

    /**
     * @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
     */
    public function getSteps(): Collection&Selectable
    {
        return $this->steps;
    }

    /**
     * @throws \Exception
     */
    public function getStepsChained(): array
    {
        if (\is_array($this->stepsChainedCache)) {
            return $this->stepsChainedCache;
        }

        /** @var \ArrayIterator $iterator */
        $iterator = $this->steps->getIterator();
        $current = null;
        $steps = [];

        $iterator->rewind();

        do {
            $previous = $current;
            $current = $iterator->current();
            $steps[] = $current;

            $current->setPrevious($previous);

            $iterator->next();

            if ($iterator->valid()) {
                $current->setNext($iterator->current());
            } else {
                $current->setNext(null);
            }
        } while ($iterator->valid());

        $this->stepsChainedCache = $steps;

        return $steps;
    }

    public function getSubscriberToFinal(): ArrayCollection|Collection
    {
        return $this->subscriberToFinal;
    }

    public function getSubscriberToStep(): ArrayCollection|Collection
    {
        return $this->subscriberToStep;
    }

    /**
     * get the step which is transitionning. Should be called only by event which will
     * concern the transition.
     */
    public function getTransitionningStep(): ?EntityWorkflowStep
    {
        return $this->transitionningStep;
    }

    /**
     * @return User[]
     */
    public function getUsersInvolved(): array
    {
        $usersInvolved = [];
        $usersInvolved[spl_object_hash($this->getCreatedBy())] = $this->getCreatedBy();

        foreach ($this->steps as $step) {
            foreach ($step->getDestUser() as $u) {
                $usersInvolved[spl_object_hash($u)] = $u;
            }
        }

        return array_values($usersInvolved);
    }

    public function getWorkflowName(): string
    {
        return $this->workflowName;
    }

    public function isFinal(): bool
    {
        foreach ($this->getStepsChained() as $step) {
            if ($step->isFinal()) {
                return true;
            }
        }

        return false;
    }

    public function isFreeze(): bool
    {
        foreach ($this->getStepsChained() as $step) {
            if ($step->isFreezeAfter()) {
                return true;
            }
        }

        return false;
    }

    public function isOnHoldByUser(User $user): bool
    {
        return $this->getCurrentStep()->isOnHoldByUser($user);
    }

    public function isUserInvolved(User $user): bool
    {
        foreach ($this->getSteps() as $step) {
            if ($step->getAllDestUser()->contains($user)) {
                return true;
            }
        }

        return false;
    }

    public function isUserSubscribedToFinal(User $user): bool
    {
        return $this->subscriberToFinal->contains($user);
    }

    public function isUserSubscribedToStep(User $user): bool
    {
        return $this->subscriberToStep->contains($user);
    }

    public function prepareStepBeforeTransition(EntityWorkflowStep $step): self
    {
        $this->transitionningStep = $step;

        return $this;
    }

    public function removeComment(EntityWorkflowComment $comment): self
    {
        if ($this->comments->removeElement($comment)) {
            $comment->setEntityWorkflow(null);
        }

        return $this;
    }

    public function removeStep(EntityWorkflowStep $step): self
    {
        if ($this->steps->removeElement($step)) {
            $step->setEntityWorkflow(null);
        }

        return $this;
    }

    public function removeSubscriberToFinal(User $user): self
    {
        $this->subscriberToFinal->removeElement($user);

        return $this;
    }

    public function removeSubscriberToStep(User $user): self
    {
        $this->subscriberToStep->removeElement($user);

        return $this;
    }

    public function setRelatedEntityClass(string $relatedEntityClass): EntityWorkflow
    {
        $this->relatedEntityClass = $relatedEntityClass;

        return $this;
    }

    public function setRelatedEntityId(int $relatedEntityId): EntityWorkflow
    {
        $this->relatedEntityId = $relatedEntityId;

        return $this;
    }

    /**
     * Method used by marking store.
     *
     * @return $this
     */
    public function setStep(
        string $step,
        WorkflowTransitionContextDTO $transitionContextDTO,
        string $transition,
        \DateTimeImmutable $transitionAt,
        ?User $byUser = null,
    ): self {
        $previousStep = $this->getCurrentStep();

        $previousStep
            ->setComment($transitionContextDTO->comment)
            ->setTransitionAfter($transition)
            ->setTransitionAt($transitionAt)
            ->setTransitionBy($byUser);

        $newStep = new EntityWorkflowStep();
        $newStep->setCurrentStep($step);

        foreach ($transitionContextDTO->futureCcUsers as $user) {
            $newStep->addCcUser($user);
        }

        foreach ($transitionContextDTO->getFutureDestUsers() as $user) {
            $newStep->addDestUser($user);
        }

        foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) {
            $newStep->addDestUserGroup($userGroup);
        }

        if (null !== $transitionContextDTO->futureUserSignature) {
            $newStep->addDestUser($transitionContextDTO->futureUserSignature);
        }

        if (null !== $transitionContextDTO->futureUserSignature) {
            new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
        } else {
            foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
                new EntityWorkflowStepSignature($newStep, $personSignature);
            }
        }

        foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
            new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
        }
        foreach ($transitionContextDTO->futureDestineeEmails as $email) {
            new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
        }

        // copy the freeze
        if ($this->isFreeze()) {
            $newStep->setFreezeAfter(true);
        }

        $this->addStep($newStep);

        return $this;
    }

    public function setWorkflowName(string $workflowName): EntityWorkflow
    {
        $this->workflowName = $workflowName;

        return $this;
    }

    private function getPreviousStepIfAny(): ?EntityWorkflowStep
    {
        if (1 === \count($this->steps)) {
            return null;
        }

        return $this->steps->get($this->steps->count() - 2);
    }

    public function isOnHoldAtCurrentStep(): bool
    {
        return $this->getCurrentStep()->getHoldsOnStep()->count() > 0;
    }

    /**
     * Determines if the workflow has become stale after a given date.
     *
     * This function checks the creation date and the transition states of the workflow steps.
     * A workflow is considered stale if:
     * - The creation date is before the given date and no transitions have occurred since the creation.
     * - Or if there are no transitions after the given date.
     *
     * @param \DateTimeImmutable $at the date to compare against the workflow's status
     *
     * @return bool true if the workflow is stale after the given date, false otherwise
     */
    public function isStaledAt(\DateTimeImmutable $at): bool
    {
        // if there is no transition since the creation, then the workflow is staled
        if ('initial' === $this->getCurrentStep()->getCurrentStep()
            && null === $this->getCurrentStep()->getTransitionAt()
        ) {
            if (null === $this->getCreatedAt()) {
                return false;
            }

            if ($this->getCreatedAt() < $at) {
                return true;
            }

            return false;
        }

        return $this->getCurrentStepChained()->getPrevious()->getTransitionAt() < $at;
    }
}
