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

use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;

#[AsMessageHandler]
final readonly class CancelStaleWorkflowHandler
{
    private const LOG_PREFIX = '[CancelStaleWorkflowHandler] ';

    public function __construct(
        private EntityWorkflowRepository $workflowRepository,
        private Registry $registry,
        private EntityManagerInterface $em,
        private LoggerInterface $logger,
        private ClockInterface $clock,
    ) {}

    public function __invoke(CancelStaleWorkflowMessage $message): void
    {
        $workflowId = $message->getWorkflowId();
        $olderThanDate = $this->clock->now()->sub(new \DateInterval(CancelStaleWorkflowCronJob::KEEP_INTERVAL));

        $workflow = $this->workflowRepository->find($message->getWorkflowId());
        if (null === $workflow) {
            $this->logger->alert(self::LOG_PREFIX.'Workflow was not found!', ['entityWorkflowId' => $workflowId]);

            return;
        }

        if (false === $workflow->isStaledAt($olderThanDate)) {
            $this->logger->alert(self::LOG_PREFIX.'Workflow has transitioned in the meantime.', ['entityWorkflowId' => $workflowId]);

            throw new UnrecoverableMessageHandlingException('the workflow is not staled any more');
        }

        $workflowComponent = $this->registry->get($workflow, $workflow->getWorkflowName());
        $metadataStore = $workflowComponent->getMetadataStore();
        $transitions = $workflowComponent->getEnabledTransitions($workflow);

        $transitionApplied = false;

        foreach ($transitions as $transition) {
            if ($this->willTransitionLeadToFinalNegative($transition, $metadataStore)) {
                $dto = new WorkflowTransitionContextDTO($workflow);
                $workflowComponent->apply($workflow, $transition->getName(), [
                    'context' => $dto,
                    'byUser' => null,
                    'transitionAt' => $this->clock->now(),
                    'transition' => $transition->getName(),
                ]);
                $this->logger->info(self::LOG_PREFIX.'EntityWorkflow has been cancelled automatically.', ['entityWorkflowId' => $workflowId]);
                $transitionApplied = true;
                break;
            }
        }

        if (!$transitionApplied) {
            $this->logger->error(self::LOG_PREFIX.'No valid transition found for EntityWorkflow.', ['entityWorkflowId' => $workflowId]);
            throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
        }

        $this->em->flush();
    }

    private function willTransitionLeadToFinalNegative(Transition $transition, MetadataStoreInterface $metadataStore): bool
    {
        foreach ($transition->getTos() as $place) {
            $metadata = $metadataStore->getPlaceMetadata($place);
            if (($metadata['isFinal'] ?? true) && false === ($metadata['isFinalPositive'] ?? true)) {
                return true;
            }
        }

        return false;
    }
}
