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

use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry;

/**
 * Helper to give supplementary permissions to a related entity.
 *
 * If a related entity is associated within a workflow, the logic of the workflow can give more permissions, or
 * remove some permissions.
 *
 * The methods of this helper return either:
 *
 * - FORCE_GRANT, which means that a permission can be given, even if it would be denied when the related
 *   entity is not associated with a workflow;
 * - FORCE_DENIED, which means that a permission should be denied, even if it would be granted when the related entity
 *   is not associated with a workflow
 * - ABSTAIN, if there is no workflow logic to add or remove permission
 *
 * For read operations:
 *
 * - if the user is involved in the workflow (is part of the current step, of a step before), the user is granted read
 *   operation;
 * - if there is a pending signature for a person, the workflow grant access to the related entity;
 * - if there a signature applyied in less than 12 hours, the workflow grant access to the related entity. This allow to
 *   show the related entity to the person during this time frame.
 *
 *
 * For write operation:
 *
 * - if the workflow is finalized "positive" (means "not canceled"), the workflow denys write operations;
 * - if there isn't any finalized "positive" workflow, and if there is a signature appliyed for a running workflow (not finalized nor canceled),
 *   the workflow denys write operations;
 * - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted;
 */
class WorkflowRelatedEntityPermissionHelper
{
    public const FORCE_GRANT = 'FORCE_GRANT';
    public const FORCE_DENIED = 'FORCE_DENIED';
    public const ABSTAIN = 'ABSTAIN';

    public function __construct(
        private readonly Security $security,
        private readonly EntityWorkflowManager $entityWorkflowManager,
        private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
        private readonly Registry $registry,
        private readonly ClockInterface $clock,
    ) {}

    /**
     * @param object $entity The entity may be an
     *
     * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
     */
    public function isAllowedByWorkflowForReadOperation(object $entity): string
    {
        if ($entity instanceof StoredObject) {
            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
            $isAttached = true;
        } else {
            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
            $isAttached = false;
        }

        if ([] === $entityWorkflows) {
            return self::ABSTAIN;
        }

        if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
            return self::FORCE_GRANT;
        }

        if ($isAttached) {
            return self::ABSTAIN;
        }

        // give a view permission if there is a Person signature pending, or in the 12 hours following
        // the signature last state
        foreach ($entityWorkflows as $workflow) {
            foreach ($workflow->getCurrentStep()->getSignatures() as $signature) {
                if ('person' === $signature->getSignerKind()) {
                    if (EntityWorkflowSignatureStateEnum::PENDING === $signature->getState()) {
                        return self::FORCE_GRANT;
                    }
                    $signatureDate = $signature->getStateDate();
                    $visibleUntil = $signatureDate->add(new \DateInterval('PT12H'));

                    if ($visibleUntil > $this->clock->now()) {
                        return self::FORCE_GRANT;
                    }

                }
            }
        }

        return self::ABSTAIN;
    }

    /**
     * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
     */
    public function isAllowedByWorkflowForWriteOperation(object $entity): string
    {
        if ($entity instanceof StoredObject) {
            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
            $isAttached = true;
        } else {
            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
            $isAttached = false;
        }

        if ([] === $entityWorkflows) {
            return self::ABSTAIN;
        }

        // if a workflow is finalized positive, anyone is allowed to edit the document anymore
        foreach ($entityWorkflows as $entityWorkflow) {
            $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
            $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
            foreach ($marking->getPlaces() as $place => $int) {
                $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
                if (
                    ($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
                    || ($placeMetadata['isSentExternal'] ?? false)
                ) {
                    // the workflow is final, and final positive, or is sentExternal, so we stop here.
                    return self::FORCE_DENIED;
                }
                if (
                    // if not finalized positive
                    $entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
                ) {
                    return self::ABSTAIN;
                }
            }
        }

        $runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());

        // if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
        if (!$isAttached) {
            foreach ($runningWorkflows as $entityWorkflow) {
                foreach ($entityWorkflow->getSteps() as $step) {
                    foreach ($step->getSignatures() as $signature) {
                        if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
                            return self::FORCE_DENIED;
                        }
                    }
                }
            }
        }

        // allow only the users involved
        if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) {
            return self::FORCE_GRANT;
        }

        if ($isAttached) {
            return self::ABSTAIN;
        }

        return self::FORCE_DENIED;
    }

    /**
     * @param list<EntityWorkflow> $entityWorkflows
     */
    private function isUserInvolvedInAWorkflow(array $entityWorkflows): bool
    {
        $currentUser = $this->security->getUser();

        if (!$currentUser instanceof User) {
            return false;
        }

        foreach ($entityWorkflows as $entityWorkflow) {
            // so, the workflow is running... We return true if the current user is involved
            if ($entityWorkflow->isUserInvolved($currentUser)) {
                return true;
            }
        }

        return false;
    }
}
