<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\CustomField;
use App\Entity\ModelType;
use App\Entity\SkeletonPieceRequirement;
use App\Entity\SkeletonProductRequirement;
use App\Entity\SkeletonSubcomponentRequirement;
use App\Enum\ModelCategory;
use Doctrine\ORM\EntityManagerInterface;

class SkeletonStructureService
{
    public function __construct(private EntityManagerInterface $em) {}

    public function updateSkeletonRequirements(ModelType $modelType, array $structure): void
    {
        // Update piece requirements in-place (match by typeId, then update position)
        $this->syncPieceRequirements($modelType, $structure['pieces'] ?? []);

        // Update product requirements in-place
        $this->syncProductRequirements($modelType, $structure['products'] ?? []);

        // Update subcomponent requirements in-place
        $this->syncSubcomponentRequirements($modelType, $structure['subcomponents'] ?? []);

        // Update custom field definitions
        $this->updateCustomFields($modelType, $structure['customFields'] ?? []);
    }

    /**
     * @param array<int, array{typePieceId: string}> $proposedPieces
     */
    private function syncPieceRequirements(ModelType $modelType, array $proposedPieces): void
    {
        $existing = $modelType->getSkeletonPieceRequirements()->toArray();

        // Index existing by typeId for matching
        $existingByTypeId = [];
        foreach ($existing as $req) {
            $existingByTypeId[$req->getTypePiece()->getId()][] = $req;
        }

        $matched  = [];
        $toCreate = [];

        foreach ($proposedPieces as $i => $pieceData) {
            $typeId = $pieceData['typePieceId'];
            if (!empty($existingByTypeId[$typeId])) {
                // Reuse existing requirement, update position
                $req = array_shift($existingByTypeId[$typeId]);
                $req->setPosition($i);
                $matched[spl_object_id($req)] = true;
            } else {
                $toCreate[] = ['data' => $pieceData, 'position' => $i];
            }
        }

        // Remove unmatched existing requirements
        foreach ($existing as $req) {
            if (!isset($matched[spl_object_id($req)])) {
                $modelType->removeSkeletonPieceRequirement($req);
            }
        }

        // Create new requirements
        foreach ($toCreate as $item) {
            $req = new SkeletonPieceRequirement();
            $req->setModelType($modelType);
            $req->setTypePiece($this->em->getReference(ModelType::class, $item['data']['typePieceId']));
            $req->setPosition($item['position']);
            $modelType->addSkeletonPieceRequirement($req);
        }
    }

    /**
     * @param array<int, array{typeProductId: string, familyCode?: ?string}> $proposedProducts
     */
    private function syncProductRequirements(ModelType $modelType, array $proposedProducts): void
    {
        $existing = $modelType->getSkeletonProductRequirements()->toArray();

        $existingByTypeId = [];
        foreach ($existing as $req) {
            $existingByTypeId[$req->getTypeProduct()->getId()][] = $req;
        }

        $matched  = [];
        $toCreate = [];

        foreach ($proposedProducts as $i => $prodData) {
            $typeId = $prodData['typeProductId'];
            if (!empty($existingByTypeId[$typeId])) {
                $req = array_shift($existingByTypeId[$typeId]);
                $req->setFamilyCode($prodData['familyCode'] ?? null);
                $req->setPosition($i);
                $matched[spl_object_id($req)] = true;
            } else {
                $toCreate[] = ['data' => $prodData, 'position' => $i];
            }
        }

        foreach ($existing as $req) {
            if (!isset($matched[spl_object_id($req)])) {
                $modelType->removeSkeletonProductRequirement($req);
            }
        }

        foreach ($toCreate as $item) {
            $req = new SkeletonProductRequirement();
            $req->setModelType($modelType);
            $req->setTypeProduct($this->em->getReference(ModelType::class, $item['data']['typeProductId']));
            $req->setFamilyCode($item['data']['familyCode'] ?? null);
            $req->setPosition($item['position']);
            $modelType->addSkeletonProductRequirement($req);
        }
    }

