<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Entity\AuditLog;
use App\Entity\ModelType;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
 * Record a lightweight, per-product audit trail.
 *
 * This MVP focuses on Product updates and captures:
 * - scalar field changes (from Doctrine change sets)
 * - constructeur collection changes (from collection updates)
 */
#[AsDoctrineListener(event: Events::onFlush)]
final class ProductAuditSubscriber implements EventSubscriber
{
    public function __construct(private readonly RequestStack $requestStack)
    {
    }

    public function getSubscribedEvents(): array
    {
        return [
            Events::onFlush,
        ];
    }

    public function onFlush(OnFlushEventArgs $args): void
    {
        $em = $args->getObjectManager();
        if (!$em instanceof EntityManagerInterface) {
            return;
        }

        $uow = $em->getUnitOfWork();
        $actorProfileId = $this->resolveActorProfileId();
        $pendingUpdates = [];
        $pendingSnapshots = [];
        $pendingProducts = [];

        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            if (!$entity instanceof Product) {
                continue;
            }

            $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
            $snapshot = $this->snapshotProduct($entity);
            $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
        }

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if (!$entity instanceof Product) {
                continue;
            }

            $productId = (string) $entity->getId();
            if ($productId === '') {
                continue;
            }

            $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
            if ($diff !== []) {
                $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
                $pendingSnapshots[$productId] = $this->snapshotProduct($entity);
                $pendingProducts[$productId] = $entity;
            }
        }

        foreach ($uow->getScheduledEntityDeletions() as $entity) {
            if (!$entity instanceof Product) {
                continue;
            }

            $snapshot = $this->snapshotProduct($entity);
            $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
        }

        // Capture constructeur collection updates, which are not included in the change set.
        foreach ($uow->getScheduledCollectionUpdates() as $collection) {
            $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
        }
        foreach ($uow->getScheduledCollectionDeletions() as $collection) {
            $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
        }

        foreach ($pendingUpdates as $productId => $diff) {
            if ($diff === []) {
                continue;
            }

            $product = $pendingProducts[$productId] ?? null;
            if (!$product instanceof Product) {
                continue;
            }

            $snapshot = $pendingSnapshots[$productId] ?? $this->snapshotProduct($product);
            $this->persistAuditLog($em, new AuditLog('product', $productId, 'update', $diff, $snapshot, $actorProfileId));
        }
    }

    /**
     * @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
     * @param array<string, array<string, mixed>> $pendingSnapshots
     * @param array<string, Product> $pendingProducts
     */
    private function collectCollectionUpdate(
        object $collection,
        array &$pendingUpdates,
        array &$pendingSnapshots,
        array &$pendingProducts,
    ): void {
        if (!$collection instanceof PersistentCollection) {
            return;
        }

        $owner = $collection->getOwner();
        if (!$owner instanceof Product) {
            return;
        }

        $productId = (string) $owner->getId();
        if ($productId === '') {
            return;
        }

        $mapping = $collection->getMapping();
        $fieldName = $mapping['fieldName'] ?? null;
        if ($fieldName !== 'constructeurs') {
            return;
        }

        $before = $this->normalizeCollection($collection->getSnapshot());
        $after = $this->normalizeCollection($collection->toArray());

        if ($before === $after) {
            return;
        }

        $diff = [
            'constructeurIds' => [
                'from' => $before,
                'to' => $after,
            ],
        ];

        $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
        $pendingSnapshots[$productId] = $this->snapshotProduct($owner);
        $pendingProducts[$productId] = $owner;
    }

    private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
    {
        $uow = $em->getUnitOfWork();
        // Ensure identifiers and timestamps are set even when persisting during onFlush.
        $log->initializeAuditLog();
        $em->persist($log);

        $meta = $em->getClassMetadata(AuditLog::class);
        $uow->computeChangeSet($meta, $log);
    }

    /**
     * @param array<string, array{0:mixed, 1:mixed}> $changeSet
     * @return array<string, array{from:mixed, to:mixed}>
     */
    private function buildDiffFromChangeSet(array $changeSet): array
    {
        $diff = [];
        foreach ($changeSet as $field => [$oldValue, $newValue]) {
            // Skip noisy timestamps managed automatically.
            if ($field === 'updatedAt' || $field === 'createdAt') {
                continue;
            }

            $normalizedOld = $this->normalizeValue($oldValue);
            $normalizedNew = $this->normalizeValue($newValue);

            if ($normalizedOld === $normalizedNew) {
                continue;
            }

            $diff[$field] = [
                'from' => $normalizedOld,
                'to' => $normalizedNew,
            ];
        }

        return $diff;
    }

    private function snapshotProduct(Product $product): array
    {
        return [
            'id' => $product->getId(),
            'name' => $product->getName(),
            'reference' => $product->getReference(),
            'supplierPrice' => $product->getSupplierPrice(),
            'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
            'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
        ];
    }

    /**
     * @param array<string, array{from:mixed, to:mixed}> $base
     * @param array<string, array{from:mixed, to:mixed}> $extra
     * @return array<string, array{from:mixed, to:mixed}>
     */
    private function mergeDiffs(array $base, array $extra): array
    {
        foreach ($extra as $field => $change) {
            $base[$field] = $change;
        }

        return $base;
    }

    /**
     * @param iterable<mixed> $items
     * @return list<string>
     */
    private function normalizeCollection(iterable $items): array
    {
        $ids = [];
        foreach ($items as $item) {
            if (\is_object($item) && \method_exists($item, 'getId')) {
                $id = $item->getId();
                if ($id !== null && $id !== '') {
                    $ids[] = (string) $id;
                }
            }
        }

        sort($ids);

        return array_values(array_unique($ids));
    }

    private function normalizeValue(mixed $value): mixed
    {
        if ($value === null || \is_scalar($value)) {
            return $value;
        }

        if ($value instanceof \DateTimeInterface) {
            return $value->format(\DateTimeInterface::ATOM);
        }

        if ($value instanceof ModelType) {
            return [
                'id' => $value->getId(),
                'name' => $value->getName(),
                'code' => $value->getCode(),
            ];
        }

        if ($value instanceof Collection) {
            return $this->normalizeCollection($value);
        }

        if (\is_object($value) && \method_exists($value, 'getId')) {
            return (string) $value->getId();
        }

        if (\is_array($value)) {
            return $value;
        }

        return (string) $value;
    }

    private function resolveActorProfileId(): ?string
    {
        $session = $this->requestStack->getSession();
        if (!$session instanceof SessionInterface) {
            return null;
        }

        $profileId = $session->get('profileId');
        if (!$profileId) {
            return null;
        }

        return (string) $profileId;
    }
}
