<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\AuditLog;
use App\Entity\Composant;
use App\Entity\ComposantConstructeurLink;
use App\Entity\ComposantPieceSlot;
use App\Entity\ComposantProductSlot;
use App\Entity\ComposantSubcomponentSlot;
use App\Entity\Machine;
use App\Entity\MachineComponentLink;
use App\Entity\MachineConstructeurLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\Piece;
use App\Entity\PieceConstructeurLink;
use App\Entity\PieceProductSlot;
use App\Entity\Product;
use App\Entity\ProductConstructeurLink;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
use App\Repository\ConstructeurRepository;
use App\Repository\CustomFieldValueRepository;
use App\Repository\MachineRepository;
use App\Repository\ModelTypeRepository;
use App\Repository\PieceRepository;
use App\Repository\ProductRepository;
use App\Repository\ProfileRepository;
use App\Repository\SiteRepository;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use LogicException;
use Throwable;

final class EntityVersionService
{
    private const ENTITY_MAP = [
        'machine'   => Machine::class,
        'composant' => Composant::class,
        'piece'     => Piece::class,
        'product'   => Product::class,
    ];

    private const BASE_FIELDS = [
        'machine'   => ['name', 'reference', 'prix'],
        'composant' => ['name', 'reference', 'description', 'prix'],
        'piece'     => ['name', 'reference', 'description', 'prix'],
        'product'   => ['name', 'reference', 'supplierPrice'],
    ];

    public function __construct(
        private readonly AuditLogRepository $auditLogs,
        private readonly EntityManagerInterface $em,
        private readonly ActorProfileResolver $actorProfileResolver,
        private readonly MachineRepository $machines,
        private readonly ComposantRepository $composants,
        private readonly PieceRepository $pieces,
        private readonly ProductRepository $products,
        private readonly ConstructeurRepository $constructeurs,
        private readonly SiteRepository $sites,
        private readonly ModelTypeRepository $modelTypes,
        private readonly CustomFieldValueRepository $customFieldValues,
        private readonly ProfileRepository $profiles,
    ) {}

    /**
     * @return array{items: list<array>, total: int}
     */
    public function getVersions(string $entityType, string $entityId): array
    {
        $logs = $this->auditLogs->findVersionHistory($entityType, $entityId);

        $actorIds = array_values(array_unique(array_filter(array_map(
            static fn (AuditLog $log) => $log->getActorProfileId(),
            $logs,
        ))));

        $actorMap = [];
        if ([] !== $actorIds) {
            $profileEntities = $this->profiles->findBy(['id' => $actorIds]);
            foreach ($profileEntities as $profile) {
                $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
                if ('' === $label) {
                    $label = $profile->getEmail() ?? $profile->getId();
                }
                $actorMap[$profile->getId()] = $label;
            }
        }

        $items = array_map(
            static function (AuditLog $log) use ($actorMap) {
                $actorId = $log->getActorProfileId();

                return [
                    'version'   => $log->getVersion(),
                    'action'    => $log->getAction(),
                    'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
                    'actor'     => $actorId
                        ? ['id' => $actorId, 'label' => $actorMap[$actorId] ?? $actorId]
                        : null,
                    'diff' => $log->getDiff(),
                ];
            },
            $logs,
        );

        return ['items' => array_values($items), 'total' => count($items)];
    }

    /**
     * @return array{version: int, restoreMode: string, diff: array, warnings: list<array>, snapshot: array}
     */
    public function getRestorePreview(string $entityType, string $entityId, int $version): array
    {
        $entity = $this->findEntity($entityType, $entityId);
        if (null === $entity) {
            throw new InvalidArgumentException('Entité introuvable.');
        }

        $auditLog = $this->auditLogs->findByVersion($entityType, $entityId, $version);
        if (null === $auditLog) {
            throw new InvalidArgumentException('Version introuvable.');
        }

        $snapshot    = $auditLog->getSnapshot() ?? [];
        $restoreMode = $this->checkSkeletonCompatibility($entityType, $entity, $snapshot);
        $warnings    = $this->checkIntegrity($entityType, $snapshot, $restoreMode);
        $diff        = $this->buildRestoreDiff($entityType, $entity, $snapshot, $restoreMode);

        return [
            'version'     => $version,
            'restoreMode' => $restoreMode,
            'diff'        => $diff,
            'warnings'    => $warnings,
            'snapshot'    => $snapshot,
        ];
    }