    /**
     * @param array<int, array{alias?: string, familyCode?: string, typeComposantId?: string}> $proposedSubs
     */
    private function syncSubcomponentRequirements(ModelType $modelType, array $proposedSubs): void
    {
        $existing = $modelType->getSkeletonSubcomponentRequirements()->toArray();

        $existingByTypeId = [];
        foreach ($existing as $req) {
            $key                      = $req->getTypeComposant()?->getId() ?? '';
            $existingByTypeId[$key][] = $req;
        }

        $matched  = [];
        $toCreate = [];

        foreach ($proposedSubs as $i => $subData) {
            $typeId = $subData['typeComposantId'] ?? '';
            if (!empty($existingByTypeId[$typeId])) {
                $req = array_shift($existingByTypeId[$typeId]);
                $req->setAlias($subData['alias'] ?? '');
                $req->setFamilyCode($subData['familyCode'] ?? '');
                if (!empty($subData['typeComposantId'])) {
                    $req->setTypeComposant($this->em->getReference(ModelType::class, $subData['typeComposantId']));
                }
                $req->setPosition($i);
                $matched[spl_object_id($req)] = true;
            } else {
                $toCreate[] = ['data' => $subData, 'position' => $i];
            }
        }

        foreach ($existing as $req) {
            if (!isset($matched[spl_object_id($req)])) {
                $modelType->removeSkeletonSubcomponentRequirement($req);
            }
        }

        foreach ($toCreate as $item) {
            $req = new SkeletonSubcomponentRequirement();
            $req->setModelType($modelType);
            $req->setAlias($item['data']['alias'] ?? '');
            $req->setFamilyCode($item['data']['familyCode'] ?? '');
            if (!empty($item['data']['typeComposantId'])) {
                $req->setTypeComposant($this->em->getReference(ModelType::class, $item['data']['typeComposantId']));
            }
            $req->setPosition($item['position']);
            $modelType->addSkeletonSubcomponentRequirement($req);
        }
    }

    /**
     * Sync CustomField entities for this ModelType.
     * Handles two frontend formats:
     *   - COMPONENT: {key, value: {type, required, options?, defaultValue?}, id?, customFieldId?}
     *   - PIECE/PRODUCT: {name, type, required, options?, orderIndex?, defaultValue?}.
     */
    private function updateCustomFields(ModelType $modelType, array $proposedFields): void
    {
        // Determine which FK to use based on category
        $category = $modelType->getCategory();
        $fkField  = match ($category) {
            ModelCategory::COMPONENT => 'typeComposant',
            ModelCategory::PIECE     => 'typePiece',
            ModelCategory::PRODUCT   => 'typeProduct',
        };

        // Load existing custom fields
        $existingFields = $this->em->getRepository(CustomField::class)->findBy(
            [$fkField => $modelType],
            ['orderIndex' => 'ASC']
        );

        // Index existing by ID, name and orderIndex for matching
        $existingById         = [];
        $existingByName       = [];
        $existingByOrderIndex = [];
        foreach ($existingFields as $cf) {
            $existingById[$cf->getId()]                   = $cf;
            $existingByName[$cf->getName()]               = $cf;
            $existingByOrderIndex[$cf->getOrderIndex()][] = $cf;
        }

        $processedIds = [];

        foreach ($proposedFields as $i => $fieldData) {
            // Normalize both formats to a common shape
            $normalized = $this->normalizeCustomFieldData($fieldData, $i);

            // Try to match an existing field by ID first, then by name as fallback
            $existingField = null;
            $fieldId       = $fieldData['customFieldId'] ?? $fieldData['id'] ?? null;
            if ($fieldId && isset($existingById[$fieldId])) {
                $existingField = $existingById[$fieldId];
            } elseif (!empty($existingByOrderIndex[$normalized['orderIndex']])) {
                foreach ($existingByOrderIndex[$normalized['orderIndex']] as $candidate) {
                    if (!isset($processedIds[$candidate->getId()])) {
                        $existingField = $candidate;

                        break;
                    }
                }
            } elseif (isset($existingByName[$normalized['name']]) && !isset($processedIds[$existingByName[$normalized['name']]->getId()])) {
                $existingField = $existingByName[$normalized['name']];
            }

            if ($existingField) {
                // Propagate rename to the parent ModelType's reference formula and required-fields list
                // so existing `{oldName}` placeholders keep resolving after the field is renamed.
                $oldName = $existingField->getName();
                if ($oldName !== $normalized['name']) {
                    $this->propagateCustomFieldRename($modelType, $oldName, $normalized['name']);
                }

                // Update existing field
                $existingField->setName($normalized['name']);
                $existingField->setType($normalized['type']);
                $existingField->setRequired($normalized['required']);
                $existingField->setOptions($normalized['options']);
                $existingField->setDefaultValue($normalized['defaultValue']);
                $existingField->setOrderIndex($normalized['orderIndex']);
                $existingField->setMachineContextOnly($normalized['machineContextOnly']);
                $processedIds[$existingField->getId()] = true;
            } else {
                // Create new field
                $cf = new CustomField();
                $cf->setName($normalized['name']);
                $cf->setType($normalized['type']);
                $cf->setRequired($normalized['required']);
                $cf->setOptions($normalized['options']);
                $cf->setDefaultValue($normalized['defaultValue']);
                $cf->setOrderIndex($normalized['orderIndex']);
                $cf->setMachineContextOnly($normalized['machineContextOnly']);

                match ($category) {
                    ModelCategory::COMPONENT => $cf->setTypeComposant($modelType),
                    ModelCategory::PIECE     => $cf->setTypePiece($modelType),
                    ModelCategory::PRODUCT   => $cf->setTypeProduct($modelType),
                };

                $this->em->persist($cf);
            }
        }

        // Remove orphaned fields (exist in DB but not in proposed)
        foreach ($existingFields as $cf) {
            if (!isset($processedIds[$cf->getId()])) {
                $this->em->remove($cf);
            }
        }
    }

