<?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\AdminBundle\Twig\Components;

use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Serenity\AdminBundle\Form\Type\SearchType;
use Serenity\CoreBundle\Search\SearchInterface;
use Serenity\CoreBundle\Shared\Doctrine\Repository\CrudRepositoryInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\Attribute\PreMount;

#[AsLiveComponent(template: '@components/SerenityAdmin/Crud.html.twig')]
class Crud
{
    use ComponentToolsTrait;
    use ComponentWithFormTrait;
    use DefaultActionTrait;

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

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

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

    #[LiveProp(writable: true, hydrateWith: 'hydrateSearch', dehydrateWith: 'dehydrateSearch', url: true)]
    public ?SearchInterface $search = null;

    #[LiveProp(writable: true, url: true)]
    public int $page = 1;

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

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

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

    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly RouterInterface $router,
        private readonly FormFactoryInterface $formFactory,
    ) {
    }

    /**
     * @param array<string> $data
     *
     * @return array<string>
     */
    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setIgnoreUndefined(true);
        $resolver->setDefined('class');
        $resolver->setDefined('searchType');
        $resolver->setDefined('searchClass');
        $resolver->setDefined('newPath');
        $resolver->setDefined('newTitle');
        $resolver->setDefined('disabledNewIfEmpty');
        $resolver->setAllowedTypes('class', 'string');
        $resolver->setAllowedTypes('searchType', ['string', 'null']);
        $resolver->setAllowedTypes('searchClass', ['string', 'null']);
        $resolver->setAllowedTypes('newPath', ['string', 'null']);
        $resolver->setAllowedTypes('newTitle', ['string', 'null']);
        $resolver->setAllowedTypes('disabledNewIfEmpty', 'bool');
        $resolver->setDefined('filterBy');
        $resolver->setAllowedTypes('filterBy', 'array');

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

            // Cas 1: URL déjà générée (ex: "/admin/foo/new" ou "https://...")
            if (\is_string($value) && (
                str_starts_with($value, '/')
                || str_starts_with($value, 'http://')
                || str_starts_with($value, 'https://')
            )) {
                return $value;
            }

            // Cas 2: nom de route => on valide qu'elle existe
            try {
                $this->router->generate($value);
            } catch (RouteNotFoundException) {
                throw new \InvalidArgumentException(\sprintf('The route "%s" does not exist and "%s" does not look like a URL (expected "/...", "http://...", "https://...").', $value, $value));
            }

            return $value;
        });

        return $resolver->resolve($data) + $data;
    }

    protected function instantiateForm(): FormInterface
    {
        if (null === $this->searchType) {
            return $this->formFactory->create(FormType::class);
        }

        return $this->formFactory->create($this->searchType, $this->search);
    }

    /**
     * @return PaginationInterface<mixed>
     */
    #[ExposeInTemplate]
    public function getItems(): PaginationInterface
    {
        /* @var CrudRepositoryInterface $repository */
        $repository = $this->entityManager->getRepository($this->class);
        \assert($repository instanceof CrudRepositoryInterface);
        $queryBuilder = $repository->searchQuery($this->search, $this->filterBy);

        return $repository->findAllPagination($this->page, queryBuilder: $queryBuilder);
    }

    #[LiveAction]
    public function filter(): void
    {
        $this->submitForm();
        if ($this->form instanceof FormInterface) {
            $this->search = $this->form->getData();
        }
    }

    /**
     * @return array<string, mixed>
     */
    public function dehydrateSearch(?SearchInterface $search = null): array
    {
        if (null === $search) {
            return [];
        }

        return $search->toArray();
    }

    public function hydrateSearch(mixed $data): ?SearchInterface
    {
        if (!\is_array($data)) {
            return null;
        }

        /** @var SearchInterface $reflection */
        $reflection = new ($this->searchClass);

        return $reflection::fromArray($data);
    }

    #[ExposeInTemplate]
    public function getNewHref(): ?string
    {
        if (null === $this->newPath) {
            return null;
        }

        // Si c'est déjà une URL
        if (
            str_starts_with($this->newPath, '/')
            || str_starts_with($this->newPath, 'http://')
            || str_starts_with($this->newPath, 'https://')
        ) {
            return $this->newPath;
        }

        // Sinon c'est un nom de route
        return $this->router->generate($this->newPath);
    }
}