    /**
     * @return array{success: true, newVersion: int, restoredFromVersion: int, restoreMode: string, warnings: list<array>}
     */
    public function restore(string $entityType, string $entityId, int $version): array
    {
        $entity = $this->findEntity($entityType, $entityId);
        if (null === $entity) {
            throw new InvalidArgumentException('Entité introuvable.');
        }

        $auditLog = $this->auditLogs->findByVersion($entityType, $entityId, $version);
        if (null === $auditLog) {
            throw new InvalidArgumentException('Version introuvable.');
        }

        $snapshot    = $auditLog->getSnapshot() ?? [];
        $restoreMode = $this->checkSkeletonCompatibility($entityType, $entity, $snapshot);
        $warnings    = $this->checkIntegrity($entityType, $snapshot, $restoreMode);

        $connection = $this->em->getConnection();
        $connection->beginTransaction();

        try {
            // Mark entity to skip audit subscriber (we create the AuditLog manually)
            if (method_exists($entity, 'setSkipAudit')) {
                $entity->setSkipAudit(true);
            }

            $this->applyRestore($entityType, $entity, $snapshot, $restoreMode);

            // Increment version manually (since subscriber is skipped)
            if (method_exists($entity, 'incrementVersion')) {
                $entity->incrementVersion();
            }

            $this->em->flush();

            $newVersion = method_exists($entity, 'getVersion') ? $entity->getVersion() : null;

            // Create the restore AuditLog manually with action = "restore"
            $restoreAuditLog = new AuditLog(
                $entityType,
                $entityId,
                'restore',
                ['restoredFromVersion' => $version, 'restoreMode' => $restoreMode],
                $this->buildCurrentSnapshot($entityType, $entity),
                $this->actorProfileResolver->resolve(),
                $newVersion,
            );
            $this->em->persist($restoreAuditLog);
            $this->em->flush();

            $connection->commit();
        } catch (Throwable $e) {
            $connection->rollBack();

            throw $e;
        } finally {
            // Clear skip flag
            if (method_exists($entity, 'setSkipAudit')) {
                $entity->setSkipAudit(false);
            }
        }

        return [
            'success'             => true,
            'newVersion'          => $newVersion,
            'restoredFromVersion' => $version,
            'restoreMode'         => $restoreMode,
            'warnings'            => $warnings,
        ];
    }

    private function findEntity(string $entityType, string $entityId): ?object
    {
        return match ($entityType) {
            'machine'   => $this->machines->find($entityId),
            'composant' => $this->composants->find($entityId),
            'piece'     => $this->pieces->find($entityId),
            'product'   => $this->products->find($entityId),
            default     => null,
        };
    }

    private function checkSkeletonCompatibility(string $entityType, object $entity, array $snapshot): string
    {
        if ('machine' === $entityType) {
            return 'full';
        }

        $currentTypeId = match ($entityType) {
            'composant' => $entity->getTypeComposant()?->getId(),
            'piece'     => $entity->getTypePiece()?->getId(),
            'product'   => $entity->getTypeProduct()?->getId(),
            default     => null,
        };

        $typeKey = match ($entityType) {
            'composant' => 'typeComposant',
            'piece'     => 'typePiece',
            'product'   => 'typeProduct',
            default     => null,
        };

        $snapshotTypeId = null;
        if ($typeKey && isset($snapshot[$typeKey])) {
            $snapshotTypeId = is_array($snapshot[$typeKey]) ? ($snapshot[$typeKey]['id'] ?? null) : $snapshot[$typeKey];
        }

        return $currentTypeId === $snapshotTypeId ? 'full' : 'partial';
    }

