<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Device;
use App\Entity\RenderedAsset;
use App\Enum\RenderStatus;
use App\Service\RotationService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DeviceImageController extends AbstractController
{
    public function __construct(
        #[Autowire('%kernel.project_dir%')]
        private readonly string $projectDir,
        private readonly RotationService $rotation,
        private readonly LoggerInterface $logger,
    ) {}

    private function computeIntervalMs(Device $device): int
    {
        if ($device->getWakeHour() !== null) {
            $tz  = new \DateTimeZone($device->getTimezone());
            $now = new \DateTimeImmutable('now', $tz);
            $next = $now->setTime($device->getWakeHour(), 0, 0);
            if ($next->getTimestamp() <= $now->getTimestamp()) {
                $next = $next->modify('+1 day');
            }
            return (int) (($next->getTimestamp() - $now->getTimestamp()) * 1000);
        }

        return $device->getRotationIntervalMinutes() * 60 * 1000;
    }

    #[Route('/api/device/{mac}/image', name: 'api_device_image', methods: ['GET'])]
    public function image(string $mac, Request $request, EntityManagerInterface $em): Response
    {
        $device = $em->getRepository(Device::class)->findOneBy(['mac' => strtoupper($mac)]);
        if (!$device) {
            $this->logger->warning('device.poll.unknown_mac', ['mac' => $mac]);
            return new Response(null, Response::HTTP_NOT_FOUND);
        }

        $intervalMs     = $this->computeIntervalMs($device);
        $currentImageId = (int) $request->headers->get('X-Current-Image-Id', '-1');
        $device->markSeen();

        // Locked image bypasses rotation entirely.
        $image = $device->getLockedImage() ?? $this->rotation->advance($device);

        if ($image === null) {
            $this->logger->info('device.poll.no_image', [
                'device_id'   => $device->getId(),
                'mac'         => $mac,
                'interval_ms' => $intervalMs,
            ]);
            $r = new Response(null, Response::HTTP_NO_CONTENT);
            $r->headers->set('X-Interval-Ms', (string) $intervalMs);
            return $r;
        }

        // Asset lookup is needed before the 304 check so we can compare its
        // rendered_at timestamp — otherwise a re-cropped image (same id, same
        // orientation, new bytes) would be incorrectly served as 304.
        $asset = $em->getRepository(RenderedAsset::class)->findOneBy([
            'image'       => $image,
            'deviceModel' => $device->getModel(),
            'orientation' => $device->getOrientation(),
            'status'      => RenderStatus::Ready,
        ]);

        if (!$asset?->getFilePath()) {
            $this->logger->warning('device.poll.no_asset', [
                'device_id'   => $device->getId(),
                'mac'         => $mac,
                'image_id'    => $image->getId(),
                'model'       => $device->getModel()->value,
                'orientation' => $device->getOrientation()->value,
            ]);
            $r = new Response(null, Response::HTTP_NO_CONTENT);
            $r->headers->set('X-Interval-Ms', (string) $intervalMs);
            return $r;
        }

        // 304: device already has this image — skip the binary transfer and redraw.
        // The image id, the orientation we last served at, AND the asset's
        // rendered_at must all match. A re-render (e.g. after re-crop) advances
        // rendered_at, so the device's cached bytes are stale and we re-send.
        $renderedAt = $asset->getRenderedAt();
        if ($image->getId() === $currentImageId
            && $device->getCurrentImageOrientation() === $device->getOrientation()
            && $renderedAt !== null
            && $device->getCurrentRenderedAt()?->getTimestamp() === $renderedAt->getTimestamp()) {
            $this->logger->info('device.poll.no_change', [
                'device_id'   => $device->getId(),
                'mac'         => $mac,
                'image_id'    => $image->getId(),
                'interval_ms' => $intervalMs,
            ]);
            $r = new Response(null, Response::HTTP_NOT_MODIFIED);
            $r->headers->set('X-Image-Id',    (string) $image->getId());
            $r->headers->set('X-Interval-Ms', (string) $intervalMs);
            return $r;
        }

        $binPath = $this->projectDir . '/' . $asset->getFilePath();
        if (!file_exists($binPath)) {
            $this->logger->error('device.poll.file_missing', [
                'device_id' => $device->getId(),
                'mac'       => $mac,
                'image_id'  => $image->getId(),
                'path'      => $binPath,
            ]);
            $r = new Response(null, Response::HTTP_NO_CONTENT);
            $r->headers->set('X-Interval-Ms', (string) $intervalMs);
            return $r;
        }

        // Record the orientation and rendered_at we're serving at so the next
        // poll's 304 check can detect a flip or a re-render and force a re-fetch.
        $device->setCurrentImageOrientation($device->getOrientation());
        $device->setCurrentRenderedAt($renderedAt);
        $em->flush();

        $this->logger->info('device.poll.served', [
            'device_id'   => $device->getId(),
            'mac'         => $mac,
            'image_id'    => $image->getId(),
            'orientation' => $device->getOrientation()->value,
            'interval_ms' => $intervalMs,
            'bytes'       => filesize($binPath),
        ]);

        $response = new BinaryFileResponse($binPath);
        $response->headers->set('Content-Type',  'application/octet-stream');
        $response->headers->set('X-Image-Id',    (string) $image->getId());
        $response->headers->set('X-Interval-Ms', (string) $intervalMs);

        return $response;
    }
}
