<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Device;
use App\Entity\Image;
use App\Entity\RenderedAsset;
use App\Enum\DeviceModel;
use App\Enum\Orientation;
use App\Enum\RenderStatus;
use App\Enum\TokenType;
use App\Message\RenderImageMessage;
use App\Service\ShareService;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/api/images')]
#[IsGranted('ROLE_USER')]
class ImageApiController extends AbstractController
{
    private const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
    private const MAX_BYTES    = 30 * 1024 * 1024; // 30 MB

    public function __construct(
        #[Autowire('%kernel.project_dir%')]
        private readonly string $projectDir,
    ) {}

    #[Route('', name: 'api_images_list', methods: ['GET'])]
    public function list(EntityManagerInterface $em): JsonResponse
    {
        $user   = $this->getUser();
        $images = $em->getRepository(Image::class)->findBy(
            ['user' => $user, 'deletedAt' => null],
            ['uploadedAt' => 'DESC'],
        );

        return $this->json(array_map($this->serialize(...), $images));
    }

    #[Route('', name: 'api_images_upload', methods: ['POST'])]
    public function upload(
        Request                $request,
        EntityManagerInterface $em,
        MessageBusInterface    $bus,
    ): JsonResponse {
        $file = $request->files->get('file');
        if (!$file) {
            return $this->json(['error' => 'No file uploaded'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }
        if ($file->getSize() > self::MAX_BYTES) {
            return $this->json(['error' => 'File too large (max 30 MB)'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }
        if (!in_array($file->getMimeType(), self::ALLOWED_MIME, true)) {
            return $this->json(['error' => 'Unsupported file type'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        /** @var \App\Entity\User $user */
        $user = $this->getUser();

        // Create Image entity first with a placeholder path so we have the ID
        $image = (new Image())
            ->setUser($user)
            ->setOriginalFilename($file->getClientOriginalName());
        $em->persist($image);
        $em->flush(); // get ID

        // Build storage directory
        $storageDir = $this->projectDir . '/var/storage/images/' . $image->getId();
        if (!is_dir($storageDir)) {
            mkdir($storageDir, 0755, true);
        }

        // If a separate pre-crop original was sent, save it; otherwise the uploaded file IS the original
        $originalFile = $request->files->get('original') ?? $file;
        $ext          = strtolower($originalFile->guessExtension() ?? 'jpg');
        $origRelPath  = 'var/storage/images/' . $image->getId() . '/original.' . $ext;
        $originalFile->move($storageDir, 'original.' . $ext);

        // If a composited (cropped+stickered) version was also sent, save it separately
        if ($request->files->get('original')) {
            // $file is the composited; move to composited.jpg for the renderer
            $file->move($storageDir, 'composited.jpg');
        }

        $image->setStoragePath($origRelPath);

        if ($request->request->has('cropParams')) {
            $image->setCropParams($request->request->get('cropParams'));
        }
        if ($request->request->has('stickerState')) {
            $image->setStickerState($request->request->get('stickerState'));
        }
        if ($request->request->has('cropOrientation')) {
            $image->setCropOrientation(
                Orientation::tryFrom((string) $request->request->get('cropOrientation'))
            );
        }

        // Generate thumbnail from composited if available, otherwise from original
        $thumbSrc = file_exists($storageDir . '/composited.jpg')
            ? $storageDir . '/composited.jpg'
            : $storageDir . '/original.' . $ext;
        $this->generateThumbnail($thumbSrc, $storageDir . '/thumbnail.jpg');

        $em->flush();

        // Dispatch rendering for all model × orientation combos
        foreach (DeviceModel::cases() as $model) {
            foreach (Orientation::cases() as $orientation) {
                $asset = (new RenderedAsset())
                    ->setImage($image)
                    ->setDeviceModel($model)
                    ->setOrientation($orientation);
                $em->persist($asset);
                $bus->dispatch(new RenderImageMessage($image->getId(), $model->value, $orientation->value));
            }
        }
        $em->flush();

        return $this->json($this->serialize($image), Response::HTTP_CREATED);
    }

    #[Route('/{id}', name: 'api_images_delete', methods: ['DELETE'])]
    public function delete(int $id, EntityManagerInterface $em): JsonResponse
    {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $image->setDeletedAt(new \DateTimeImmutable());
        $em->flush();

        return $this->json(null, Response::HTTP_NO_CONTENT);
    }

    #[Route('/{id}/thumbnail', name: 'api_images_thumbnail', methods: ['GET'])]
    public function thumbnail(int $id, EntityManagerInterface $em): Response
    {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $thumbPath = $this->projectDir . '/var/storage/images/' . $id . '/thumbnail.jpg';
        if (!file_exists($thumbPath)) {
            return $this->json(['error' => 'Thumbnail not ready'], Response::HTTP_NOT_FOUND);
        }

        return new BinaryFileResponse($thumbPath);
    }

    #[Route('/{id}/original', name: 'api_images_original', methods: ['GET'])]
    public function original(int $id, EntityManagerInterface $em): Response
    {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $storageDir = $this->projectDir . '/var/storage/images/' . $id;
        foreach (['original.jpg', 'original.png', 'original.webp', 'original.gif'] as $candidate) {
            $path = $storageDir . '/' . $candidate;
            if (file_exists($path)) {
                return new BinaryFileResponse($path);
            }
        }

        return $this->json(['error' => 'Original not found'], Response::HTTP_NOT_FOUND);
    }

    #[Route('/{id}/reprocess', name: 'api_images_reprocess', methods: ['POST'])]
    public function reprocess(
        int $id,
        Request $request,
        EntityManagerInterface $em,
        MessageBusInterface $bus,
    ): JsonResponse {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $file = $request->files->get('file');
        if (!$file) {
            return $this->json(['error' => 'No file uploaded'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $storageDir = $this->projectDir . '/var/storage/images/' . $id;

        // Overwrite composited with the new version
        $file->move($storageDir, 'composited.jpg');

        // Regenerate thumbnail from new composited
        $this->generateThumbnail($storageDir . '/composited.jpg', $storageDir . '/thumbnail.jpg');

        // Persist updated crop/sticker metadata if provided
        if ($request->request->has('cropParams')) {
            $image->setCropParams($request->request->get('cropParams'));
        }
        if ($request->request->has('stickerState')) {
            $image->setStickerState($request->request->get('stickerState'));
        }
        if ($request->request->has('cropOrientation')) {
            $image->setCropOrientation(
                Orientation::tryFrom((string) $request->request->get('cropOrientation'))
            );
        }

        // Reset all rendered assets so they re-render from the new composited
        foreach ($image->getRenderedAssets() as $asset) {
            $asset->setStatus(RenderStatus::Pending)->setFilePath(null);
            $bus->dispatch(new RenderImageMessage($id, $asset->getDeviceModel()->value, $asset->getOrientation()->value));
        }

        $em->flush();

        return $this->json($this->serialize($image));
    }

    #[Route('/{id}/share', name: 'api_images_share', methods: ['POST'])]
    public function share(int $id, Request $request, EntityManagerInterface $em, ShareService $shareService): JsonResponse
    {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $body  = json_decode($request->getContent(), true) ?? [];
        $email = trim((string) ($body['recipientEmail'] ?? ''));
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->json(['error' => 'Invalid email address'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        try {
            /** @var \App\Entity\User $user */
            $user   = $this->getUser();
            $shareService->share($image, $user, $email);
        } catch (\InvalidArgumentException $e) {
            return $this->json(['error' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        return $this->json(null, Response::HTTP_NO_CONTENT);
    }

    #[Route('/{id}/hard-delete-request', name: 'api_images_hard_delete_request', methods: ['POST'])]
    public function hardDeleteRequest(int $id, EntityManagerInterface $em, TokenService $tokenService): JsonResponse
    {
        $image = $this->findOwnedImage($id, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        /** @var \App\Entity\User $user */
        $user  = $this->getUser();
        $ttl   = (int) ($_ENV['HARD_DELETE_TOKEN_TTL_DAYS'] ?? 30);
        $tokenService->issue(TokenType::HardDeleteConfirm, $image, $user, $user->getEmail(), $ttl);
        $em->flush();

        return $this->json(null, Response::HTTP_NO_CONTENT);
    }

    #[Route('/{id}/approve/{deviceId}', name: 'api_images_approve', methods: ['POST'])]
    public function approve(int $id, int $deviceId, EntityManagerInterface $em): JsonResponse
    {
        return $this->toggleApproval($id, $deviceId, $em, true);
    }

    #[Route('/{id}/approve/{deviceId}', name: 'api_images_revoke', methods: ['DELETE'])]
    public function revoke(int $id, int $deviceId, EntityManagerInterface $em): JsonResponse
    {
        return $this->toggleApproval($id, $deviceId, $em, false);
    }

    private function toggleApproval(int $imageId, int $deviceId, EntityManagerInterface $em, bool $approve): JsonResponse
    {
        /** @var \App\Entity\User $user */
        $user = $this->getUser();

        $image = $this->findOwnedImage($imageId, $em);
        if (!$image) {
            return $this->json(['error' => 'Not found'], Response::HTTP_NOT_FOUND);
        }

        $device = $em->getRepository(Device::class)->findOneBy(['id' => $deviceId, 'user' => $user]);
        if (!$device) {
            return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
        }

        if ($approve) {
            $image->approveForDevice($device);
        } else {
            $image->revokeForDevice($device);
        }
        $em->flush();

        return $this->json($this->serialize($image));
    }

    private function findOwnedImage(int $id, EntityManagerInterface $em): ?Image
    {
        $image = $em->getRepository(Image::class)->findOneBy([
            'id'        => $id,
            'user'      => $this->getUser(),
            'deletedAt' => null,
        ]);
        return $image;
    }

    private function serialize(Image $image): array
    {
        $id = $image->getId();
        return [
            'id'               => $id,
            'originalFilename' => $image->getOriginalFilename(),
            'thumbnailUrl'     => '/api/images/' . $id . '/thumbnail',
            'originalUrl'      => '/api/images/' . $id . '/original',
            'uploadedAt'       => $image->getUploadedAt()->format(\DateTimeInterface::ATOM),
            'approvedDeviceIds' => array_values($image->getApprovedDevices()->map(fn($d) => $d->getId())->toArray()),
            'cropParams'       => $image->getCropParams() ? json_decode($image->getCropParams(), true) : null,
            'stickerState'     => $image->getStickerState() ? json_decode($image->getStickerState(), true) : null,
            'cropOrientation'  => $image->getCropOrientation()?->value,
        ];
    }

    private function generateThumbnail(string $srcPath, string $destPath): void
    {
        $imagick = new \Imagick($srcPath);
        $imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
        $imagick->setBackgroundColor('white');
        $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
        $imagick->autoOrient();
        $imagick->thumbnailImage(800, 600, true);
        $imagick->setImageFormat('jpeg');
        $imagick->setImageCompressionQuality(80);
        $imagick->writeImage($destPath);
        $imagick->destroy();
    }
}