    private function checkIntegrity(string $entityType, array $snapshot, string $restoreMode): array
    {
        $warnings = [];

        // Check constructeurs (batch query)
        if (!empty($snapshot['constructeurIds'])) {
            $constructeurEntries = [];
            foreach ($snapshot['constructeurIds'] as $entry) {
                $id   = is_array($entry) ? ($entry['id'] ?? null) : $entry;
                $name = is_array($entry) ? ($entry['name'] ?? $id) : $id;
                if ($id) {
                    $constructeurEntries[$id] = $name;
                }
            }
            if ([] !== $constructeurEntries) {
                $foundIds = array_map(
                    fn ($c) => $c->getId(),
                    $this->constructeurs->findBy(['id' => array_keys($constructeurEntries)]),
                );
                foreach ($constructeurEntries as $id => $name) {
                    if (!in_array($id, $foundIds, true)) {
                        $warnings[] = [
                            'field'             => 'constructeurIds',
                            'message'           => sprintf("Le fournisseur '%s' n'existe plus. Il ne sera pas restauré.", $name),
                            'missingEntityId'   => $id,
                            'missingEntityName' => $name,
                        ];
                    }
                }
            }
        }

        // Machine: check site
        if ('machine' === $entityType && !empty($snapshot['site'])) {
            $siteId = is_array($snapshot['site']) ? ($snapshot['site']['id'] ?? null) : $snapshot['site'];
            if ($siteId && null === $this->sites->find($siteId)) {
                $siteName   = is_array($snapshot['site']) ? ($snapshot['site']['name'] ?? $siteId) : $siteId;
                $warnings[] = [
                    'field'             => 'site',
                    'message'           => sprintf("Le site '%s' n'existe plus. Le site actuel sera conservé.", $siteName),
                    'missingEntityId'   => $siteId,
                    'missingEntityName' => $siteName,
                ];
            }
        }

        if ('partial' === $restoreMode) {
            return $warnings;
        }

        // Machine: check link references
        if ('machine' === $entityType) {
            $linkChecks = [
                ['key' => 'componentLinks', 'refKey' => 'composantId', 'nameKey' => 'composantName', 'label' => 'composant', 'repo' => $this->composants],
                ['key' => 'pieceLinks', 'refKey' => 'pieceId', 'nameKey' => 'pieceName', 'label' => 'pièce', 'repo' => $this->pieces],
                ['key' => 'productLinks', 'refKey' => 'productId', 'nameKey' => 'productName', 'label' => 'produit', 'repo' => $this->products],
            ];

            foreach ($linkChecks as $check) {
                $links  = $snapshot[$check['key']] ?? [];
                $refIds = [];
                foreach ($links as $link) {
                    $refId = $link[$check['refKey']] ?? null;
                    if (null !== $refId) {
                        $refIds[$refId] = $link[$check['nameKey']] ?? $refId;
                    }
                }
                if ([] === $refIds) {
                    continue;
                }
                $foundIds = array_map(fn ($e) => $e->getId(), $check['repo']->findBy(['id' => array_keys($refIds)]));
                foreach ($refIds as $id => $name) {
                    if (!in_array($id, $foundIds, true)) {
                        $warnings[] = [
                            'field'             => $check['key'],
                            'message'           => sprintf("Le %s '%s' n'existe plus. Le lien ne sera pas restauré.", $check['label'], $name),
                            'missingEntityId'   => $id,
                            'missingEntityName' => $name,
                        ];
                    }
                }
            }
        }

        // Full mode: check slot references (batch queries per slot type)
        $slotChecks = match ($entityType) {
            'composant' => [
                ['key' => 'pieceSlots', 'refKey' => 'selectedPieceId', 'label' => 'pièce', 'repo' => $this->pieces],
                ['key' => 'subcomponentSlots', 'refKey' => 'selectedComposantId', 'label' => 'sous-composant', 'repo' => $this->composants],
                ['key' => 'productSlots', 'refKey' => 'selectedProductId', 'label' => 'produit', 'repo' => $this->products],
            ],
            'piece' => [
                ['key' => 'productSlots', 'refKey' => 'selectedProductId', 'label' => 'produit', 'repo' => $this->products],
            ],
            default => [],
        };

        foreach ($slotChecks as $check) {
            $slots = $snapshot[$check['key']] ?? [];
            // Collect all referenced IDs for batch lookup
            $refIds = [];
            foreach ($slots as $i => $slot) {
                $refId = $slot[$check['refKey']] ?? null;
                if (null !== $refId) {
                    $refIds[$i] = $refId;
                }
            }
            if ([] === $refIds) {
                continue;
            }
            $foundEntities = $check['repo']->findBy(['id' => array_values(array_unique($refIds))]);
            $foundIds      = array_map(fn ($e) => $e->getId(), $foundEntities);
            foreach ($refIds as $i => $refId) {
                if (!in_array($refId, $foundIds, true)) {
                    $warnings[] = [
                        'field'             => sprintf('%s[%d].%s', $check['key'], $i, $check['refKey']),
                        'message'           => sprintf("Le %s sélectionné dans le slot n'existe plus. Le slot sera restauré vide.", $check['label']),
                        'missingEntityId'   => $refId,
                        'missingEntityName' => null,
                    ];
                }
            }
        }

        return $warnings;
    }

