<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Composant;
use App\Entity\Constructeur;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\Machine;
use App\Entity\MachineComponentLink;
use App\Entity\MachineConstructeurLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\Product;
use App\Entity\Site;
use App\Repository\ComposantRepository;
use App\Repository\MachineComponentLinkRepository;
use App\Repository\MachinePieceLinkRepository;
use App\Repository\MachineProductLinkRepository;
use App\Repository\MachineRepository;
use App\Repository\PieceRepository;
use App\Repository\ProductRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/machines')]
class MachineStructureController extends AbstractController
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly MachineRepository $machineRepository,
        private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
        private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
        private readonly MachineProductLinkRepository $machineProductLinkRepository,
        private readonly ComposantRepository $composantRepository,
        private readonly PieceRepository $pieceRepository,
        private readonly ProductRepository $productRepository,
    ) {}

    #[Route('/{id}/structure', name: 'machine_structure_get', methods: ['GET'])]
    public function getStructure(string $id): JsonResponse
    {
        $this->denyAccessUnlessGranted('ROLE_VIEWER');

        $machine = $this->machineRepository->find($id);
        if (!$machine instanceof Machine) {
            return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
        }

        $componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']);
        $pieceLinks     = $this->machinePieceLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']);
        $productLinks   = $this->machineProductLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']);

        return $this->json($this->normalizeStructureResponse(
            $machine,
            $componentLinks,
            $pieceLinks,
            $productLinks
        ));
    }

    #[Route('/{id}/structure', name: 'machine_structure_update', methods: ['PATCH'])]
    public function updateStructure(string $id, Request $request): JsonResponse
    {
        $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');

        $machine = $this->machineRepository->find($id);
        if (!$machine instanceof Machine) {
            return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
        }

        $payload = json_decode($request->getContent(), true);
        if (!is_array($payload)) {
            return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
        }

        $componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []);
        $pieceLinksPayload     = $this->normalizePayloadList($payload['pieceLinks'] ?? []);
        $productLinksPayload   = $this->normalizePayloadList($payload['productLinks'] ?? []);

        $componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload);
        if ($componentLinks instanceof JsonResponse) {
            return $componentLinks;
        }

        $pieceLinks = $this->applyPieceLinks($machine, $pieceLinksPayload, $componentLinks);
        if ($pieceLinks instanceof JsonResponse) {
            return $pieceLinks;
        }

        $productLinks = $this->applyProductLinks($machine, $productLinksPayload, $componentLinks, $pieceLinks);
        if ($productLinks instanceof JsonResponse) {
            return $productLinks;
        }

        $this->entityManager->flush();

        return $this->json($this->normalizeStructureResponse(
            $machine,
            $componentLinks,
            $pieceLinks,
            $productLinks
        ));
    }

    #[Route('/{id}/clone', name: 'machine_clone', methods: ['POST'])]
    public function cloneMachine(string $id, Request $request): JsonResponse
    {
        $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');

        $source = $this->machineRepository->find($id);
        if (!$source instanceof Machine) {
            return $this->json(['success' => false, 'error' => 'Machine source introuvable.'], 404);
        }

        $payload = json_decode($request->getContent(), true);
        if (!is_array($payload) || empty($payload['name']) || empty($payload['siteId'])) {
            return $this->json(['success' => false, 'error' => 'name et siteId sont requis.'], 400);
        }

        $site = $this->entityManager->getRepository(Site::class)->find($payload['siteId']);
        if (!$site) {
            return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
        }

        // Clone mode: 'full' copies concrete components/pieces/products; 'structure'
        // only keeps the slots' categories (modelType) with empty concrete entities.
        $mode = $payload['mode'] ?? 'full';
        if (!in_array($mode, ['full', 'structure'], true)) {
            return $this->json(['success' => false, 'error' => 'mode invalide (valeurs autorisées : full, structure).'], 400);
        }
        $structureOnly = 'structure' === $mode;

        // Create new machine
        $newMachine = new Machine();
        $newMachine->setName($payload['name']);
        $newMachine->setSite($site);
        if (!empty($payload['reference'])) {
            $newMachine->setReference($payload['reference']);
        }
        $newMachine->setPrix($source->getPrix());

        // Copy constructeur links
        foreach ($source->getConstructeurLinks() as $link) {
            $newLink = new MachineConstructeurLink();
            $newLink->setMachine($newMachine);
            $newLink->setConstructeur($link->getConstructeur());
            $newLink->setSupplierReference($link->getSupplierReference());
            $this->entityManager->persist($newLink);
        }

        $this->entityManager->persist($newMachine);

        // Copy custom fields and values
        $this->cloneCustomFields($source, $newMachine);

        // Copy component links (preserving hierarchy)
        $componentLinkMap = $this->cloneComponentLinks($source, $newMachine, $structureOnly);

        // Copy piece links
        $pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap, $structureOnly);

        // Copy product links
        $this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap, $structureOnly);

        $this->entityManager->flush();

        $componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']);
        $pieceLinks     = $this->machinePieceLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']);
        $productLinks   = $this->machineProductLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']);

        return $this->json($this->normalizeStructureResponse(
            $newMachine,
            $componentLinks,
            $pieceLinks,
            $productLinks
        ), 201);
    }

    private function cloneCustomFields(Machine $source, Machine $target): void
    {
        $cfMap = [];

        foreach ($source->getCustomFields() as $cf) {
            $newCf = new CustomField();
            $newCf->setName($cf->getName());
            $newCf->setType($cf->getType());
            $newCf->setRequired($cf->isRequired());
            $newCf->setDefaultValue($cf->getDefaultValue());
            $newCf->setOptions($cf->getOptions());
            $newCf->setOrderIndex($cf->getOrderIndex());
            $newCf->setMachineContextOnly($cf->isMachineContextOnly());
            $newCf->setMachine($target);
            $this->entityManager->persist($newCf);

            $cfMap[$cf->getId()] = $newCf;
        }

        foreach ($source->getCustomFieldValues() as $cfv) {
            $originalCf = $cfv->getCustomField();
            $newCf      = $cfMap[$originalCf->getId()] ?? null;
            if (!$newCf) {
                continue;
            }

            $newValue = new CustomFieldValue();
            $newValue->setMachine($target);
            $newValue->setCustomField($newCf);
            $newValue->setValue($cfv->getValue());
            $this->entityManager->persist($newValue);
        }
    }

    /**
     * @return array<string, MachineComponentLink> Map of old link ID → new link
     */
    private function cloneComponentLinks(Machine $source, Machine $target, bool $structureOnly = false): array
    {
        $sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
        $linkMap     = [];

        // First pass: create all links without parent relationships
        foreach ($sourceLinks as $link) {
            $newLink = new MachineComponentLink();
            $newLink->setMachine($target);

            if ($structureOnly) {
                // Keep only the slot category; leave the concrete component empty.
                $newLink->setModelType($link->getModelType() ?? $link->getComposant()?->getTypeComposant());
                $this->entityManager->persist($newLink);
                $linkMap[$link->getId()] = $newLink;

                continue;
            }

            $newLink->setComposant($link->getComposant());
            $newLink->setNameOverride($link->getNameOverride());
            $newLink->setReferenceOverride($link->getReferenceOverride());
            $newLink->setPrixOverride($link->getPrixOverride());
            $this->entityManager->persist($newLink);

            foreach ($link->getContextFieldValues() as $cfv) {
                $newValue = new CustomFieldValue();
                $newValue->setCustomField($cfv->getCustomField());
                $newValue->setValue($cfv->getValue());
                $newValue->setMachineComponentLink($newLink);
                $newValue->setComposant($newLink->getComposant());
                $this->entityManager->persist($newValue);
                $newLink->getContextFieldValues()->add($newValue);
            }

            $linkMap[$link->getId()] = $newLink;
        }

        // Second pass: set parent relationships
        foreach ($sourceLinks as $link) {
            $parent = $link->getParentLink();
            if ($parent && isset($linkMap[$parent->getId()])) {
                $linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
            }
        }

        return $linkMap;
    }

    /**
     * @param array<string, MachineComponentLink> $componentLinkMap
     *
     * @return array<string, MachinePieceLink> Map of old link ID → new link
     */
    private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap, bool $structureOnly = false): array
    {
        $sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
        $linkMap     = [];

        foreach ($sourceLinks as $link) {
            $newLink = new MachinePieceLink();
            $newLink->setMachine($target);

            $parent = $link->getParentLink();
            if ($parent && isset($componentLinkMap[$parent->getId()])) {
                $newLink->setParentLink($componentLinkMap[$parent->getId()]);
            }

            if ($structureOnly) {
                // Keep only the slot category; leave the concrete piece empty.
                $newLink->setModelType($link->getModelType() ?? $link->getPiece()?->getTypePiece());
                $this->entityManager->persist($newLink);
                $linkMap[$link->getId()] = $newLink;

                continue;
            }

            $newLink->setPiece($link->getPiece());
            $newLink->setNameOverride($link->getNameOverride());
            $newLink->setReferenceOverride($link->getReferenceOverride());
            $newLink->setPrixOverride($link->getPrixOverride());
            $newLink->setQuantity($link->getQuantity());

            $this->entityManager->persist($newLink);

            foreach ($link->getContextFieldValues() as $cfv) {
                $newValue = new CustomFieldValue();
                $newValue->setCustomField($cfv->getCustomField());
                $newValue->setValue($cfv->getValue());
                $newValue->setMachinePieceLink($newLink);
                $newValue->setPiece($newLink->getPiece());
                $this->entityManager->persist($newValue);
                $newLink->getContextFieldValues()->add($newValue);
            }

            $linkMap[$link->getId()] = $newLink;
        }

        return $linkMap;
    }

    /**
     * @param array<string, MachineComponentLink> $componentLinkMap
     * @param array<string, MachinePieceLink>     $pieceLinkMap
     */
    private function cloneProductLinks(
        Machine $source,
        Machine $target,
        array $componentLinkMap,
        array $pieceLinkMap,
        bool $structureOnly = false,
    ): void {
        $sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
        $linkMap     = [];

        // First pass: create all links
        foreach ($sourceLinks as $link) {
            $newLink = new MachineProductLink();
            $newLink->setMachine($target);

            if ($structureOnly) {
                // Keep only the slot category; leave the concrete product empty.
                $newLink->setModelType($link->getModelType() ?? $link->getProduct()?->getTypeProduct());
            } else {
                $newLink->setProduct($link->getProduct());
            }

            $parentComponent = $link->getParentComponentLink();
            if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
                $newLink->setParentComponentLink($componentLinkMap[$parentComponent->getId()]);
            }

            $parentPiece = $link->getParentPieceLink();
            if ($parentPiece && isset($pieceLinkMap[$parentPiece->getId()])) {
                $newLink->setParentPieceLink($pieceLinkMap[$parentPiece->getId()]);
            }

            $this->entityManager->persist($newLink);
            $linkMap[$link->getId()] = $newLink;
        }

        // Second pass: set parent product link relationships
        foreach ($sourceLinks as $link) {
            $parent = $link->getParentLink();
            if ($parent && isset($linkMap[$parent->getId()])) {
                $linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
            }
        }
    }

    private function normalizePayloadList(mixed $value): array
    {
        if (!is_array($value)) {
            return [];
        }

        return array_values(array_filter($value, static fn ($item) => is_array($item)));
    }

    private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse
    {
        $existing       = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']));
        $keepIds        = [];
        $pendingParents = [];
        $links          = [];

        foreach ($payload as $entry) {
            $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
            $link   = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink();
            if (!$linkId) {
                $linkId = $this->generateCuid();
            }
            if (!$link->getId()) {
                $link->setId($linkId);
            }

            $composantId = $this->resolveIdentifier($entry, ['composantId', 'componentId', 'idComposant']);
            if (!$composantId) {
                return $this->json(['success' => false, 'error' => 'Composant requis.'], 400);
            }
            $composant = $this->composantRepository->find($composantId);
            if (!$composant instanceof Composant) {
                return $this->json(['success' => false, 'error' => 'Composant introuvable.'], 404);
            }

            $link->setMachine($machine);
            $link->setComposant($composant);

            $this->applyOverrides($link, $entry['overrides'] ?? null);

            $pendingParents[$linkId] = $this->resolveIdentifier($entry, [
                'parentComponentLinkId',
                'parentLinkId',
                'parentMachineComponentLinkId',
            ]);

            $this->entityManager->persist($link);
            $links[$linkId] = $link;
            $keepIds[]      = $linkId;
        }

        foreach ($pendingParents as $linkId => $parentId) {
            if (!$parentId || !isset($links[$linkId])) {
                continue;
            }
            $parent = $links[$parentId] ?? $existing[$parentId] ?? null;
            if ($parent instanceof MachineComponentLink) {
                $links[$linkId]->setParentLink($parent);
            }
        }

        $this->removeMissingLinks($existing, $keepIds);

        return array_values($links);
    }

    private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse
    {
        $existing       = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']));
        $componentIndex = $this->indexLinksById($componentLinks);
        $keepIds        = [];
        $pendingParents = [];
        $links          = [];

        foreach ($payload as $entry) {
            $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
            $link   = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink();
            if (!$linkId) {
                $linkId = $this->generateCuid();
            }
            if (!$link->getId()) {
                $link->setId($linkId);
            }

            $pieceId = $this->resolveIdentifier($entry, ['pieceId']);
            if (!$pieceId) {
                return $this->json(['success' => false, 'error' => 'Pièce requise.'], 400);
            }
            $piece = $this->pieceRepository->find($pieceId);
            if (!$piece instanceof Piece) {
                return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404);
            }

            $link->setMachine($machine);
            $link->setPiece($piece);

            $this->applyOverrides($link, $entry['overrides'] ?? null);

            if (!isset($entry['parentComponentLinkId']) && !isset($entry['parentLinkId'])) {
                $quantity = isset($entry['quantity']) ? (int) $entry['quantity'] : $link->getQuantity();
                $link->setQuantity(max(1, $quantity));
            }

            $pendingParents[$linkId] = $this->resolveIdentifier($entry, [
                'parentComponentLinkId',
                'parentLinkId',
                'parentMachineComponentLinkId',
            ]);

            $this->entityManager->persist($link);
            $links[$linkId] = $link;
            $keepIds[]      = $linkId;
        }

        foreach ($pendingParents as $linkId => $parentId) {
            if (!$parentId || !isset($links[$linkId])) {
                continue;
            }
            $parent = $componentIndex[$parentId] ?? null;
            if ($parent instanceof MachineComponentLink) {
                $links[$linkId]->setParentLink($parent);
            }
        }

        $this->removeMissingLinks($existing, $keepIds);

        return array_values($links);
    }

    private function applyProductLinks(
        Machine $machine,
        array $payload,
        array $componentLinks,
        array $pieceLinks,
    ): array|JsonResponse {
        $existing       = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']));
        $componentIndex = $this->indexLinksById($componentLinks);
        $pieceIndex     = $this->indexLinksById($pieceLinks);
        $keepIds        = [];
        $pendingParents = [];
        $links          = [];

        foreach ($payload as $entry) {
            $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
            $link   = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink();
            if (!$linkId) {
                $linkId = $this->generateCuid();
            }
            if (!$link->getId()) {
                $link->setId($linkId);
            }

            $productId = $this->resolveIdentifier($entry, ['productId']);
            if (!$productId) {
                return $this->json(['success' => false, 'error' => 'Produit requis.'], 400);
            }
            $product = $this->productRepository->find($productId);
            if (!$product instanceof Product) {
                return $this->json(['success' => false, 'error' => 'Produit introuvable.'], 404);
            }

            $link->setMachine($machine);
            $link->setProduct($product);

            $pendingParents[$linkId] = [
                'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']),
                'parentPieceLinkId'     => $this->resolveIdentifier($entry, ['parentPieceLinkId']),
                'parentLinkId'          => $this->resolveIdentifier($entry, ['parentLinkId']),
            ];

            $this->entityManager->persist($link);
            $links[$linkId] = $link;
            $keepIds[]      = $linkId;
        }

        foreach ($pendingParents as $linkId => $parentIds) {
            if (!isset($links[$linkId])) {
                continue;
            }
            if (!empty($parentIds['parentComponentLinkId']) && isset($componentIndex[$parentIds['parentComponentLinkId']])) {
                $links[$linkId]->setParentComponentLink($componentIndex[$parentIds['parentComponentLinkId']]);
            }
            if (!empty($parentIds['parentPieceLinkId']) && isset($pieceIndex[$parentIds['parentPieceLinkId']])) {
                $links[$linkId]->setParentPieceLink($pieceIndex[$parentIds['parentPieceLinkId']]);
            }
            if (!empty($parentIds['parentLinkId']) && isset($links[$parentIds['parentLinkId']])) {
                $links[$linkId]->setParentLink($links[$parentIds['parentLinkId']]);
            }
        }

        $this->removeMissingLinks($existing, $keepIds);

        return array_values($links);
    }

    private function normalizeStructureResponse(
        Machine $machine,
        array $componentLinks,
        array $pieceLinks,
        array $productLinks,
    ): array {
        $normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
        $componentIndex           = $this->indexNormalizedLinks($normalizedComponentLinks);
        $normalizedPieceLinks     = $this->normalizePieceLinks($pieceLinks);

        $childIds = [];
        foreach ($normalizedComponentLinks as $link) {
            $parentId = $link['parentComponentLinkId'] ?? null;
            if ($parentId && isset($componentIndex[$parentId])) {
                $componentIndex[$parentId]['childLinks'][] = $link;
                $childIds[$link['id']]                     = true;
            }
        }

        $this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);

        $rootComponents = array_filter(
            $componentIndex,
            static fn (array $link) => !isset($childIds[$link['id']]),
        );

        return [
            'machine'        => $this->normalizeMachine($machine),
            'componentLinks' => array_values($rootComponents),
            'pieceLinks'     => $normalizedPieceLinks,
            'productLinks'   => $this->normalizeProductLinks($productLinks),
        ];
    }

    private function attachPiecesToComponents(array &$componentIndex, array $pieceLinks): void
    {
        foreach ($pieceLinks as $pieceLink) {
            $parentId = $pieceLink['parentComponentLinkId'] ?? null;
            if ($parentId && isset($componentIndex[$parentId])) {
                $componentIndex[$parentId]['pieceLinks'][] = $pieceLink;
            }
        }

        foreach ($componentIndex as &$component) {
            if (!empty($component['childLinks'])) {
                $this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
            }
        }
    }

    private function attachPiecesToChildComponents(array &$childLinks, array $pieceLinks): void
    {
        foreach ($childLinks as &$child) {
            $childId = $child['id'] ?? $child['linkId'] ?? null;
            if ($childId) {
                foreach ($pieceLinks as $pieceLink) {
                    $parentId = $pieceLink['parentComponentLinkId'] ?? null;
                    if ($parentId === $childId) {
                        $child['pieceLinks'][] = $pieceLink;
                    }
                }
            }

            if (!empty($child['childLinks'])) {
                $this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
            }
        }
    }

    private function normalizeMachine(Machine $machine): array
    {
        $site = $machine->getSite();

        return [
            'id'        => $machine->getId(),
            'name'      => $machine->getName(),
            'reference' => $machine->getReference(),
            'prix'      => $machine->getPrix(),
            'siteId'    => $site->getId(),
            'site'      => [
                'id'   => $site->getId(),
                'name' => $site->getName(),
            ],
            'constructeurs'     => $this->normalizeConstructeurLinks($machine->getConstructeurLinks()),
            'customFields'      => $this->normalizeCustomFields($machine->getCustomFields()),
            'documents'         => null,
            'customFieldValues' => $this->normalizeCustomFieldValues($machine->getCustomFieldValues()),
        ];
    }

    private function normalizeCustomFields(Collection $customFields): array
    {
        $items = [];
        foreach ($customFields as $customField) {
            if (!$customField instanceof CustomField) {
                continue;
            }
            $items[] = [
                'id'                 => $customField->getId(),
                'name'               => $customField->getName(),
                'type'               => $customField->getType(),
                'required'           => $customField->isRequired(),
                'options'            => $customField->getOptions(),
                'defaultValue'       => $customField->getDefaultValue(),
                'orderIndex'         => $customField->getOrderIndex(),
                'machineContextOnly' => $customField->isMachineContextOnly(),
            ];
        }

        return $items;
    }

    private function normalizeComponentLinks(array $links): array
    {
        return array_map(function (MachineComponentLink $link): array {
            $composant  = $link->getComposant();
            $modelType  = $link->getModelType();
            $parentLink = $link->getParentLink();
            $type       = $composant?->getTypeComposant();

            return [
                'id'                       => $link->getId(),
                'linkId'                   => $link->getId(),
                'machineId'                => $link->getMachine()->getId(),
                'composantId'              => $composant?->getId(),
                'composant'                => $composant ? $this->normalizeComposant($composant) : null,
                'modelTypeId'              => $modelType?->getId(),
                'modelType'                => $modelType ? $this->normalizeModelType($modelType) : null,
                'pendingEntity'            => null === $composant,
                'parentLinkId'             => $parentLink?->getId(),
                'parentComponentLinkId'    => $parentLink?->getId(),
                'parentComponentId'        => $parentLink?->getComposant()?->getId(),
                'overrides'                => $this->normalizeOverrides($link),
                'childLinks'               => [],
                'pieceLinks'               => [],
                'contextCustomFields'      => $type ? $this->normalizeContextCustomFieldDefinitions($type->getComponentCustomFields()) : [],
                'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()),
            ];
        }, $links);
    }

    private function normalizePieceLinks(array $links): array
    {
        return array_map(function (MachinePieceLink $link): array {
            $piece      = $this->ensurePieceExists($link->getPiece());
            $modelType  = $link->getModelType();
            $parentLink = $link->getParentLink();
            $type       = $piece?->getTypePiece();

            return [
                'id'                       => $link->getId(),
                'linkId'                   => $link->getId(),
                'machineId'                => $link->getMachine()->getId(),
                'pieceId'                  => $piece?->getId(),
                'piece'                    => $piece ? $this->normalizePiece($piece) : null,
                'modelTypeId'              => $modelType?->getId(),
                'modelType'                => $modelType ? $this->normalizeModelType($modelType) : null,
                'pendingEntity'            => null === $piece,
                'parentLinkId'             => $parentLink?->getId(),
                'parentComponentLinkId'    => $parentLink?->getId(),
                'parentComponentId'        => $parentLink?->getComposant()?->getId(),
                'overrides'                => $this->normalizeOverrides($link),
                'quantity'                 => $piece ? $this->resolvePieceQuantity($link) : 1,
                'contextCustomFields'      => $type ? $this->normalizeContextCustomFieldDefinitions($type->getPieceCustomFields()) : [],
                'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()),
            ];
        }, $links);
    }

    private function resolvePieceQuantity(MachinePieceLink $link): int
    {
        $parentLink = $link->getParentLink();
        $piece      = $this->ensurePieceExists($link->getPiece());

        if (!$parentLink || !$piece) {
            return $link->getQuantity();
        }

        $composant = $parentLink->getComposant();
        if (!$composant) {
            return $link->getQuantity();
        }

        foreach ($composant->getPieceSlots() as $slot) {
            $selected = $this->ensurePieceExists($slot->getSelectedPiece());
            if ($selected?->getId() === $piece->getId()) {
                return $slot->getQuantity();
            }
        }

        return $link->getQuantity();
    }

    private function normalizeProductLinks(array $links): array
    {
        return array_map(function (MachineProductLink $link): array {
            $product   = $link->getProduct();
            $modelType = $link->getModelType();

            return [
                'id'                    => $link->getId(),
                'linkId'                => $link->getId(),
                'machineId'             => $link->getMachine()->getId(),
                'productId'             => $product?->getId(),
                'product'               => $product ? $this->normalizeProduct($product) : null,
                'modelTypeId'           => $modelType?->getId(),
                'modelType'             => $modelType ? $this->normalizeModelType($modelType) : null,
                'pendingEntity'         => null === $product,
                'parentLinkId'          => $link->getParentLink()?->getId(),
                'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
                'parentPieceLinkId'     => $link->getParentPieceLink()?->getId(),
            ];
        }, $links);
    }

    private function normalizeComposant(Composant $composant): array
    {
        $type = $composant->getTypeComposant();

        return [
            'id'                => $composant->getId(),
            'name'              => $composant->getName(),
            'reference'         => $composant->getReference(),
            'prix'              => $composant->getPrix(),
            'typeComposantId'   => $type?->getId(),
            'typeComposant'     => $this->normalizeModelType($type),
            'productId'         => $composant->getProduct()?->getId(),
            'product'           => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
            'structure'         => $this->buildStructureFromSlots($composant),
            'constructeurs'     => $this->normalizeConstructeurLinks($composant->getConstructeurLinks()),
            'documents'         => [],
            'customFields'      => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
            'customFieldValues' => $this->normalizeCustomFieldValues($composant->getCustomFieldValues()),
        ];
    }

    private function buildStructureFromSlots(Composant $composant): array
    {
        $pieces = [];
        foreach ($composant->getPieceSlots() as $slot) {
            $selectedPiece = $this->ensurePieceExists($slot->getSelectedPiece());
            $pieceData     = [
                'slotId'          => $slot->getId(),
                'typePieceId'     => $slot->getTypePiece()?->getId(),
                'typePiece'       => $this->normalizeModelType($slot->getTypePiece()),
                'quantity'        => $slot->getQuantity(),
                'selectedPieceId' => $selectedPiece?->getId(),
            ];
            if ($selectedPiece) {
                $pieceData['resolvedPiece'] = $this->normalizePiece($selectedPiece);
            }
            $pieces[] = $pieceData;
        }

        $subcomponents = [];
        foreach ($composant->getSubcomponentSlots() as $slot) {
            $subcomponents[] = [
                'alias'               => $slot->getAlias(),
                'familyCode'          => $slot->getFamilyCode(),
                'typeComposantId'     => $slot->getTypeComposant()?->getId(),
                'selectedComponentId' => $slot->getSelectedComposant()?->getId(),
            ];
        }

        $products = [];
        foreach ($composant->getProductSlots() as $slot) {
            $products[] = [
                'typeProductId'     => $slot->getTypeProduct()?->getId(),
                'familyCode'        => $slot->getFamilyCode(),
                'selectedProductId' => $slot->getSelectedProduct()?->getId(),
            ];
        }

        return [
            'pieces'        => $pieces,
            'subcomponents' => $subcomponents,
            'products'      => $products,
        ];
    }

    /**
     * Returns the Piece if its underlying row still exists in DB, otherwise null.
     * getId() on a Doctrine proxy does NOT trigger __load() (the id is the key used
     * to build the proxy), so we force initialization via initializeObject() to
     * surface a stale FK here instead of crashing on the first real getter.
     */
    private function ensurePieceExists(?Piece $piece): ?Piece
    {
        if (null === $piece) {
            return null;
        }

        try {
            $this->entityManager->initializeObject($piece);

            return $piece;
        } catch (EntityNotFoundException) {
            return null;
        }
    }

    /**
     * Returns the CustomField if its underlying row still exists, otherwise null.
     * getId() on a Doctrine proxy does NOT trigger __load() — the id is the key used
     * to build the proxy. We force initialization explicitly so a stale FK to a
     * deleted CustomField surfaces here instead of crashing on getName() later.
     */
    private function ensureCustomFieldExists(?CustomField $cf): ?CustomField
    {
        if (null === $cf) {
            return null;
        }

        try {
            $this->entityManager->initializeObject($cf);

            return $cf;
        } catch (EntityNotFoundException) {
            return null;
        }
    }

    private function normalizePiece(Piece $piece): array
    {
        $type = $piece->getTypePiece();

        return [
            'id'                => $piece->getId(),
            'name'              => $piece->getName(),
            'reference'         => $piece->getReference(),
            'prix'              => $piece->getPrix(),
            'typePieceId'       => $type?->getId(),
            'typePiece'         => $this->normalizeModelType($type),
            'productId'         => $piece->getProduct()?->getId(),
            'product'           => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
            'constructeurs'     => $this->normalizeConstructeurLinks($piece->getConstructeurLinks()),
            'documents'         => [],
            'customFields'      => $type ? $this->normalizeCustomFieldDefinitions($type->getPieceCustomFields()) : [],
            'customFieldValues' => $this->normalizeCustomFieldValues($piece->getCustomFieldValues()),
        ];
    }

    private function normalizeProduct(Product $product): array
    {
        $type = $product->getTypeProduct();

        return [
            'id'                => $product->getId(),
            'name'              => $product->getName(),
            'reference'         => $product->getReference(),
            'supplierPrice'     => $product->getSupplierPrice(),
            'typeProductId'     => $type?->getId(),
            'typeProduct'       => $this->normalizeModelType($type),
            'constructeurs'     => $this->normalizeConstructeurLinks($product->getConstructeurLinks()),
            'documents'         => [],
            'customFields'      => $type ? $this->normalizeCustomFieldDefinitions($type->getProductCustomFields()) : [],
            'customFieldValues' => $this->normalizeCustomFieldValues($product->getCustomFieldValues()),
        ];
    }

    private function normalizeModelType(?ModelType $type): ?array
    {
        if (!$type instanceof ModelType) {
            return null;
        }

        return [
            'id'        => $type->getId(),
            'name'      => $type->getName(),
            'code'      => $type->getCode(),
            'category'  => $type->getCategory()->value,
            'structure' => $type->getStructure(),
        ];
    }

    private function normalizeConstructeurLinks(Collection $constructeurLinks): array
    {
        $items = [];
        foreach ($constructeurLinks as $link) {
            $items[] = [
                'id'           => $link->getId(),
                'constructeur' => [
                    'id'    => $link->getConstructeur()->getId(),
                    'name'  => $link->getConstructeur()->getName(),
                    'email' => $link->getConstructeur()->getEmail(),
                    'phone' => $this->constructeurPhone($link->getConstructeur()),
                ],
                'supplierReference' => $link->getSupplierReference(),
            ];
        }

        return $items;
    }

    private function constructeurPhone(Constructeur $constructeur): ?string
    {
        $first = $constructeur->getTelephones()->first();

        return false !== $first ? $first->getNumero() : null;
    }

    private function normalizeCustomFieldDefinitions(Collection $customFields): array
    {
        $items = [];
        foreach ($customFields as $cf) {
            if (!$cf instanceof CustomField) {
                continue;
            }
            $items[] = [
                'id'                 => $cf->getId(),
                'name'               => $cf->getName(),
                'type'               => $cf->getType(),
                'required'           => $cf->isRequired(),
                'options'            => $cf->getOptions(),
                'defaultValue'       => $cf->getDefaultValue(),
                'orderIndex'         => $cf->getOrderIndex(),
                'machineContextOnly' => $cf->isMachineContextOnly(),
            ];
        }

        usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);

        return $items;
    }

    private function normalizeCustomFieldValues(Collection $customFieldValues): array
    {
        $items = [];
        foreach ($customFieldValues as $cfv) {
            if (!$cfv instanceof CustomFieldValue) {
                continue;
            }
            $cf = $this->ensureCustomFieldExists($cfv->getCustomField());
            if (null === $cf) {
                continue;
            }
            $items[] = [
                'id'          => $cfv->getId(),
                'value'       => $cfv->getValue(),
                'customField' => [
                    'id'                 => $cf->getId(),
                    'name'               => $cf->getName(),
                    'type'               => $cf->getType(),
                    'required'           => $cf->isRequired(),
                    'options'            => $cf->getOptions(),
                    'defaultValue'       => $cf->getDefaultValue(),
                    'orderIndex'         => $cf->getOrderIndex(),
                    'machineContextOnly' => $cf->isMachineContextOnly(),
                ],
            ];
        }

        return $items;
    }

    private function normalizeContextCustomFieldDefinitions(Collection $customFields): array
    {
        $items = [];
        foreach ($customFields as $cf) {
            if (!$cf instanceof CustomField || !$cf->isMachineContextOnly()) {
                continue;
            }
            $items[] = [
                'id'                 => $cf->getId(),
                'name'               => $cf->getName(),
                'type'               => $cf->getType(),
                'required'           => $cf->isRequired(),
                'options'            => $cf->getOptions(),
                'defaultValue'       => $cf->getDefaultValue(),
                'orderIndex'         => $cf->getOrderIndex(),
                'machineContextOnly' => true,
            ];
        }

        usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);

        return $items;
    }

    private function normalizeOverrides(object $link): ?array
    {
        $name      = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
        $reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
        $prix      = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;

        if (null === $name && null === $reference && null === $prix) {
            return null;
        }

        return [
            'name'      => $name,
            'reference' => $reference,
            'prix'      => $prix,
        ];
    }

    private function applyOverrides(object $link, mixed $overrides): void
    {
        if (!is_array($overrides)) {
            return;
        }

        if (array_key_exists('name', $overrides) && method_exists($link, 'setNameOverride')) {
            $link->setNameOverride($this->stringOrNull($overrides['name']));
        }
        if (array_key_exists('reference', $overrides) && method_exists($link, 'setReferenceOverride')) {
            $link->setReferenceOverride($this->stringOrNull($overrides['reference']));
        }
        if (array_key_exists('prix', $overrides) && method_exists($link, 'setPrixOverride')) {
            $link->setPrixOverride($this->stringOrNull($overrides['prix']));
        }
    }

    private function stringOrNull(mixed $value): ?string
    {
        if (null === $value) {
            return null;
        }
        $string = trim((string) $value);

        return '' === $string ? null : $string;
    }

    private function resolveIdentifier(array $entry, array $keys): ?string
    {
        foreach ($keys as $key) {
            if (!array_key_exists($key, $entry)) {
                continue;
            }
            $value = $entry[$key];
            if (null === $value || '' === $value) {
                continue;
            }

            return (string) $value;
        }

        return null;
    }

    /**
     * @param array<array-key, object> $links
     *
     * @return array<string, object>
     */
    private function indexLinksById(array $links): array
    {
        $indexed = [];
        foreach ($links as $link) {
            if (method_exists($link, 'getId') && $link->getId()) {
                $indexed[$link->getId()] = $link;
            }
        }

        return $indexed;
    }

    private function indexNormalizedLinks(array $links): array
    {
        $indexed = [];
        foreach ($links as $link) {
            if (is_array($link) && isset($link['id'])) {
                $indexed[$link['id']] = $link;
            }
        }

        return $indexed;
    }

    private function removeMissingLinks(array $existing, array $keepIds): void
    {
        $keep = array_flip($keepIds);
        foreach ($existing as $link) {
            if (!method_exists($link, 'getId')) {
                continue;
            }
            $id = $link->getId();
            if ($id && !isset($keep[$id])) {
                $this->entityManager->remove($link);
            }
        }
    }

    private function generateCuid(): string
    {
        return 'cl'.bin2hex(random_bytes(12));
    }
}
