<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Device;
use App\Entity\Image;
use App\Entity\RenderedAsset;
use App\Entity\User;
use App\Enum\Orientation;
use App\Enum\RenderStatus;
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\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/api/devices')]
#[IsGranted('ROLE_USER')]
class DeviceApiController extends AbstractController
{
    // Spectra 6 palette — must match RenderImageMessageHandler::PALETTE.
    // Used to decode the device's 4bpp .bin into an RGB preview that mirrors
    // exactly what the e-ink displays.
    private const PALETTE = [
        0x0 => [26,  26,  26 ],  // BLACK
        0x1 => [245, 245, 240],  // WHITE
        0x2 => [240, 208, 0  ],  // YELLOW
        0x3 => [192, 48,  32 ],  // RED
        0x5 => [24,  64,  192],  // BLUE
        0x6 => [16,  160, 64 ],  // GREEN
    ];

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

    #[Route('', name: 'api_devices_list', methods: ['GET'])]
    public function list(EntityManagerInterface $em): JsonResponse
    {
        /** @var User $user */
        $user = $this->getUser();
        $devices = $em->getRepository(Device::class)->findBy(['user' => $user]);

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

    #[Route('/{id}', name: 'api_device_update', methods: ['PATCH'])]
    public function update(int $id, Request $request, EntityManagerInterface $em): JsonResponse
    {
        /** @var User $user */
        $user   = $this->getUser();
        $device = $em->getRepository(Device::class)->findOneBy(['id' => $id, 'user' => $user]);

        if (!$device) {
            return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
        }

        $body = json_decode($request->getContent(), true) ?? [];

        if (isset($body['name'])) {
            $name = trim((string) $body['name']);
            if (empty($name)) {
                return $this->json(['error' => 'Name cannot be empty'], Response::HTTP_UNPROCESSABLE_ENTITY);
            }
            $device->setName($name);
        }

        if (isset($body['orientation'])) {
            $orientation = Orientation::tryFrom($body['orientation']);
            if (!$orientation) {
                return $this->json(['error' => 'Invalid orientation'], Response::HTTP_UNPROCESSABLE_ENTITY);
            }
            $device->setOrientation($orientation);
        }

        if (isset($body['rotationIntervalMinutes'])) {
            $device->setRotationIntervalMinutes(max(1, (int) $body['rotationIntervalMinutes']));
        }

        if (array_key_exists('wakeHour', $body)) {
            $device->setWakeHour($body['wakeHour'] === null ? null : (int) $body['wakeHour']);
        }

        if (isset($body['timezone'])) {
            try {
                new \DateTimeZone((string) $body['timezone']);
                $device->setTimezone((string) $body['timezone']);
            } catch (\Exception) {
                return $this->json(['error' => 'Invalid timezone identifier'], Response::HTTP_UNPROCESSABLE_ENTITY);
            }
        }

        if (isset($body['uniquenessWindow'])) {
            $device->setUniquenessWindow(max(1, (int) $body['uniquenessWindow']));
        }

        $em->flush();

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

    #[Route('/{id}/lock', name: 'api_device_lock', methods: ['PUT'])]
    public function lock(int $id, Request $request, EntityManagerInterface $em): JsonResponse
    {
        /** @var User $user */
        $user   = $this->getUser();
        $device = $em->getRepository(Device::class)->findOneBy(['id' => $id, 'user' => $user]);

        if (!$device) {
            return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
        }

        $body    = json_decode($request->getContent(), true) ?? [];
        $imageId = $body['imageId'] ?? null;

        if (!$imageId) {
            return $this->json(['error' => 'imageId required'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $image = $em->getRepository(Image::class)->find($imageId);
        if (!$image || $image->getUser() !== $user) {
            return $this->json(['error' => 'Image not found'], Response::HTTP_NOT_FOUND);
        }

        if (!$image->isApprovedForDevice($device)) {
            return $this->json(['error' => 'Image is not approved for this device'], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $device->setLockedImage($image);
        $em->flush();

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

    #[Route('/{id}/lock', name: 'api_device_unlock', methods: ['DELETE'])]
    public function unlock(int $id, EntityManagerInterface $em): JsonResponse
    {
        /** @var User $user */
        $user   = $this->getUser();
        $device = $em->getRepository(Device::class)->findOneBy(['id' => $id, 'user' => $user]);

        if (!$device) {
            return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
        }

        $device->setLockedImage(null);
        $em->flush();

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

    private function serialize(Device $d): array
    {
        return [
            'id'                      => $d->getId(),
            'mac'                     => $d->getMac(),
            'name'                    => $d->getName(),
            'orientation'             => $d->getOrientation()->value,
            'rotationIntervalMinutes' => $d->getRotationIntervalMinutes(),
            'wakeHour'                => $d->getWakeHour(),
            'timezone'                => $d->getTimezone(),
            'uniquenessWindow'        => $d->getUniquenessWindow(),
            'linkedAt'                => $d->getLinkedAt()->format(\DateTimeInterface::ATOM),
            'lastSeenAt'              => $d->getLastSeenAt()?->format(\DateTimeInterface::ATOM),
            'lockedImageId'           => $d->getLockedImage()?->getId(),
            'currentImageId'          => $d->getCurrentImage()?->getId(),
        ];
    }

    /**
     * Serve a PNG preview of the image currently shown on the frame, decoded
     * from the device's rendered 4bpp Spectra-6 .bin so the colors match what
     * the e-ink actually displays. The PNG is cached on disk next to the .bin.
     */
    #[Route('/{id}/preview', name: 'api_device_preview', methods: ['GET'])]
    public function preview(int $id, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user   = $this->getUser();
        $device = $em->getRepository(Device::class)->findOneBy(['id' => $id, 'user' => $user]);
        if (!$device) {
            return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
        }

        $image = $device->getLockedImage() ?? $device->getCurrentImage();
        if (!$image || $image->isDeleted()) {
            return $this->json(['error' => 'No current image'], Response::HTTP_NOT_FOUND);
        }

        $asset = $em->getRepository(RenderedAsset::class)->findOneBy([
            'image'       => $image,
            'deviceModel' => $device->getModel(),
            'orientation' => $device->getOrientation(),
            'status'      => RenderStatus::Ready,
        ]);
        if (!$asset?->getFilePath()) {
            return $this->json(['error' => 'Render not ready'], Response::HTTP_NOT_FOUND);
        }

        $binPath = $this->projectDir . '/' . $asset->getFilePath();
        if (!file_exists($binPath)) {
            return $this->json(['error' => 'Render file missing'], Response::HTTP_NOT_FOUND);
        }

        $pngPath = preg_replace('/\.bin$/', '.png', $binPath);
        if (!file_exists($pngPath) || filemtime($pngPath) < filemtime($binPath)) {
            $this->renderBinToPng(
                $binPath,
                $pngPath,
                $device->getModel()->nativeWidth(),
                $device->getModel()->nativeHeight(),
                $device->getOrientation(),
            );
        }

        $response = new BinaryFileResponse($pngPath);
        $response->headers->set('Content-Type', 'image/png');
        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_INLINE,
            'frame-' . $device->getId() . '.png',
        );
        return $response;
    }

    private function renderBinToPng(string $binPath, string $pngPath, int $width, int $height, Orientation $orientation): void
    {
        $bin   = (string) file_get_contents($binPath);
        $len   = strlen($bin);
        $rgb   = '';
        $white = self::PALETTE[0x1];
        for ($i = 0; $i < $len; $i++) {
            $byte = ord($bin[$i]);
            foreach ([($byte >> 4) & 0xF, $byte & 0xF] as $idx) {
                $c = self::PALETTE[$idx] ?? $white;
                $rgb .= chr($c[0]) . chr($c[1]) . chr($c[2]);
            }
        }

        // Wrap the raw RGB bytes in a PPM (P6) header so Imagick can decode the
        // blob directly. importImagePixels would also work but its current
        // signature requires an array of ints, which is ~70 MB for an 800×480.
        $ppm = "P6\n{$width} {$height}\n255\n" . $rgb;

        $im = new \Imagick();
        $im->readImageBlob($ppm);

        // The .bin is always laid out in EPD-native scan order. For portrait,
        // the renderer pre-rotated the photo 90° CCW; rotate 90° here so the
        // browser-side preview shows the photo upright.
        if ($orientation === Orientation::Portrait) {
            $im->rotateImage(new \ImagickPixel('white'), 90);
        }

        $im->setImageFormat('png');
        $im->writeImage($pngPath);
        $im->destroy();
    }
}