    private function buildRestoreDiff(string $entityType, object $entity, array $snapshot, string $restoreMode): array
    {
        $diff       = [];
        $baseFields = self::BASE_FIELDS[$entityType] ?? [];

        foreach ($baseFields as $field) {
            $getter = 'get'.ucfirst($field);
            if (!method_exists($entity, $getter)) {
                continue;
            }
            $current  = $entity->{$getter}();
            $restored = $snapshot[$field] ?? null;
            if ((string) ($current ?? '') !== (string) ($restored ?? '')) {
                $diff[$field] = ['current' => $current, 'restored' => $restored];
            }
        }

        // Constructeurs
        $currentConstructeurIds = [];
        if (method_exists($entity, 'getConstructeurLinks')) {
            foreach ($entity->getConstructeurLinks() as $link) {
                $currentConstructeurIds[] = $link->getConstructeur()->getId();
            }
        }
        $snapshotConstructeurIds = [];
        foreach ($snapshot['constructeurIds'] ?? [] as $entry) {
            $snapshotConstructeurIds[] = is_array($entry) ? ($entry['id'] ?? null) : $entry;
        }
        sort($currentConstructeurIds);
        sort($snapshotConstructeurIds);
        if ($currentConstructeurIds !== $snapshotConstructeurIds) {
            $diff['constructeurIds'] = ['current' => $currentConstructeurIds, 'restored' => $snapshotConstructeurIds];
        }

        if ('full' === $restoreMode) {
            // We signal slot restore as a single diff entry
            $slotKeys = match ($entityType) {
                'composant' => ['pieceSlots', 'subcomponentSlots', 'productSlots'],
                'piece'     => ['productSlots'],
                default     => [],
            };
            foreach ($slotKeys as $key) {
                if (!empty($snapshot[$key])) {
                    $diff[$key] = ['current' => '(structure actuelle)', 'restored' => sprintf('%d slot(s)', count($snapshot[$key]))];
                }
            }

            // Machine: link diffs
            if ('machine' === $entityType) {
                $linkTypes = [
                    'componentLinks' => ['idKey' => 'composantId', 'nameKey' => 'composantName', 'getter' => 'getComponentLinks', 'entityGetter' => 'getComposant'],
                    'pieceLinks'     => ['idKey' => 'pieceId', 'nameKey' => 'pieceName', 'getter' => 'getPieceLinks', 'entityGetter' => 'getPiece'],
                    'productLinks'   => ['idKey' => 'productId', 'nameKey' => 'productName', 'getter' => 'getProductLinks', 'entityGetter' => 'getProduct'],
                ];

                foreach ($linkTypes as $key => $config) {
                    $currentIds   = [];
                    $currentNames = [];
                    if (method_exists($entity, $config['getter'])) {
                        foreach ($entity->{$config['getter']}() as $link) {
                            $linked                         = $link->{$config['entityGetter']}();
                            $currentIds[]                   = $linked->getId();
                            $currentNames[$linked->getId()] = $linked->getName();
                        }
                    }

                    $snapshotIds   = [];
                    $snapshotNames = [];
                    foreach ($snapshot[$key] ?? [] as $entry) {
                        $id = $entry[$config['idKey']] ?? null;
                        if ($id) {
                            $snapshotIds[]      = $id;
                            $snapshotNames[$id] = $entry[$config['nameKey']] ?? $id;
                        }
                    }

                    sort($currentIds);
                    sort($snapshotIds);
                    if ($currentIds !== $snapshotIds) {
                        $currentDisplay  = array_map(fn ($id) => $currentNames[$id] ?? $id, $currentIds);
                        $restoredDisplay = array_map(fn ($id) => $snapshotNames[$id] ?? $id, $snapshotIds);
                        $diff[$key]      = [
                            'current'  => [] !== $currentDisplay ? implode(', ', $currentDisplay) : '(aucun)',
                            'restored' => [] !== $restoredDisplay ? implode(', ', $restoredDisplay) : '(aucun)',
                        ];
                    }
                }
            }

            // Custom field values diff
            $snapshotCfvs = $snapshot['customFieldValues'] ?? [];
            if ([] !== $snapshotCfvs && method_exists($entity, 'getCustomFieldValues')) {
                $currentCfvsByFieldId = [];
                foreach ($entity->getCustomFieldValues() as $cfv) {
                    $fieldId = $cfv->getCustomField()?->getId();
                    if (null !== $fieldId) {
                        $currentCfvsByFieldId[$fieldId] = [
                            'fieldName' => $cfv->getCustomField()->getName(),
                            'value'     => $cfv->getValue(),
                        ];
                    }
                }

                foreach ($snapshotCfvs as $cfvData) {
                    $fieldId   = $cfvData['fieldId'] ?? null;
                    $fieldName = $cfvData['fieldName'] ?? $fieldId;
                    if (!$fieldId) {
                        continue;
                    }
                    $currentValue  = $currentCfvsByFieldId[$fieldId]['value'] ?? null;
                    $restoredValue = $cfvData['value'] ?? null;
                    if ((string) ($currentValue ?? '') !== (string) ($restoredValue ?? '')) {
                        $diff['customField:'.$fieldName] = ['current' => $currentValue, 'restored' => $restoredValue];
                    }
                }
            }
        }

        return $diff;
    }

