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

use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Serenity\CoreBundle\Notification\Command\NotificationCommand;
use Serenity\CoreBundle\Notification\Enum\PushLevel;
use Serenity\MediaBundle\Doctrine\Repository\AttachmentRepository;
use Serenity\MediaBundle\Enum\PathStorage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Service\Attribute\Required;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
use Symfony\UX\TwigComponent\Attribute\PreMount;
use Vich\UploaderBundle\Entity\File;

#[AsLiveComponent(template: '@components/SerenityMedia/FileForm.html.twig')]
class FileForm extends AbstractController
{
    use ComponentToolsTrait;
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    private string $relativeDefaultLocalDirectory;
    private readonly CacheManager $imagineCacheManager;
    private readonly ValidatorInterface $validator;
    private readonly MessageBusInterface $commandBus;
    private readonly AttachmentRepository $attachmentRepository;

    #[Required]
    public function setLocalDirectory(
        #[Autowire(param: 'relative_default_local_directory')]
        string $relativeDefaultLocalDirectory,
    ): void {
        $this->relativeDefaultLocalDirectory = $relativeDefaultLocalDirectory;
    }

    #[Required]
    public function setCacheManager(CacheManager $imagineCacheManager): void
    {
        $this->imagineCacheManager = $imagineCacheManager;
    }

    #[Required]
    public function setValidator(ValidatorInterface $validator): void
    {
        $this->validator = $validator;
    }

