<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Components\Offer;

use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Section\SectionComponentInterface;
use Ferienpass\AdminBundle\Service\FileUploader;
use Ferienpass\CoreBundle\Entity\DbafsMedia;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Validator\ValidatorInterface;
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\DefaultActionTrait;

#[AsLiveComponent(template: '@FerienpassAdmin/components/Offer/MediaManager.html.twig', route: 'live_component_admin')]
class MediaManager extends AbstractController implements SectionComponentInterface
{
    use ComponentToolsTrait;
    use DefaultActionTrait;

    #[LiveProp]
    public OfferInterface $offer;

    #[LiveProp(writable: true)]
    public string $mediaAction = '';

    #[LiveProp(writable: true)]
    public ?string $activeMediaUuid = null;

    #[LiveProp(writable: true)]
    public array $mediaMetadata = [];

    public bool $mediaUnassigned = false;
    public bool $mediaDeleted = false;
    public bool $mediaUnassignedAndNotDeleted = false;
    public ?string $error = null;
    public bool $success = false;
    public bool $mediaAssigned = false;

    public string $message = '';

    #[LiveProp]
    public array $sections = [];

    #[LiveProp]
    public ?string $section = null;

    #[LiveProp]
    public bool $isDirty = false;

    #[LiveProp]
    public bool $isReadonly = false;

    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly FileUploader $fileUploader,
        private readonly VirtualFilesystemInterface $mediaStorage,
        private readonly ValidatorInterface $validator,
    ) {
    }

    public function mount(OfferInterface $offer): void
    {
        $this->offer = $offer;

        if (null === $this->activeMediaUuid && !$this->offer->getMedia()->isEmpty()) {
            $this->activeMediaUuid = $this->offer->getMedia()->first()->getMedia()->getUuid()->toRfc4122();
        }

        $this->syncMetadata();
    }

    public static function getSectionName(): string
    {
        return 'media';
    }

    public static function getSectionScope(): string
    {
        return 'offer';
    }

    #[LiveListener('fileAssigned')]
    public function onFileAssigned(): void
    {
        $this->denyAccessIfReadonly();

        $this->mediaAssigned = true;
        $this->entityManager->refresh($this->offer);
        $this->syncMetadata();
    }

    #[LiveAction]
    public function reorderMedia(#[LiveArg] array $uuids): void
    {
        $this->denyAccessIfReadonly();

        $sorting = 0;
        foreach ($uuids as $uuid) {
            foreach ($this->offer->getMedia() as $om) {
                if ($om->getMedia()->getUuid()->toRfc4122() === $uuid) {
                    $om->setSorting($sorting++);
                    break;
                }
            }
        }

        $this->entityManager->flush();
        $this->entityManager->refresh($this->offer);
        $this->syncMetadata();
    }

    #[LiveAction]
    public function setMediaAction(#[LiveArg] string $mode): void
    {
        $this->denyAccessIfReadonly();

        $this->mediaAction = $mode;
    }

    #[LiveAction]
    public function setActiveMedia(#[LiveArg] string $uuid): void
    {
        $this->activeMediaUuid = $uuid;
    }

    #[LiveAction]
    public function unassignMedia(#[LiveArg] string $uuid): void
    {
        $this->denyAccessIfReadonly();

        foreach ($this->offer->getMedia() as $om) {
            if ($om->getMedia()->getUuid()->toRfc4122() === $uuid) {
                $this->offer->removeMedia($om);
                $om->getMedia()->getOffers()->removeElement($om);
                $this->entityManager->remove($om);
                break;
            }
        }

        if ($this->activeMediaUuid === $uuid) {
            $this->activeMediaUuid = null;
        }

        $this->entityManager->flush();
        $this->entityManager->refresh($this->offer);

        $this->mediaUnassigned = true;
    }

    #[LiveAction]
    public function deleteMedia(#[LiveArg] string $uuid): void
    {
        $this->denyAccessIfReadonly();

        $media = $this->entityManager->find(DbafsMedia::class, $uuid);
        if (!$media) {
            return;
        }

        foreach ($this->offer->getMedia() as $om) {
            if ($om->getMedia()->getUuid()->toRfc4122() === $uuid) {
                $this->offer->removeMedia($om);
                $media->getOffers()->removeElement($om);
                $this->entityManager->remove($om);
                break;
            }
        }

        $this->entityManager->flush();

        if ($media->getOffers()->isEmpty()) {
            $mediaUuid = $media->getUuid();
            if ($mediaUuid && $this->mediaStorage->has($mediaUuid)) {
                $this->mediaStorage->delete($mediaUuid);
            }

            $this->entityManager->remove($media);
            $this->mediaDeleted = true;
        } else {
            $this->mediaUnassignedAndNotDeleted = true;
        }

        if ($this->activeMediaUuid === $uuid) {
            $this->activeMediaUuid = null;
        }

        $this->entityManager->flush();
        $this->entityManager->refresh($this->offer);
    }

    #[LiveAction]
    public function resetMediaAction(): void
    {
        $this->denyAccessIfReadonly();

        $this->mediaAction = '';
    }

    #[LiveListener('imageSelected')]
    #[LiveListener('imageGenerated')]
    public function onImageSelected(#[LiveArg] string $uuid): void
    {
        $this->denyAccessIfReadonly();

        $this->mediaAction = '';
        $this->activeMediaUuid = $uuid;

        $media = $this->entityManager->find(DbafsMedia::class, $uuid);
        if ($media) {
            if (!$this->isGranted('view', $media)) {
                $this->error = 'Du hast keine Berechtigung, dieses Bild zu verwenden.';

                return;
            }

            // Check if already assigned
            $found = false;
            foreach ($this->offer->getMedia() as $om) {
                if ($om->getMedia()->getUuid()->toRfc4122() === $uuid) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                $this->offer->addMedia($media);
            }

            // Re-sort media collection: selected one first (sorting 0)
            $mediaCollection = $this->offer->getMedia();
            $found = false;
            foreach ($mediaCollection as $om) {
                if ($om->getMedia()->getUuid()->toRfc4122() === $uuid) {
                    $om->setSorting(0);
                    $found = true;
                    break;
                }
            }

            if ($found) {
                $i = 1;
                foreach ($mediaCollection as $om) {
                    if ($om->getMedia()->getUuid()->toRfc4122() !== $uuid) {
                        $om->setSorting($i++);
                    }
                }
            }

            $this->entityManager->flush();
            $this->entityManager->refresh($this->offer);
            $this->syncMetadata();
        }
    }

    #[LiveAction]
    public function upload(Request $request): void
    {
        $this->denyAccessIfReadonly();

        $uploadedFile = $request->files->get('media');
        if (!$uploadedFile instanceof UploadedFile) {
            return;
        }

        $violations = $this->validator->validate($uploadedFile, [
            new Image([
                'maxSize' => '6M',
                'mimeTypes' => [
                    'image/jpeg',
                    'image/png',
                    'image/webp',
                ],
            ]),
        ]);

        if ($violations->count() > 0) {
            foreach ($violations as $violation) {
                $this->error = $violation->getMessage();
            }

            return;
        }

        $uuid = $this->fileUploader->upload($this->mediaStorage, $uploadedFile);

        if ($uuid) {
            $media = $this->entityManager->getReference(DbafsMedia::class, $uuid);
            $media->setUploadedBy($this->getUser());

            $this->offer->addMedia($media);
            $this->entityManager->flush();
            $this->entityManager->refresh($this->offer);

            $this->syncMetadata();

            $this->mediaAction = '';
            $this->activeMediaUuid = $uuid->toRfc4122();
        }
    }

    public function getCropperOptions(string $uuid): array
    {
        $metadata = $this->mediaMetadata[$uuid] ?? null;
        $importantPart = $metadata['importantPart'] ?? null;

        return [
            'data' => \is_array($importantPart) ? $importantPart : (\is_string($importantPart) ? json_decode($importantPart, true) : null),
        ];
    }

    #[LiveAction]
    public function save(#[LiveArg] ?string $nextSection = null, #[LiveArg] ?string $redirect = null): ?Response
    {
        $this->denyAccessIfReadonly();

        foreach ($this->offer->getMedia() as $om) {
            $uuid = $om->getMedia()->getUuid()->toRfc4122();
            if (isset($this->mediaMetadata[$uuid])) {
                $media = $this->entityManager->find(DbafsMedia::class, $uuid);
                if (!$media) {
                    continue;
                }
                $data = $this->mediaMetadata[$uuid];
                $media->setAlt($data['alt'] ?? null);
                $media->setCreator($data['creator'] ?? null);
                if (isset($data['importantPart'])) {
                    $this->setImportantPartFromJson($media, $data['importantPart']);
                }
            }
        }

        $this->entityManager->flush();

        // Re-hydrate offer to ensure collection order is reflected if changed by submitForm/re-sorting
        $this->entityManager->refresh($this->offer);
        $this->syncMetadata(true);

        $this->success = true;

        if ($nextSection || $redirect) {
            $this->addFlash('success', ['type' => 'success', 'title' => 'Daten gespeichert', 'message' => 'Die Änderungen wurden erfolgreich übernommen.']);
        } else {
            $this->message = 'Die Änderungen wurden erfolgreich übernommen.';
        }

        $this->dispatchBrowserEvent('live:saved', ['success' => true]);

        if ($nextSection) {
            return $this->redirectToRoute('admin_offers_edit', ['uuid' => $this->offer->getUuid()->toBase58(), 'section' => $nextSection]);
        }
        if ($redirect) {
            return $this->redirectToRoute($redirect, ['uuid' => $this->offer->getUuid()->toBase58()]);
        }

        return null;
    }

    public function getPublicUrl(DbafsMedia $media): string
    {
        return (string) $this->mediaStorage->generatePublicUri($media->getUuid());
    }

    /**
     * Two reasons the media section can be read-only:
     *  - the offer is a variant (its media is propagated from the base
     *    by {@see \Ferienpass\CoreBundle\MessageHandler\SyncOfferVariants});
     *  - the current user lacks the 'edit' voter (e.g. host editing a
     *    finalised offer).
     *
     * The template hides every mutation control in either case; this
     * is the defensive server-side backstop against a tampered request
     * or stale LiveProp. We re-check the voter rather than trusting
     * the {@see $isReadonly} LiveProp, which the client could flip.
     */
    private function denyAccessIfReadonly(): void
    {
        if ($this->offer->isVariant()) {
            throw new BadRequestHttpException('Cannot modify media on a variant offer.');
        }

        $this->denyAccessUnlessGranted('edit', $this->offer);
    }

    private function syncMetadata(bool $force = false): void
    {
        foreach ($this->offer->getMedia() as $om) {
            $media = $om->getMedia();
            $uuid = $media->getUuid()->toRfc4122();
            if ($force || !isset($this->mediaMetadata[$uuid])) {
                $this->mediaMetadata[$uuid] = [
                    'alt' => $media->getAlt(),
                    'creator' => $media->getCreator(),
                    'importantPart' => $this->getImportantPartJson($media),
                ];
            }
        }
    }

    private function getImportantPartJson(DbafsMedia $media): ?array
    {
        $importantPart = $media->getImportantPart();
        if ([] === array_filter($importantPart)) {
            return null;
        }

        return [
            'x' => (float) $importantPart['x'],
            'y' => (float) $importantPart['y'],
            'width' => (float) $importantPart['width'],
            'height' => (float) $importantPart['height'],
        ];
    }

    private function setImportantPartFromJson(DbafsMedia $media, $json): void
    {
        $data = \is_array($json) ? $json : json_decode($json ?? '', true);
        if (!$data || !isset($data['width'], $data['height'], $data['x'], $data['y'])) {
            return;
        }

        $x = max(0.0, min(1.0, (float) $data['x']));
        $y = max(0.0, min(1.0, (float) $data['y']));
        $width = max(0.0, min(1.0, (float) $data['width']));
        $height = max(0.0, min(1.0, (float) $data['height']));

        $width = min($width, max(0.0, 1.0 - $x));
        $height = min($height, max(0.0, 1.0 - $y));

        $x = round($x, 4, \RoundingMode::TowardsZero);
        $y = round($y, 4, \RoundingMode::TowardsZero);
        $width = round($width, 4, \RoundingMode::TowardsZero);
        $height = round($height, 4, \RoundingMode::TowardsZero);

        $width = min($width, max(0.0, 1.0 - $x));
        $height = min($height, max(0.0, 1.0 - $y));

        $media->setImportantPart([
            'width' => $width,
            'height' => $height,
            'x' => $x,
            'y' => $y,
        ]);
    }
}