    private function applyRestore(string $entityType, object $entity, array $snapshot, string $restoreMode): void
    {
        // Restore base fields
        $baseFields = self::BASE_FIELDS[$entityType] ?? [];
        foreach ($baseFields as $field) {
            $setter = 'set'.ucfirst($field);
            if (method_exists($entity, $setter) && array_key_exists($field, $snapshot)) {
                $entity->{$setter}($snapshot[$field]);
            }
        }

        // Restore constructeurs
        $this->restoreConstructeurs($entity, $snapshot);

        // Machine: restore site
        if ('machine' === $entityType && isset($snapshot['site'])) {
            $siteId = is_array($snapshot['site']) ? ($snapshot['site']['id'] ?? null) : $snapshot['site'];
            if ($siteId) {
                $site = $this->sites->find($siteId);
                if (null !== $site) {
                    $entity->setSite($site);
                }
            }
        }

        if ('partial' === $restoreMode) {
            return;
        }

        // Full mode: restore slots
        match ($entityType) {
            'composant' => $this->restoreComposantSlots($entity, $snapshot),
            'piece'     => $this->restorePieceSlots($entity, $snapshot),
            default     => null,
        };

        // Machine: restore links
        if ('machine' === $entityType) {
            $this->restoreMachineLinks($entity, $snapshot);
        }

        // Full mode: restore custom field values
        $this->restoreCustomFieldValues($entityType, $entity, $snapshot);
    }

    private function restoreConstructeurs(object $entity, array $snapshot): void
    {
        if (!method_exists($entity, 'getConstructeurLinks')) {
            return;
        }

        $targetIds  = [];
        $targetRefs = [];
        foreach ($snapshot['constructeurIds'] ?? [] as $entry) {
            $id = is_array($entry) ? ($entry['id'] ?? null) : $entry;
            if ($id) {
                $targetIds[]     = $id;
                $targetRefs[$id] = is_array($entry) ? ($entry['supplierReference'] ?? null) : null;
            }
        }

        // Remove current links not in snapshot
        foreach ($entity->getConstructeurLinks()->toArray() as $link) {
            $cId = $link->getConstructeur()->getId();
            if (!in_array($cId, $targetIds, true)) {
                $this->em->remove($link);
            } else {
                // Update supplierReference if present in snapshot
                $link->setSupplierReference($targetRefs[$cId] ?? null);
            }
        }

        // Add missing constructeur links from snapshot
        $currentIds = array_map(
            fn ($link) => $link->getConstructeur()->getId(),
            $entity->getConstructeurLinks()->toArray(),
        );
        foreach ($targetIds as $id) {
            if (!in_array($id, $currentIds, true)) {
                $constructeur = $this->constructeurs->find($id);
                if (null !== $constructeur) {
                    $link = $this->createConstructeurLink($entity);
                    $link->setConstructeur($constructeur);
                    $link->setSupplierReference($targetRefs[$id] ?? null);
                    $this->em->persist($link);
                }
            }
        }
    }

