<?php

declare(strict_types=1);

/*
 * 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\Shared\Doctrine\EventListener;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Events;
use Serenity\CoreBundle\Shared\Aggregate\AggregateRootInterface;
use Serenity\CoreBundle\Shared\Messenger\EventBus;

/**
 * Listener Doctrine : DoctrinePublishDomainEventsOnFlushListener.
 *
 * ## Objectif
 * Ce listener intercepte l’événement Doctrine `onFlush` pour **publier automatiquement
 * les événements de domaine** générés par les entités agrégats (`AggregateRootInterface`)
 * avant la validation de la transaction.
 *
 * Il s’agit d’une étape clé du pattern DDD (Domain-Driven Design) :
 * les entités racines d’agrégat produisent des événements métier (`DomainEvent`)
 * lorsque leur état change, et ces événements doivent être propagés
 * (par exemple via le bus Symfony Messenger).
 *
 * Ce listener :
 * - parcourt toutes les entités affectées par la transaction (`insert`, `update`, `delete`, collections) ;
 * - vérifie si elles implémentent `AggregateRootInterface` ;
 * - récupère leurs événements de domaine via `pullDomainEvents()` ;
 * - publie ces événements via le `EventBus`.
 *
 * ## Avantages
 * - Découple complètement la logique métier du mécanisme de publication ;
 * - Évite d’avoir à injecter manuellement l’`EventBus` dans les services métier ;
 * - Assure que **tous les événements du domaine sont publiés automatiquement** dès qu’une unité de travail Doctrine est flushée.
 *
 * ## Exemple d’utilisation
 * Implémentez `AggregateRootInterface` dans vos entités racines :
 * ```php
 * final class User implements AggregateRootInterface
 * {
 *     use AggregateRootTrait;
 *
 *     public function register(): void
 *     {
 *         // Logique métier...
 *         $this->recordEvent((new UserRegisteredEvent($this->id));
 *     }
 * }
 * ```
 *
 * Doctrine déclenchera ensuite ce listener à chaque `EntityManager::flush()`,
 * et publiera automatiquement les événements enregistrés.
 *
 * ## Notes techniques
 * - L’événement `onFlush` est appelé **avant** l’exécution effective du `flush()`
 *   (l’écriture en base est encore en attente).
 * - Ce listener s’appuie sur le `EventBus` interne, basé sur Symfony Messenger.
 * - Il prend aussi en charge les collections Doctrine (OneToMany, ManyToMany).
 *
 * @see AggregateRootInterface
 * @see Serenity\CoreBundle\Domain\Event\DomainEvent
 * @see Serenity\CoreBundle\Infrastructure\Shared\Symfony\Messenger\EventBus
 */
#[AsDoctrineListener(event: Events::onFlush, priority: -256)]
#[AsDoctrineListener(event: Events::postFlush, priority: -256)]
class DoctrinePublishDomainEventsOnFlushListener
{
    /** @var AggregateRootInterface[] */
    private array $collectedEntities = [];

    public function __construct(private readonly EventBus $eventBus)
    {
    }

    public function onFlush(OnFlushEventArgs $eventArgs): void
    {
        $unitOfWork = $eventArgs->getObjectManager()->getUnitOfWork();

        foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
            $this->publishDomainEvent($entity);
        }

        foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
            $this->publishDomainEvent($entity);
        }

        foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
            $this->publishDomainEvent($entity);
        }

        foreach ($unitOfWork->getScheduledCollectionDeletions() as $collection) {
            foreach ($collection as $entity) {
                $this->publishDomainEvent($entity);
            }
        }

        foreach ($unitOfWork->getScheduledCollectionUpdates() as $collection) {
            foreach ($collection as $entity) {
                $this->publishDomainEvent($entity);
            }
        }
    }

    public function postFlush(PostFlushEventArgs $eventArgs): void
    {
        foreach ($this->collectedEntities as $entity) {
            foreach ($entity->pullDomainEvents() as $event) {
                $this->eventBus->publish($event);
            }
        }
        $this->collectedEntities = [];
    }

    private function publishDomainEvent(object $entity): void
    {
        if ($entity instanceof AggregateRootInterface && !$entity->domainEventsEmpty()) {
            $this->collectedEntities[] = $entity;
        }
    }
}
