<?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\DocStoreBundle\Security\Authorization\StoredObjectVoter;

use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;

abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
{
    abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;

    /**
     * @return class-string
     */
    abstract protected function getClass(): string;

    abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;

    abstract protected function canBeAssociatedWithWorkflow(): bool;

    public function __construct(
        private readonly Security $security,
        private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
        private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
    ) {}

    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
    {
        $class = $this->getClass();

        return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
    }

    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
    {
        // Retrieve the related entity
        $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);

        // Determine the attribute to pass to the voter for argument
        $voterAttribute = $this->attributeToRole($attribute);

        $regularPermission = $this->security->isGranted($voterAttribute, $entity);

        if (!$this->canBeAssociatedWithWorkflow()) {
            return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject);
        }

        $workflowPermission = match ($attribute) {
            StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity),
            StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity),
        };

        return match ($workflowPermission) {
            WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true,
            WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false,
            WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject),
        };
    }

    private function voteOnStoredObjectAsAttachementOfAWorkflow(StoredObjectRoleEnum $attribute, bool $regularPermission, StoredObject $storedObject): bool
    {
        $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($storedObject);

        // we get all the entity workflows where the stored object is attached
        $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);

        // we compute all the permission for each entity workflow
        $permissions = array_map(fn (EntityWorkflow $entityWorkflow): string => match ($attribute) {
            StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entityWorkflow),
            StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entityWorkflow),
        }, $entityWorkflows);

        // now, we reduce the permissions: abstain are ignored. Between DENIED and and GRANT, DENIED takes precedence
        $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN;
        foreach ($permissions as $permission) {
            if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED === $permission) {
                return false;
            }
            if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT === $permission) {
                $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT;
            }
        }

        if (WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN === $computedPermission) {
            return $regularPermission;
        }

        // this is the case where WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT is returned
        return true;
    }
}