    private function createConstructeurLink(object $entity): object
    {
        if ($entity instanceof Machine) {
            $link = new MachineConstructeurLink();
            $link->setMachine($entity);

            return $link;
        }
        if ($entity instanceof Piece) {
            $link = new PieceConstructeurLink();
            $link->setPiece($entity);

            return $link;
        }
        if ($entity instanceof Composant) {
            $link = new ComposantConstructeurLink();
            $link->setComposant($entity);

            return $link;
        }
        if ($entity instanceof Product) {
            $link = new ProductConstructeurLink();
            $link->setProduct($entity);

            return $link;
        }

        throw new LogicException('Unsupported entity type for constructeur link');
    }

    private function restoreMachineLinks(Machine $machine, array $snapshot): void
    {
        // Remove all existing links
        foreach ($machine->getProductLinks()->toArray() as $link) {
            $this->em->remove($link);
        }
        foreach ($machine->getPieceLinks()->toArray() as $link) {
            $this->em->remove($link);
        }
        foreach ($machine->getComponentLinks()->toArray() as $link) {
            $this->em->remove($link);
        }

        // Flush removals to avoid FK conflicts
        $this->em->flush();

        // Recreate component links
        foreach ($snapshot['componentLinks'] ?? [] as $data) {
            $composant = !empty($data['composantId']) ? $this->composants->find($data['composantId']) : null;
            if (null === $composant) {
                continue;
            }
            $link = new MachineComponentLink();
            $link->setMachine($machine);
            $link->setComposant($composant);
            $this->em->persist($link);
        }

        // Recreate piece links
        foreach ($snapshot['pieceLinks'] ?? [] as $data) {
            $piece = !empty($data['pieceId']) ? $this->pieces->find($data['pieceId']) : null;
            if (null === $piece) {
                continue;
            }
            $link = new MachinePieceLink();
            $link->setMachine($machine);
            $link->setPiece($piece);
            if (isset($data['quantity']) && $data['quantity'] > 0) {
                $link->setQuantity((int) $data['quantity']);
            }
            $this->em->persist($link);
        }

        // Recreate product links
        foreach ($snapshot['productLinks'] ?? [] as $data) {
            $product = !empty($data['productId']) ? $this->products->find($data['productId']) : null;
            if (null === $product) {
                continue;
            }
            $link = new MachineProductLink();
            $link->setMachine($machine);
            $link->setProduct($product);
            $this->em->persist($link);
        }
    }

    private function restoreComposantSlots(Composant $entity, array $snapshot): void
    {
        // Clear existing slots
        foreach ($entity->getPieceSlots()->toArray() as $slot) {
            $this->em->remove($slot);
        }
        foreach ($entity->getSubcomponentSlots()->toArray() as $slot) {
            $this->em->remove($slot);
        }
        foreach ($entity->getProductSlots()->toArray() as $slot) {
            $this->em->remove($slot);
        }

        // Flush removals first to avoid FK constraint conflicts with new slots
        $this->em->flush();

        // Recreate piece slots
        foreach ($snapshot['pieceSlots'] ?? [] as $slotData) {
            $slot = new ComposantPieceSlot();
            $slot->setComposant($entity);
            $slot->setQuantity($slotData['quantity'] ?? 1);
            $slot->setPosition($slotData['position'] ?? 0);
            if (!empty($slotData['typePieceId'])) {
                $slot->setTypePiece($this->modelTypes->find($slotData['typePieceId']));
            }
            if (!empty($slotData['selectedPieceId'])) {
                $piece = $this->pieces->find($slotData['selectedPieceId']);
                if (null !== $piece) {
                    $slot->setSelectedPiece($piece);
                }
            }
            $this->em->persist($slot);
        }

        // Recreate subcomponent slots
        foreach ($snapshot['subcomponentSlots'] ?? [] as $slotData) {
            $slot = new ComposantSubcomponentSlot();
            $slot->setComposant($entity);
            $slot->setAlias($slotData['alias'] ?? null);
            $slot->setFamilyCode($slotData['familyCode'] ?? null);
            $slot->setPosition($slotData['position'] ?? 0);
            if (!empty($slotData['typeComposantId'])) {
                $slot->setTypeComposant($this->modelTypes->find($slotData['typeComposantId']));
            }
            if (!empty($slotData['selectedComposantId'])) {
                $composant = $this->composants->find($slotData['selectedComposantId']);
                if (null !== $composant) {
                    $slot->setSelectedComposant($composant);
                }
            }
            $this->em->persist($slot);
        }

        // Recreate product slots
        foreach ($snapshot['productSlots'] ?? [] as $slotData) {
            $slot = new ComposantProductSlot();
            $slot->setComposant($entity);
            $slot->setFamilyCode($slotData['familyCode'] ?? null);
            $slot->setPosition($slotData['position'] ?? 0);
            if (!empty($slotData['typeProductId'])) {
                $slot->setTypeProduct($this->modelTypes->find($slotData['typeProductId']));
            }
            if (!empty($slotData['selectedProductId'])) {
                $product = $this->products->find($slotData['selectedProductId']);
                if (null !== $product) {
                    $slot->setSelectedProduct($product);
                }
            }
            $this->em->persist($slot);
        }
    }

