<?php

declare(strict_types=1);

namespace App\Service\Sync;

use App\DTO\SyncConfirmation;
use App\DTO\SyncExecutionResult;
use App\DTO\SyncPreviewResult;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\PieceProductSlot;
use App\Entity\SkeletonProductRequirement;
use App\Enum\ModelCategory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

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

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

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

        $proposedProducts     = $newStructure['products'] ?? [];
        $proposedCustomFields = $newStructure['customFields'] ?? [];

        $addedProductSlots   = 0;
        $deletedProductSlots = 0;
        $addedCfValues       = 0;
        $deletedCfValues     = 0;

        // Build proposed typeId list
        $proposedProductTypeIds = [];
        foreach ($proposedProducts as $pp) {
            $proposedProductTypeIds[] = $pp['typeProductId'];
        }

        // 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(
            ['typePiece' => $modelType],
            ['orderIndex' => 'ASC']
        );
        $existingCfByOrder = [];
        foreach ($existingFields as $field) {
            $existingCfByOrder[$field->getOrderIndex()] = $field;
        }

        // Count custom field additions/deletions (definition-level, affects all pieces)
        $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 ($pieces as $piece) {
            // Product slots — smart matching by typeId
            $productSlots         = $this->em->getRepository(PieceProductSlot::class)->findBy(['piece' => $piece]);
            $existingProductTypes = array_map(fn (PieceProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlots);
            $result               = $this->smartMatchPreview($existingProductTypes, $proposedProductTypeIds);
            $addedProductSlots += $result['added'];
            $deletedProductSlots += $result['deleted'];

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

        $itemCount = count($pieces);

        return new SyncPreviewResult(
            modelTypeId: $modelType->getId(),
            category: 'piece',
            itemCount: $itemCount,
            additions: [
                'productSlots'      => $addedProductSlots,
                'customFieldValues' => $addedCfValues,
            ],
            deletions: [
                'productSlots'      => $deletedProductSlots,
                'customFieldValues' => $deletedCfValues,
            ],
        );
    }

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

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

        $addedProductSlots   = 0;
        $deletedProductSlots = 0;
        $addedCfValues       = 0;
        $deletedCfValues     = 0;
        $itemsUpdated        = 0;

        foreach ($pieces as $piece) {
            $changed = false;

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

            // Update matched slots (position/familyCode may have changed)
            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 PieceProductSlot();
                $slot->setPiece($piece);
                $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];
                    $piece->removeProductSlot($slot);
                    $this->em->remove($slot);
                    ++$deletedProductSlots;
                    $changed = true;
                }
            }

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

            $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->setPiece($piece);
                    $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) {
                $piece->incrementVersion();
                ++$itemsUpdated;
            }
        }

        $this->em->flush();

        return new SyncExecutionResult(
            itemsUpdated: $itemsUpdated,
            additions: [
                'productSlots'      => $addedProductSlots,
                'customFieldValues' => $addedCfValues,
            ],
            deletions: [
                'productSlots'      => $deletedProductSlots,
                '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];
    }

    /**
     * @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']),
        ];
    }
}
