<?php

/*
 * This file is part of the Serenity package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Serenity\CoreBundle\Shared\Twig\Components;

use Doctrine\ORM\EntityManagerInterface;
use Serenity\CoreBundle\Notification\Command\NotificationCommand;
use Serenity\CoreBundle\Notification\Enum\PushLevel;
use Serenity\CoreBundle\Notification\Enum\PushTransport;
use Serenity\CoreBundle\Shared\ObjectMapper\ObjectMapperProvider;
use Serenity\MediaBundle\Trait\ComponentMediaTrait;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\LiveCollectionTrait;
use Symfony\UX\TwigComponent\Attribute\PreMount;

#[AsLiveComponent(template: '@components/SerenityCore/Form.html.twig')]
class Form extends AbstractController
{
    use ComponentMediaTrait;
    use ComponentToolsTrait;
    use ComponentWithFormTrait;
    use DefaultActionTrait;
    use LiveCollectionTrait;

    #[LiveProp(writable: false)]
    public ?string $template = null;

    /**
     * @var class-string<FormTypeInterface>
     */
    #[LiveProp(writable: false)]
    public string $formType;

    /**
     * @var class-string<object>
     */
    #[LiveProp(writable: false)]
    public string $class;

    /**
     * @var class-string<object>
     */
    #[LiveProp(writable: false)]
    public ?string $dto = null;

    #[LiveProp(writable: false)]
    public ?string $messageSuccess = null;

    #[LiveProp(writable: false)]
    public ?string $messageDelete = null;

    #[LiveProp(writable: false)]
    public ?string $redirectNew = null;

    #[LiveProp(writable: false)]
    public ?string $redirectSuccess = null;

    #[LiveProp(writable: false)]
    public ?string $cancelUrl = null;

    #[LiveProp(writable: false)]
    public ?string $cancelAction = null;

    #[LiveProp(writable: false, hydrateWith: 'hydrateObject', dehydrateWith: 'dehydrateObject')]
    public mixed $data = null;

    #[LiveProp(writable: false)]
    public bool $findOne = false;

    #[LiveProp(writable: false)]
    public bool $newInstance = false;

    #[LiveProp(writable: false)]
    public bool $edit = false;

    #[LiveProp(writable: false)]
    public bool $delete = true;

    /** @var array<string, mixed> */
    #[LiveProp(writable: false)]
    public array $initialFormData = [];

    /** @var array<string, mixed> */
    #[LiveProp(writable: false)]
    public array $additionalData = [];

    #[LiveProp(writable: false)]
    public ?string $title = null;

    public function __construct(private readonly EntityManagerInterface $entityManager,
                                private readonly MessageBusInterface $commandSimpleBus,
                                private readonly TranslatorInterface $translator,
                                private readonly RouterInterface $router,
                                private readonly DenormalizerInterface $serializer,
                                private readonly FormFactoryInterface $formFactory,
                                private readonly ObjectMapperProvider $objectMapper,
    ) {
    }

    #[LiveListener('form:refreshForm')]
    public function __invoke(): void
    {
    }

    /**
     * @param array<string> $data
     *
     * @return array<string>
     */
    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setIgnoreUndefined(true);
        $resolver->setDefined('template');
        $resolver->setRequired('formType');
        $resolver->setRequired('class');
        $resolver->setDefined('dto');
        $resolver->setDefined('messageSuccess');
        $resolver->setDefined('messageDelete');
        $resolver->setDefined('redirectNew');
        $resolver->setDefined('redirectSuccess');
        $resolver->setDefined('cancelUrl');
        $resolver->setDefined('cancelAction');
        $resolver->setDefined('initialFormData');
        $resolver->setDefined('additionalData');
        $resolver->setDefined('findOne');
        $resolver->setDefined('newInstance');
        $resolver->setDefined('edit');
        $resolver->setDefined('delete');
        $resolver->setDefined('name');
        $resolver->setDefined('title');
        $resolver->setAllowedTypes('template', ['string', 'null']);
        $resolver->setAllowedTypes('messageSuccess', ['string', 'null']);
        $resolver->setAllowedTypes('messageDelete', ['string', 'null']);
        $resolver->setAllowedTypes('redirectNew', ['string', 'null']);
        $resolver->setAllowedTypes('redirectSuccess', ['string', 'null']);
        $resolver->setAllowedTypes('class', 'string');
        $resolver->setAllowedTypes('dto', ['string', 'null']);
        $resolver->setAllowedTypes('cancelUrl', ['string', 'null']);
        $resolver->setAllowedTypes('title', ['string', 'null']);
        $resolver->setAllowedTypes('cancelAction', ['string', 'null']);
        $resolver->setAllowedTypes('initialFormData', ['array', 'null']);
        $resolver->setAllowedTypes('additionalData', ['array', 'null']);
        $resolver->setAllowedTypes('findOne', 'bool');
        $resolver->setAllowedTypes('newInstance', 'bool');
        $resolver->setAllowedTypes('edit', 'bool');
        $resolver->setAllowedTypes('delete', 'bool');

        // Validation custom pour la route :
        $resolver->setNormalizer('cancelUrl', function (Options $options, $value) {
            if (null === $value) {
                return null;
            }

            try {
                $this->router->generate($value);
            } catch (RouteNotFoundException $e) {
                throw new \InvalidArgumentException(\sprintf('The route "%s" does not exist.', $value));
            }

            return $value;
        });

        $resolver->setNormalizer('redirectSuccess', function (Options $options, $value) {
            if (null === $value) {
                return null;
            }

            try {
                $this->router->generate($value);
            } catch (RouteNotFoundException $e) {
                throw new \InvalidArgumentException(\sprintf('The route "%s" does not exist.', $value));
            }

            return $value;
        });

        // Validation custom pour la route :
        $resolver->setNormalizer('redirectNew', function (Options $options, $value) {
            if (null === $value) {
                return null;
            }

            try {
                $this->router->generate($value, ['id' => Uuid::v4()]);
            } catch (RouteNotFoundException $e) {
                throw new \InvalidArgumentException(\sprintf('The route "%s" does not exist.', $value));
            }

            return $value;
        });

        $resolved = $resolver->resolve($data);
        if (($resolved['findOne'] ?? false) && !empty($resolved['initialFormData'])) {
            throw new \InvalidArgumentException('You cannot define both "findOne" and "initialFormData". Only one must be used.');
        }

        $resolver->setNormalizer('delete', function (Options $options, $value) {
            if ($value && null === $options['cancelUrl']) {
                throw new \InvalidArgumentException('The "cancelUrl" option is required when "delete" is true.');
            }

            return $value;
        });

        return $resolved + $data;
    }

    #[LiveAction]
    public function remove(#[LiveArg] Uuid $id): RedirectResponse
    {
        if (null === $this->cancelUrl) {
            throw new \LogicException('Missing cancel URL for delete action.');
        }
        $this->submitForm(false);
        /** @var string $cancelUrl */
        $cancelUrl = $this->cancelUrl;
        $url = $this->router->generate($cancelUrl);
        $entity = $this->entityManager->getRepository($this->class)->find($id);
        if (!$entity) {
            return new RedirectResponse($url, 302);
        }
        $this->entityManager->remove($entity);
        $this->entityManager->flush();

        if ($this->messageDelete) {
            $notification = new NotificationCommand(
                message: $this->messageDelete,
                transport: PushTransport::FLASH
            );
            $this->commandSimpleBus->dispatch($notification);
        }

        return new RedirectResponse($url, 302);
    }

    protected function instantiateForm(): FormInterface
    {
        $repository = $this->entityManager->getRepository($this->class);
        $data = null;

        // 1. Recherche via initialFormData
        if (!empty($this->initialFormData)) {
            $data = $repository->findOneBy($this->initialFormData);
        }

        // 2. Recherche générique (findOne sans critères)
        if ($this->findOne && !$data) {
            $data = $repository->findOneBy([]);
        }

        // 3. Tentative de dénormalisation si aucune donnée trouvée
        if (!$data && !empty($this->initialFormData)) {
            $data = $this->tryDenormalize($this->initialFormData);
        }

        // 4. Création d'une nouvelle instance si rien trouvé
        if (!$data && $this->newInstance && !$this->dto) {
            $data = new $this->class();
        }

        // 5. Préparation de la donnée à injecter dans le formulaire
        $this->data = $data;

        if ($this->dto) {
            $this->data = $data
                ? $this->objectMapper->map($data, $this->dto)
                : new $this->dto();
        }

        return $this->formFactory->create($this->formType, $this->data, [
            'edit' => $this->edit,
        ]);
    }

    /**
     * Essaie de dénormaliser des données initiales.
     */
    /**
     * @param array<string, mixed> $data
     */
    private function tryDenormalize(array $data, ?object $object = null): ?object
    {
        $class = $this->dto ?? $this->class;
        try {
            $context = [
                'allow_extra_attributes' => false,
            ];
            if ($object) {
                $context['object_to_populate'] = $object;
            }

            return $this->serializer->denormalize(
                $data,
                $class,
                null,
                $context
            );
        } catch (\Throwable) {
            return null;
        }
    }

    #[LiveAction]
    public function save(Request $request): ?Response
    {
        if ($this->uploadedFiles) {
            $this->formValues = array_replace_recursive($this->formValues, $this->deserializeUploadedFiles($this->uploadedFiles));
        }

            try {
                $this->submitForm();
            if ($this->form instanceof FormInterface) {
                $data = $this->form->getData();
                if ($this->additionalData) {
                    $data = $this->tryDenormalize($this->additionalData, $data);
                }

                if ($this->dto) {
                    $id = $data->id ?? null;
                    $target = $id
                        ? $this->entityManager->getRepository($this->class)->find($id)
                        : $this->class;
                    $data = $this->objectMapper->map($data, $target);
                }

                $this->entityManager->persist($data);
                $this->entityManager->flush();

                if ($this->redirectNew && false === $this->edit) {
                    $url = $this->router->generate($this->redirectNew, ['id' => $data->getId()]);
                    if (null !== $this->messageSuccess) {
                        $notification = new NotificationCommand(
                            message: $this->messageSuccess,
                            transport: PushTransport::FLASH
                        );
                        $this->commandSimpleBus->dispatch($notification);
                    }

                    $this->resetForm();

                    return new RedirectResponse($url, 302);
                }

                if (null !== $this->redirectSuccess) {
                    $url = $this->router->generate($this->redirectSuccess);
                    if (null !== $this->messageSuccess) {
                        $notification = new NotificationCommand(
                            message: $this->messageSuccess,
                            transport: PushTransport::FLASH
                        );
                        $this->commandSimpleBus->dispatch($notification);
                    }
                    $this->resetForm();

                    return new RedirectResponse($url, 302);
                }

                if (null !== $this->messageSuccess) {
                    $notification = new NotificationCommand(
                        message: $this->messageSuccess
                    );
                    $this->commandSimpleBus->dispatch($notification);
                }

                $this->uploadedFiles = [];
                $this->resetForm();
            }
        } catch (UnprocessableEntityHttpException $e) {
            $this->entityManager->detach($this->form->getData());
            if ($this->form->getErrors(true)->count() > 0) {
                $notification = new NotificationCommand(
                    message: $this->translator->trans('form.error', [], 'serenity_core_messages.admin'),
                    level: PushLevel::ERROR
                );
                $this->commandSimpleBus->dispatch($notification);
            }
        }

        return null;
    }

    public function dehydrateObject(mixed $data): ?Uuid
    {
        // Si c’est un objet Doctrine avec une méthode getId()
        if (\is_object($data) && method_exists($data, 'getId')) {
            $id = $data->getId();

            return $id instanceof Uuid ? $id : null;
        }

        // Si c’est un objet avec une propriété publique id
        if (\is_object($data) && property_exists($data, 'id')) {
            $id = $data->id;

            return $id instanceof Uuid ? $id : null;
        }

        // Rien trouvé
        return null;
    }

    public function hydrateObject(mixed $data): mixed
    {
        if (null === $data) {
            return null;
        }

        $repository = $this->entityManager->getRepository($this->class);

        return $repository->find($data);
    }

    #[LiveListener('form:updateFormValues')]
    public function updateFormValues(#[LiveArg] string $model, #[LiveArg] string $value): void
    {
        $this->setFormValueByPath($model, $value);
        $this->submitForm();
    }

    /**
     * @param array<string, mixed> $formValues
     */
    #[LiveListener('form:addFormValues')]
    public function addFormValues(#[LiveArg] array $formValues): void
    {
        $this->formValues = $this->smartMerge($this->formValues, $formValues);
        $this->submitForm();
    }

    /**
     * @param array<string, mixed> $original
     * @param array<string, mixed> $new
     *
     * @return array<string, mixed>
     */
    private function smartMerge(array $original, array $new): array
    {
        foreach ($new as $key => $value) {
            // Si on reçoit une liste → remplacement total
            if (\is_array($value) && array_is_list($value)) {
                $original[$key] = $value;
                continue;
            }

            // Si tableau associatif des deux côtés → prune + merge
            if (
                isset($original[$key])
                && \is_array($value)
                && \is_array($original[$key])
            ) {
                // 🔥 prune UNIQUEMENT sur cette branche
                foreach ($original[$key] as $subKey => $_) {
                    if (!\array_key_exists($subKey, $value)) {
                        unset($original[$key][$subKey]);
                    }
                }

                $original[$key] = $this->smartMerge($original[$key], $value);
                continue;
            }

            // Valeur simple → overwrite
            $original[$key] = $value;
        }

        return $original;
    }

    private function setFormValueByPath(string $path, mixed $value): void
    {
        $keys = explode('.', $path);
        $target = &$this->formValues;

        foreach ($keys as $key) {
            if (!isset($target[$key]) || !\is_array($target[$key])) {
                $target[$key] = [];
            }
            $target = &$target[$key];
        }

        // Enfin, on assigne la valeur au bon endroit
        $target = $value;
    }
}