    private function restorePieceSlots(Piece $entity, array $snapshot): void
    {
        foreach ($entity->getProductSlots()->toArray() as $slot) {
            $this->em->remove($slot);
        }

        // Flush removals first to avoid FK constraint conflicts
        $this->em->flush();

        foreach ($snapshot['productSlots'] ?? [] as $slotData) {
            $slot = new PieceProductSlot();
            $slot->setPiece($entity);
            $slot->setFamilyCode($slotData['familyCode'] ?? null);
            $slot->setPosition($slotData['position'] ?? 0);
            if (!empty($slotData['typeProductId'])) {
                $slot->setTypeProduct($this->modelTypes->find($slotData['typeProductId']));
            }
            if (!empty($slotData['selectedProductId'])) {
                $product = $this->products->find($slotData['selectedProductId']);
                if (null !== $product) {
                    $slot->setSelectedProduct($product);
                }
            }
            $this->em->persist($slot);
        }
    }

    private function restoreCustomFieldValues(string $entityType, object $entity, array $snapshot): void
    {
        $snapshotCfvs = $snapshot['customFieldValues'] ?? [];
        if ([] === $snapshotCfvs) {
            return;
        }

        // Build a map of current CFVs by fieldId for lookup
        $currentCfvsByFieldId = [];
        if (method_exists($entity, 'getCustomFieldValues')) {
            foreach ($entity->getCustomFieldValues() as $cfv) {
                $fieldId = $cfv->getCustomField()?->getId();
                if (null !== $fieldId) {
                    $currentCfvsByFieldId[$fieldId] = $cfv;
                }
            }
        }

        foreach ($snapshotCfvs as $cfvData) {
            $fieldId = $cfvData['fieldId'] ?? null;
            if (!$fieldId) {
                continue;
            }

            // Try to find the current CFV by fieldId (resilient to ID changes after sync)
            $cfv = $currentCfvsByFieldId[$fieldId] ?? null;

            // Fallback: try by original ID if fieldId lookup failed
            if (null === $cfv && !empty($cfvData['id'])) {
                $cfv = $this->customFieldValues->find($cfvData['id']);
            }

            if (null !== $cfv) {
                $cfv->setValue($cfvData['value'] ?? null);
            }
        }
    }