    #[Required]
    public function setMessageBus(#[Target] MessageBusInterface $commandBus): void
    {
        $this->commandBus = $commandBus;
    }

    #[Required]
    public function setAttachmentRepository(AttachmentRepository $attachmentRepository): void
    {
        $this->attachmentRepository = $attachmentRepository;
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, mixed>
     */
    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setIgnoreUndefined(true);
        $resolver->setDefined('deleteButton');
        $resolver->setAllowedTypes('deleteButton', 'bool');
        $resolver->setDefined('formId');
        $resolver->setRequired('formId');
        $resolver->setAllowedTypes('formId', ['string']);

        $resolver->setDefined('fileId');
        $resolver->setAllowedTypes('fileId', [Uuid::class, 'string', 'null']);

        $resolver->setDefined('required');
        $resolver->setRequired('required');
        $resolver->setAllowedTypes('required', ['bool']);

        $resolver->setDefined('name');
        $resolver->setRequired('name');
        $resolver->setAllowedTypes('name', ['string']);

        $resolver->setDefined('type');
        $resolver->setRequired('type');
        $resolver->setAllowedTypes('type', ['string']);
        $resolver->setAllowedValues('type', ['image', 'file', 'video']);

        $resolver->setDefined('classForValidateUpload');
        $resolver->setRequired('classForValidateUpload');
        $resolver->setAllowedTypes('classForValidateUpload', ['string']);
        $resolver->setAllowedValues('classForValidateUpload', function ($value) {
            if (!\is_string($value) || !class_exists($value)) {
                return false;
            }

            $reflection = new \ReflectionClass($value);

            return $reflection->hasProperty('uploadedFile');
        });

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

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

    #[LiveProp(writable: true, hydrateWith: 'hydrateFile', dehydrateWith: 'dehydrateFile', updateFromParent: true)]
    public ?File $file = null;

    #[LiveProp(writable: true)]
    public ?UploadedFile $uploadedFile = null;

    #[LiveProp(writable: false)]
    public string $name;

    #[LiveProp(writable: false)]
    public string $required;

    #[LiveProp(writable: false, hydrateWith: 'hydrateBase64', dehydrateWith: 'DehydrateBase64')]
    public string $classForValidateUpload;

    #[LiveProp(writable: false)]
    public string $type;

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

    #[LiveProp(writable: false)]
    public string $formId;

    public function mount(string $type, string $classForValidateUpload): void
    {
        $this->type = $type;
        $this->classForValidateUpload = $classForValidateUpload;
    }

    public function dehydrateBase64(?string $value = null): ?string
    {
        return null === $value ? null : base64_encode($value);
    }

    public function hydrateBase64(?string $value = null): ?string
    {
        if (null === $value) {
            return null;
        }

        $decoded = base64_decode($value, true);

        return false === $decoded ? null : $decoded;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function dehydrateFile(?File $file = null): ?array
    {
        if (null === $file) {
            return null;
        }

        return [
            'name' => $file->getName(),
            'originalName' => $file->getOriginalName(),
            'mimeType' => $file->getMimeType(),
            'size' => $file->getSize(),
            'dimensions' => $file->getDimensions(),
        ];
    }

    /**
     * @param array{name: string, originalName: string, mimeType: string, size: int, dimensions: mixed} $data
     */
    public function hydrateFile(?array $data): ?File
    {
        if (null === $data) {
            return null;
        }

        $file = new File();
        $file->setName($data['name']);
        $file->setOriginalName($data['originalName']);
        $file->setMimeType($data['mimeType']);
        $file->setSize($data['size']);
        $file->setDimensions($data['dimensions']);

        return $file;
    }

    #[LiveAction]
    public function upload(Request $request): void
    {
        $upload = $request->files->all();
        if (!$upload) {
            return;
        }
        $uploadedFiles = $this->findUploadedFiles($upload);
        $addUploadedFiles = [];
        $removeUploadedFiles = [];

        foreach ($uploadedFiles as $key => $uploadedFile) {
            $file = new $this->classForValidateUpload();

            // ✅ Typage dynamique pour PHPStan
            if (!method_exists($file, 'setUploadedFile')) {
                throw new \LogicException(\sprintf('Class "%s" must implement a setUploadedFile method.', $this->classForValidateUpload));
            }

            $file->setUploadedFile($uploadedFile);
            $constraint = $this->validator->validate($file);

            if ($constraint->count()) {
                $notification = new NotificationCommand(
                    message: $constraint->get(0)->getMessage(),
                    level: PushLevel::ERROR
                );
                $this->commandBus->dispatch($notification);

                return;
            }

            $addUploadedFiles[$key] = $this->serializeUploadedFile($uploadedFile);
        }

        if ('image' === $this->type) {
            $this->imagePreview($addUploadedFiles);
        }

        $this->emitUp('uploadedFilesChanged', [
            'addUploadedFiles' => $addUploadedFiles,
            'removeUploadedFiles' => $removeUploadedFiles,
            'refresh' => true,
        ]);
    }

    #[LiveAction]
    public function remove(#[LiveArg] string $fieldName, #[LiveArg] ?string $id = null): void
    {
        if ($id) {
            if ($this->required) {
                $notification = new NotificationCommand(
                    message: 'L\'image est obligatoire, elle ne peut-être supprimée',
                    level: PushLevel::ERROR
                );
                $this->commandBus->dispatch($notification);

                return;
            }
            $this->file = null;
            if ($file = $this->attachmentRepository->find($id)) {
                $this->attachmentRepository->remove($file);
            }
        } elseif ('image' === $this->type) {
            $this->dispatchBrowserEvent('preview:remove', ['id' => $fieldName]);
        }

        $this->emitUp('uploadedFilesChanged', ['removeUploadedFiles' => [$fieldName]]);
    }

    /**
     * @param array<string, mixed> $array
     *
     * @return array<string, UploadedFile>
     */
    private function findUploadedFiles(array $array, string $needle = 'uploadedFile', string $prefix = ''): array
    {
        $result = [];

        foreach ($array as $key => $value) {
            $path = '' === $prefix ? $key : $prefix.'_'.$key;

            if (false !== mb_stripos($key, $needle)) {
                $result[$path] = $value;
            }

            if (\is_array($value)) {
                $result += $this->findUploadedFiles($value, $needle, $path);
            }
        }

        return $result;
    }

    /**
     * @param array<string, array{path: string, originalName: string, mimeType: string}> $serializeUploadedFile
     */
    private function imagePreview(array $serializeUploadedFile): void
    {
        foreach ($serializeUploadedFile as $key => $value) {
            if ('image' === $this->type) {
                $this->dispatchBrowserEvent('preview:image', [
                    'id' => $key,
                    'path' => $value['path'],
                    'url' => $this->imagineCacheManager->generateUrl($value['path'], 'thumbnail_temp'),
                ]);
            }
        }
    }

    /**
     * @return array{path: string, originalName: string, mimeType: string}
     */
    private function serializeUploadedFile(UploadedFile $uploadedFile): array
    {
        $random = bin2hex(random_bytes(4));
        $filename = $random.'-'.$uploadedFile->getClientOriginalName();

        $uploadedFile->move($this->relativeDefaultLocalDirectory.PathStorage::TEMP->value, $filename);

        return [
            'path' => $this->relativeDefaultLocalDirectory.PathStorage::pathTemp($filename),
            'originalName' => $uploadedFile->getClientOriginalName(),
            'mimeType' => $uploadedFile->getClientMimeType(),
        ];
    }
}
