<?php

declare(strict_types=1);

namespace App\Service\Sync;

use App\DTO\SyncConfirmation;
use App\DTO\SyncExecutionResult;
use App\DTO\SyncPreviewResult;
use App\Entity\Composant;
use App\Entity\ComposantPieceSlot;
use App\Entity\ComposantProductSlot;
use App\Entity\ComposantSubcomponentSlot;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\ModelType;
use App\Entity\SkeletonPieceRequirement;
use App\Entity\SkeletonProductRequirement;
use App\Entity\SkeletonSubcomponentRequirement;
use App\Enum\ModelCategory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('app.sync_strategy')]
class ComposantSyncStrategy implements SyncStrategyInterface
{
    public function __construct(
        private readonly EntityManagerInterface $em,
    ) {}

    public function supports(ModelType $modelType): bool
    {
        return ModelCategory::COMPONENT === $modelType->getCategory();
    }

    public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
    {
        $composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]);

        $proposedPieces        = $newStructure['pieces'] ?? [];
        $proposedProducts      = $newStructure['products'] ?? [];
        $proposedSubcomponents = $newStructure['subcomponents'] ?? [];
        $proposedCustomFields  = $newStructure['customFields'] ?? [];

        $addedPieceSlots     = 0;
        $deletedPieceSlots   = 0;
        $addedProductSlots   = 0;
        $deletedProductSlots = 0;
        $addedSubSlots       = 0;
        $deletedSubSlots     = 0;
        $addedCfValues       = 0;
        $deletedCfValues     = 0;

        // Build proposed typeId lists (one entry per requirement, order = position)
        $proposedPieceTypeIds = [];
        foreach ($proposedPieces as $pp) {
            $proposedPieceTypeIds[] = $pp['typePieceId'];
        }

        $proposedProductTypeIds = [];
        foreach ($proposedProducts as $pp) {
            $proposedProductTypeIds[] = $pp['typeProductId'];
        }

        $proposedSubTypeIds = [];
        foreach ($proposedSubcomponents as $ps) {
            $proposedSubTypeIds[] = $ps['typeComposantId'];
        }

        // Map proposed custom fields by orderIndex (falls back to array index)
        $proposedCfByOrder = [];
        foreach ($proposedCustomFields as $i => $pcf) {
            $order                     = $pcf['orderIndex'] ?? $i;
            $proposedCfByOrder[$order] = $pcf;
        }

        // Get existing custom fields for this model type
        $existingFields = $this->em->getRepository(CustomField::class)->findBy(
            ['typeComposant' => $modelType],
            ['orderIndex' => 'ASC']
        );
        $existingCfByOrder = [];
        foreach ($existingFields as $field) {
            $existingCfByOrder[$field->getOrderIndex()] = $field;
        }

        // Count custom field additions/deletions (definition-level, affects all composants)
        $cfAdded   = 0;
        $cfDeleted = 0;
        foreach ($proposedCfByOrder as $orderIndex => $pcf) {
            if (!isset($existingCfByOrder[$orderIndex])) {
                ++$cfAdded;
            }
        }
        foreach ($existingCfByOrder as $orderIndex => $ef) {
            if (!isset($proposedCfByOrder[$orderIndex])) {
                ++$cfDeleted;
            }
        }

        foreach ($composants as $composant) {
            // Piece slots
            $pieceSlots         = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]);
            $existingPieceTypes = array_map(fn (ComposantPieceSlot $s) => $s->getTypePiece()?->getId() ?? '', $pieceSlots);
            $result             = $this->smartMatchPreview($existingPieceTypes, $proposedPieceTypeIds);
            $addedPieceSlots += $result['added'];
            $deletedPieceSlots += $result['deleted'];

            // Product slots
            $productSlots         = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]);
            $existingProductTypes = array_map(fn (ComposantProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlots);
            $result               = $this->smartMatchPreview($existingProductTypes, $proposedProductTypeIds);
            $addedProductSlots += $result['added'];
            $deletedProductSlots += $result['deleted'];

            // Subcomponent slots
            $subSlots         = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]);
            $existingSubTypes = array_map(fn (ComposantSubcomponentSlot $s) => $s->getTypeComposant()?->getId() ?? '', $subSlots);
            $result           = $this->smartMatchPreview($existingSubTypes, $proposedSubTypeIds);
            $addedSubSlots += $result['added'];
            $deletedSubSlots += $result['deleted'];

            // Custom field values
            $addedCfValues += $cfAdded;
            $deletedCfValues += $cfDeleted;
        }

        $itemCount = count($composants);

        return new SyncPreviewResult(
            modelTypeId: $modelType->getId(),
            category: 'component',
            itemCount: $itemCount,
            additions: [
                'pieceSlots'        => $addedPieceSlots,
                'productSlots'      => $addedProductSlots,
                'subcomponentSlots' => $addedSubSlots,
                'customFieldValues' => $addedCfValues,
            ],
            deletions: [
                'pieceSlots'        => $deletedPieceSlots,
                'productSlots'      => $deletedProductSlots,
                'subcomponentSlots' => $deletedSubSlots,
                'customFieldValues' => $deletedCfValues,
            ],
        );
    }

    public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
    {
        $composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]);

        // Load skeleton requirements
        $pieceReqs    = $this->em->getRepository(SkeletonPieceRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
        $productReqs  = $this->em->getRepository(SkeletonProductRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
        $subReqs      = $this->em->getRepository(SkeletonSubcomponentRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
        $customFields = $this->em->getRepository(CustomField::class)->findBy(
            ['typeComposant' => $modelType],
            ['orderIndex' => 'ASC']
        );

        $addedPieceSlots     = 0;
        $deletedPieceSlots   = 0;
        $addedProductSlots   = 0;
        $deletedProductSlots = 0;
        $addedSubSlots       = 0;
        $deletedSubSlots     = 0;
        $addedCfValues       = 0;
        $deletedCfValues     = 0;
        $itemsUpdated        = 0;

        foreach ($composants as $composant) {
            $changed = false;

            // --- Piece slots ---
            $pieceSlotEntities    = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]);
            $existingPieceTypeIds = array_map(fn (ComposantPieceSlot $s) => $s->getTypePiece()?->getId() ?? '', $pieceSlotEntities);
            $reqPieceTypeIds      = array_map(fn (SkeletonPieceRequirement $r) => $r->getTypePiece()->getId(), $pieceReqs);
            $matchResult          = $this->smartMatch($existingPieceTypeIds, $reqPieceTypeIds);

            // Update matched slots (position may have changed)
            foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
                $slot = $pieceSlotEntities[$slotIdx];
                $req  = $pieceReqs[$reqIdx];
                if ($slot->getPosition() !== $req->getPosition()) {
                    $slot->setPosition($req->getPosition());
                    $changed = true;
                }
            }

            // Add new piece slots for unmatched requirements
            foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
                $req  = $pieceReqs[$reqIdx];
                $slot = new ComposantPieceSlot();
                $slot->setComposant($composant);
                $slot->setTypePiece($req->getTypePiece());
                $slot->setPosition($req->getPosition());
                $this->em->persist($slot);
                ++$addedPieceSlots;
                $changed = true;
            }

            // Delete orphaned piece slots
            if ($confirmation->confirmDeletions) {
                foreach ($matchResult['orphanedSlots'] as $slotIdx) {
                    $slot = $pieceSlotEntities[$slotIdx];
                    $composant->removePieceSlot($slot);
                    $this->em->remove($slot);
                    ++$deletedPieceSlots;
                    $changed = true;
                }
            }

            // --- Product slots ---
            $productSlotEntities    = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]);
            $existingProductTypeIds = array_map(fn (ComposantProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlotEntities);
            $reqProductTypeIds      = array_map(fn (SkeletonProductRequirement $r) => $r->getTypeProduct()->getId(), $productReqs);
            $matchResult            = $this->smartMatch($existingProductTypeIds, $reqProductTypeIds);

            // Update matched slots
            foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
                $slot = $productSlotEntities[$slotIdx];
                $req  = $productReqs[$reqIdx];
                if ($slot->getPosition() !== $req->getPosition()) {
                    $slot->setPosition($req->getPosition());
                    $changed = true;
                }
                if ($slot->getFamilyCode() !== $req->getFamilyCode()) {
                    $slot->setFamilyCode($req->getFamilyCode());
                    $changed = true;
                }
            }

            // Add new product slots
            foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
                $req  = $productReqs[$reqIdx];
                $slot = new ComposantProductSlot();
                $slot->setComposant($composant);
                $slot->setTypeProduct($req->getTypeProduct());
                $slot->setPosition($req->getPosition());
                if (null !== $req->getFamilyCode()) {
                    $slot->setFamilyCode($req->getFamilyCode());
                }
                $this->em->persist($slot);
                ++$addedProductSlots;
                $changed = true;
            }

            // Delete orphaned product slots
            if ($confirmation->confirmDeletions) {
                foreach ($matchResult['orphanedSlots'] as $slotIdx) {
                    $slot = $productSlotEntities[$slotIdx];
                    $composant->removeProductSlot($slot);
                    $this->em->remove($slot);
                    ++$deletedProductSlots;
                    $changed = true;
                }
            }

            // --- Subcomponent slots ---
            $subSlotEntities    = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]);
            $existingSubTypeIds = array_map(fn (ComposantSubcomponentSlot $s) => $s->getTypeComposant()?->getId() ?? '', $subSlotEntities);
            $reqSubTypeIds      = array_map(fn (SkeletonSubcomponentRequirement $r) => $r->getTypeComposant()?->getId() ?? '', $subReqs);
            $matchResult        = $this->smartMatch($existingSubTypeIds, $reqSubTypeIds);

            // Update matched slots
            foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
                $slot = $subSlotEntities[$slotIdx];
                $req  = $subReqs[$reqIdx];
                if ($slot->getPosition() !== $req->getPosition()) {
                    $slot->setPosition($req->getPosition());
                    $changed = true;
                }
                if ($slot->getAlias() !== $req->getAlias()) {
                    $slot->setAlias($req->getAlias());
                    $changed = true;
                }
                if ($slot->getFamilyCode() !== $req->getFamilyCode()) {
                    $slot->setFamilyCode($req->getFamilyCode());
                    $changed = true;
                }
            }

            // Add new subcomponent slots
            foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
                $req  = $subReqs[$reqIdx];
                $slot = new ComposantSubcomponentSlot();
                $slot->setComposant($composant);
                $slot->setTypeComposant($req->getTypeComposant());
                $slot->setPosition($req->getPosition());
                $slot->setAlias($req->getAlias());
                $slot->setFamilyCode($req->getFamilyCode());
                $this->em->persist($slot);
                ++$addedSubSlots;
                $changed = true;
            }

            // Delete orphaned subcomponent slots
            if ($confirmation->confirmDeletions) {
                foreach ($matchResult['orphanedSlots'] as $slotIdx) {
                    $slot = $subSlotEntities[$slotIdx];
                    $composant->removeSubcomponentSlot($slot);
                    $this->em->remove($slot);
                    ++$deletedSubSlots;
                    $changed = true;
                }
            }

            // --- Custom field values ---
            $existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([
                'composant' => $composant,
            ]);

            $existingByFieldId = [];
            foreach ($existingValues as $cfv) {
                $existingByFieldId[$cfv->getCustomField()->getId()] = $cfv;
            }

            // Add missing custom field values
            foreach ($customFields as $cf) {
                if (!isset($existingByFieldId[$cf->getId()])) {
                    $cfv = new CustomFieldValue();
                    $cfv->setCustomField($cf);
                    $cfv->setComposant($composant);
                    $cfv->setValue('');
                    $this->em->persist($cfv);
                    ++$addedCfValues;
                    $changed = true;
                }
            }

            // Delete orphaned custom field values
            if ($confirmation->confirmDeletions) {
                $fieldIds = array_map(fn (CustomField $cf) => $cf->getId(), $customFields);
                foreach ($existingValues as $cfv) {
                    if (!in_array($cfv->getCustomField()->getId(), $fieldIds, true)) {
                        $this->em->remove($cfv);
                        ++$deletedCfValues;
                        $changed = true;
                    }
                }
            }

            if ($changed) {
                $composant->incrementVersion();
                ++$itemsUpdated;
            }
        }

        $this->em->flush();

        return new SyncExecutionResult(
            itemsUpdated: $itemsUpdated,
            additions: [
                'pieceSlots'        => $addedPieceSlots,
                'productSlots'      => $addedProductSlots,
                'subcomponentSlots' => $addedSubSlots,
                'customFieldValues' => $addedCfValues,
            ],
            deletions: [
                'pieceSlots'        => $deletedPieceSlots,
                'productSlots'      => $deletedProductSlots,
                'subcomponentSlots' => $deletedSubSlots,
                'customFieldValues' => $deletedCfValues,
            ],
        );
    }

    /**
     * Smart-match existing slots to proposed requirements by typeId.
     *
     * Pass 1: exact match by typeId + position index.
     * Pass 2: match remaining by typeId only (handles reordering/insertion).
     *
     * @param string[] $existingTypeIds typeIds of existing slots (index = slot index)
     * @param string[] $proposedTypeIds typeIds of proposed requirements (index = req index)
     *
     * @return array{matched: list<array{int, int}>, orphanedSlots: int[], unmatchedReqs: int[]}
     */
    private function smartMatch(array $existingTypeIds, array $proposedTypeIds): array
    {
        $matchedSlots = [];
        $matchedReqs  = [];
        $matched      = [];

        // Pass 1: exact match where typeId AND position index are identical
        foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
            if (isset($existingTypeIds[$reqIdx]) && $existingTypeIds[$reqIdx] === $reqTypeId && !isset($matchedSlots[$reqIdx])) {
                $matched[]             = [$reqIdx, $reqIdx];
                $matchedSlots[$reqIdx] = true;
                $matchedReqs[$reqIdx]  = true;
            }
        }

        // Pass 2: match remaining by typeId only (preserves selections on reorder)
        $remainingSlotsByType = [];
        foreach ($existingTypeIds as $slotIdx => $typeId) {
            if (!isset($matchedSlots[$slotIdx])) {
                $remainingSlotsByType[$typeId][] = $slotIdx;
            }
        }

        foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
            if (!isset($matchedReqs[$reqIdx]) && !empty($remainingSlotsByType[$reqTypeId])) {
                $slotIdx                = array_shift($remainingSlotsByType[$reqTypeId]);
                $matched[]              = [$slotIdx, $reqIdx];
                $matchedSlots[$slotIdx] = true;
                $matchedReqs[$reqIdx]   = true;
            }
        }

        // Collect unmatched
        $orphanedSlots = [];
        foreach ($existingTypeIds as $slotIdx => $_) {
            if (!isset($matchedSlots[$slotIdx])) {
                $orphanedSlots[] = $slotIdx;
            }
        }

        $unmatchedReqs = [];
        foreach ($proposedTypeIds as $reqIdx => $_) {
            if (!isset($matchedReqs[$reqIdx])) {
                $unmatchedReqs[] = $reqIdx;
            }
        }

        return ['matched' => $matched, 'orphanedSlots' => $orphanedSlots, 'unmatchedReqs' => $unmatchedReqs];
    }

    /**
     * Preview version of smart matching — counts additions and deletions.
     *
     * @param string[] $existingTypeIds
     * @param string[] $proposedTypeIds
     *
     * @return array{added: int, deleted: int}
     */
    private function smartMatchPreview(array $existingTypeIds, array $proposedTypeIds): array
    {
        $result = $this->smartMatch($existingTypeIds, $proposedTypeIds);

        return [
            'added'   => count($result['unmatchedReqs']),
            'deleted' => count($result['orphanedSlots']),
        ];
    }
}