    /**
     * Build a complete snapshot of the entity in its current state (after restore).
     * Must be consistent with the snapshots built by the audit subscribers,
     * so that a future restore from a "restore" version works correctly.
     */
    private function buildCurrentSnapshot(string $entityType, object $entity): array
    {
        $snapshot = ['id' => $entity->getId()];

        // Base fields
        $baseFields = self::BASE_FIELDS[$entityType] ?? [];
        foreach ($baseFields as $field) {
            $getter = 'get'.ucfirst($field);
            if (method_exists($entity, $getter)) {
                $snapshot[$field] = $entity->{$getter}();
            }
        }

        // Version
        if (method_exists($entity, 'getVersion')) {
            $snapshot['version'] = $entity->getVersion();
        }

        // Constructeur links
        if (method_exists($entity, 'getConstructeurLinks')) {
            $snapshot['constructeurLinks'] = [];
            foreach ($entity->getConstructeurLinks() as $link) {
                $snapshot['constructeurLinks'][] = [
                    'id'                => $link->getId(),
                    'constructeurId'    => $link->getConstructeur()->getId(),
                    'constructeurName'  => $link->getConstructeur()->getName(),
                    'supplierReference' => $link->getSupplierReference(),
                ];
            }
        }

        // Type (ModelType reference)
        $typeGetters = ['composant' => 'getTypeComposant', 'piece' => 'getTypePiece', 'product' => 'getTypeProduct'];
        $typeKeys    = ['composant' => 'typeComposant', 'piece' => 'typePiece', 'product' => 'typeProduct'];
        if (isset($typeGetters[$entityType]) && method_exists($entity, $typeGetters[$entityType])) {
            $type                             = $entity->{$typeGetters[$entityType]}();
            $snapshot[$typeKeys[$entityType]] = $type ? ['id' => $type->getId(), 'name' => $type->getName(), 'code' => $type->getCode()] : null;
        }

        // Machine: site
        if ('machine' === $entityType && method_exists($entity, 'getSite')) {
            $site             = $entity->getSite();
            $snapshot['site'] = $site ? ['id' => $site->getId(), 'name' => $site->getName()] : null;
        }

        // Machine: links
        if ('machine' === $entityType) {
            $snapshot['componentLinks'] = [];
            foreach ($entity->getComponentLinks() as $link) {
                $snapshot['componentLinks'][] = [
                    'id'            => $link->getId(), 'composantId' => $link->getComposant()?->getId(),
                    'composantName' => $link->getComposant()?->getName(),
                    'modelTypeId'   => $link->getModelType()?->getId(),
                ];
            }
            $snapshot['pieceLinks'] = [];
            foreach ($entity->getPieceLinks() as $link) {
                $snapshot['pieceLinks'][] = [
                    'id'          => $link->getId(), 'pieceId' => $link->getPiece()?->getId(),
                    'pieceName'   => $link->getPiece()?->getName(), 'quantity' => $link->getQuantity(),
                    'modelTypeId' => $link->getModelType()?->getId(),
                ];
            }
            $snapshot['productLinks'] = [];
            foreach ($entity->getProductLinks() as $link) {
                $snapshot['productLinks'][] = [
                    'id'          => $link->getId(), 'productId' => $link->getProduct()?->getId(),
                    'productName' => $link->getProduct()?->getName(),
                    'modelTypeId' => $link->getModelType()?->getId(),
                ];
            }
        }

        // Composant/Piece: product
        if (in_array($entityType, ['composant', 'piece'], true) && method_exists($entity, 'getProduct')) {
            $product             = $entity->getProduct();
            $snapshot['product'] = $product ? ['id' => $product->getId(), 'name' => $product->getName()] : null;
        }

        // Slots
        if ('composant' === $entityType) {
            $snapshot['pieceSlots'] = [];
            foreach ($entity->getPieceSlots() as $slot) {
                $snapshot['pieceSlots'][] = [
                    'id'              => $slot->getId(), 'typePieceId' => $slot->getTypePiece()?->getId(),
                    'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
                    'quantity'        => $slot->getQuantity(), 'position' => $slot->getPosition(),
                ];
            }
            $snapshot['subcomponentSlots'] = [];
            foreach ($entity->getSubcomponentSlots() as $slot) {
                $snapshot['subcomponentSlots'][] = [
                    'id'                  => $slot->getId(), 'alias' => $slot->getAlias(), 'familyCode' => $slot->getFamilyCode(),
                    'typeComposantId'     => $slot->getTypeComposant()?->getId(),
                    'selectedComposantId' => $slot->getSelectedComposant()?->getId(),
                    'position'            => $slot->getPosition(),
                ];
            }
            $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots());
        }

        if ('piece' === $entityType) {
            $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots());
        }

        // Custom field values
        if (method_exists($entity, 'getCustomFieldValues')) {
            $snapshot['customFieldValues'] = [];
            foreach ($entity->getCustomFieldValues() as $cfv) {
                $snapshot['customFieldValues'][] = [
                    'id'      => $cfv->getId(), 'fieldName' => $cfv->getCustomField()?->getName(),
                    'fieldId' => $cfv->getCustomField()?->getId(), 'value' => $cfv->getValue(),
                ];
            }
        }

        return $snapshot;
    }

    /**
     * @param iterable<ComposantProductSlot|PieceProductSlot> $slots
     *
     * @return list<array<string, mixed>>
     */
    private function serializeProductSlots(iterable $slots): array
    {
        $serialized = [];
        foreach ($slots as $slot) {
            $serialized[] = [
                'id'                => $slot->getId(),
                'typeProductId'     => $slot->getTypeProduct()?->getId(),
                'selectedProductId' => $slot->getSelectedProduct()?->getId(),
                'familyCode'        => $slot->getFamilyCode(),
                'position'          => $slot->getPosition(),
            ];
        }

        return $serialized;
    }
}