    private function propagateCustomFieldRename(ModelType $modelType, string $oldName, string $newName): void
    {
        $formula = $modelType->getReferenceFormula();
        if (null !== $formula && '' !== $formula) {
            $newFormula = preg_replace(
                '/\{'.preg_quote($oldName, '/').'\}/',
                '{'.$newName.'}',
                $formula
            );
            if (null !== $newFormula && $newFormula !== $formula) {
                $modelType->setReferenceFormula($newFormula);
            }
        }

        $required = $modelType->getRequiredFieldsForReference();
        if ($required) {
            $changed     = false;
            $newRequired = [];
            foreach ($required as $fieldName) {
                if ($fieldName === $oldName) {
                    $newRequired[] = $newName;
                    $changed       = true;
                } else {
                    $newRequired[] = $fieldName;
                }
            }
            if ($changed) {
                $modelType->setRequiredFieldsForReference($newRequired);
            }
        }
    }

    /**
     * Normalize frontend custom field data to a common shape.
     *
     * @return array{name: string, type: string, required: bool, options: ?array, defaultValue: ?string, orderIndex: int, machineContextOnly: bool}
     */
    private function normalizeCustomFieldData(array $fieldData, int $index): array
    {
        // COMPONENT format: {key: "name", value: {type, required, options?, defaultValue?}}
        if (isset($fieldData['key'], $fieldData['value'])) {
            $value = $fieldData['value'];

            return [
                'name'               => $fieldData['key'],
                'type'               => $value['type'] ?? 'text',
                'required'           => (bool) ($value['required'] ?? false),
                'options'            => $value['options'] ?? null,
                'defaultValue'       => $value['defaultValue'] ?? null,
                'orderIndex'         => $index,
                'machineContextOnly' => (bool) ($value['machineContextOnly'] ?? false),
            ];
        }

        // PIECE/PRODUCT format: {name, type, required, options?, orderIndex?, defaultValue?}
        return [
            'name'               => $fieldData['name'] ?? '',
            'type'               => $fieldData['type'] ?? 'text',
            'required'           => (bool) ($fieldData['required'] ?? false),
            'options'            => $fieldData['options'] ?? null,
            'defaultValue'       => $fieldData['defaultValue'] ?? null,
            'orderIndex'         => $fieldData['orderIndex'] ?? $index,
            'machineContextOnly' => (bool) ($fieldData['machineContextOnly'] ?? false),
        ];
    }
}
