<?php

/*
 * This file is part of the Serenity package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Serenity\CoreBundle\Seed\EventListener;

use Serenity\CoreBundle\Seed\Command\SeedCommand;
use Serenity\CoreBundle\Seed\Doctrine\Repository\SeedRunRepository;
use Serenity\CoreBundle\Seed\Entity\SeedRun;
use Serenity\CoreBundle\Seed\Exception\SeedAlreadyRunningException;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\MessageSentToTransportsEvent;
use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageRetriedEvent;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;

class SeedRunTrackingListener
{
    /** @var array<string, LockInterface> */
    private array $locks = [];

    public function __construct(
        private SeedRunRepository $seedRuns,
        private LockFactory $seedLockFactory,
    ) {
    }

    #[AsEventListener]
    public function onSendToTransports(SendMessageToTransportsEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }

        $run = $this->getOrCreateRun($cmd);
        $run->markQueued();

        $this->seedRuns->save($run);
    }

    #[AsEventListener]
    public function onMessageSent(MessageSentToTransportsEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }

        $run = $this->getOrCreateRun($cmd);

        $transportMessageId = $event->getEnvelope()->last(TransportMessageIdStamp::class)?->getId();
        $run->markSent($transportMessageId);

        $this->seedRuns->save($run);
    }

    /**
     * @throws \JsonException
     */
    #[AsEventListener]
    public function onReceived(WorkerMessageReceivedEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }
        $run = $this->getOrCreateRun($cmd);

        $lockKey = $this->createKeyLock($event->getEnvelope());
        $lock = $this->seedLockFactory->createLock($lockKey, ttl: 300);
        $this->locks[$lockKey] = $lock;

        if (!$lock->acquire()) {
            $run->markIgnoredBecauseLocked('Seed already running (%s)');
            $this->seedRuns->save($run);
            throw new SeedAlreadyRunningException(\sprintf('Seed already running for "%s".', $lockKey));
        }

        $this->locks[$lockKey] = $lock;
        $run = $this->getOrCreateRun($cmd);

        $attempt = $this->resolveAttempt($event->getEnvelope());
        $run->markRunning($attempt);

        $this->seedRuns->save($run);
    }

    /**
     * @throws \JsonException
     */
    #[AsEventListener]
    public function onHandled(WorkerMessageHandledEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }

        $run = $this->getOrCreateRun($cmd);
        $run->markSuccess();

        $this->seedRuns->save($run);
        $this->releaseLock($event->getEnvelope());
    }

    /**
     * @throws \JsonException
     */
    #[AsEventListener]
    public function onFailed(WorkerMessageFailedEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }

        $run = $this->getOrCreateRun($cmd);

        $attempt = $this->resolveAttempt($event->getEnvelope());
        $e = $event->getThrowable();

        if ($e instanceof SeedAlreadyRunningException) {
            return; // pas de retry, pas de markFailed, rien à faire
        }

        // Si c’est explicitement "unrecoverable", jamais de retry
        if ($e instanceof UnrecoverableMessageHandlingException) {
            $run->markFailed($e, $attempt);
            $this->seedRuns->save($run);
            $this->releaseLock($event->getEnvelope());

            return;
        }

        // Symfony >= 6.3/6.4 expose souvent willRetry()
        $willRetry = $event->willRetry();

        if ($willRetry) {
            $run->markRetrying($e, $attempt);
        } else {
            $run->markFailed($e, $attempt);
        }

        $this->seedRuns->save($run);
        $this->releaseLock($event->getEnvelope());
    }

    #[AsEventListener]
    public function onRetried(WorkerMessageRetriedEvent $event): void
    {
        $cmd = $this->extractSeedCommand($event->getEnvelope());
        if (!$cmd) {
            return;
        }

        // Ici c’est “re-queue”, pas “running”.
        // Deux options :
        // - garder STATUS_RETRYING jusqu’au prochain onReceived()
        // - ou repasser STATUS_QUEUED (à toi de décider)
        $run = $this->getOrCreateRun($cmd);

        // Option A : on garde RETRYING (aucune modif nécessaire)
        // Option B : on repasse QUEUED
        $run->markQueued();
        $this->seedRuns->save($run);
    }

    private function extractSeedCommand(Envelope $envelope): ?SeedCommand
    {
        $msg = $envelope->getMessage();

        return $msg instanceof SeedCommand ? $msg : null;
    }

    /**
     * @throws \JsonException
     */
    private function releaseLock(Envelope $envelope): void
    {
        $lockKey = $this->createKeyLock($envelope);

        if (!isset($this->locks[$lockKey])) {
            return;
        }

        try {
            $this->locks[$lockKey]->release();
        } finally {
            unset($this->locks[$lockKey]);
        }
    }

    /**
     * @throws \JsonException
     */
    private function createKeyLock(Envelope $envelope): string
    {
        $msg = $envelope->getMessage();

        if (!$msg instanceof SeedCommand) {
            throw new UnrecoverableMessageHandlingException(\sprintf('Invalid message type: %s', get_debug_type($msg)));
        }

        return $msg->payload->lockKey();
    }

    private function getOrCreateRun(SeedCommand $cmd): SeedRun
    {
        $runId = $cmd->payload->runId;

        $run = $this->seedRuns->findOneByRunId($runId);
        if ($run) {
            return $run;
        }

        // cas edge : si jamais le run n’a pas été créé avant dispatch
        $run = SeedRun::pending($cmd->payload);
        $this->seedRuns->save($run);

        return $run;
    }

    /**
     * attempt "humain"
     * - 1ère exécution => 1
     * - 1er retry => 2
     * - 2e retry => 3.
     */
    private function resolveAttempt(Envelope $envelope): int
    {
        $retryCount = $envelope->last(RedeliveryStamp::class)?->getRetryCount();

        return null === $retryCount ? 1 : $retryCount + 2;
    }
}
