<?php

declare(strict_types=1);

namespace App\Entity;

use App\Enum\DeviceModel;
use App\Entity\Image;
use App\Enum\Orientation;
use App\Enum\RotationMode;
use App\Repository\DeviceRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: DeviceRepository::class)]
class Device
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 17, unique: true)]
    private string $mac = '';

    #[ORM\Column(length: 100)]
    private string $name = '';

    #[ORM\Column(enumType: DeviceModel::class)]
    private DeviceModel $model = DeviceModel::V1;

    #[ORM\Column(enumType: Orientation::class)]
    private Orientation $orientation = Orientation::Landscape;

    /** Minutes between rotation cycles (used when wakeTimes is empty). */
    #[ORM\Column]
    private int $rotationIntervalMinutes = 1440;

    /**
     * Wake times stored as minutes-since-midnight (0-1439) in `timezone`.
     * Empty array = use rotationIntervalMinutes (interval mode).
     * Non-empty = wake at each listed time of day.
     *
     * @var int[]
     */
    #[ORM\Column(type: 'json')]
    private array $wakeTimes = [];

    /** IANA timezone for wakeTimes scheduling (e.g. 'Europe/Stockholm'). */
    #[ORM\Column(length: 60)]
    private string $timezone = 'UTC';

    /** Number of display cycles before an image may repeat. */
    #[ORM\Column]
    private int $uniquenessWindow = 10;

    /** How RotationService picks the next image once the uniqueness filter has run. */
    #[ORM\Column(length: 32, enumType: RotationMode::class)]
    private RotationMode $rotationMode = RotationMode::OldestUpload;

    /**
     * When true and the candidate set contains any never-shown images, the
     * selection narrows to those before the rotation mode runs. Lets users
     * burn through new uploads before the catalog re-shuffles.
     */
    #[ORM\Column]
    private bool $prioritizeNeverShown = false;

    #[ORM\ManyToOne(inversedBy: 'devices')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $user = null;

    #[ORM\Column]
    private \DateTimeImmutable $linkedAt;

    #[ORM\ManyToOne]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?Image $currentImage = null;

    /** When set, rotation is bypassed and this image is always served. */
    #[ORM\ManyToOne]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?Image $lockedImage = null;

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $lastSeenAt = null;

    /**
     * Server-stamped wall-clock time at which the device is expected to poll
     * next, computed as `now + computeIntervalMs($device)` at every successful
     * response. The PWA reads this directly so the displayed "next sync"
     * always reflects the schedule the device is *actually on* — not whatever
     * the user just saved through the settings sheet (which the device won't
     * pick up until that next poll).
     */
    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $nextPollExpectedAt = null;

    /**
     * Orientation in effect when currentImage was last served as a 200 response.
     * Used alongside currentImage's id to decide whether a poll can be answered
     * with 304: if the device's orientation has flipped since, the cached image
     * on-device is stale and we must re-send the freshly-rendered .bin.
     */
    #[ORM\Column(nullable: true, enumType: Orientation::class)]
    private ?Orientation $currentImageOrientation = null;

    /**
     * RenderedAsset.rendered_at timestamp at the moment we last served this
     * device its current image. Lets the 304 cache check detect a re-render
     * (e.g. user re-cropped the image) — the image id and orientation may
     * still match, but the bytes have changed and we must re-send.
     */
    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $currentRenderedAt = null;

    public function __construct()
    {
        $this->linkedAt = new \DateTimeImmutable();
    }

    public function getId(): ?int { return $this->id; }

    public function getMac(): string { return $this->mac; }
    public function setMac(string $mac): static { $this->mac = strtoupper($mac); return $this; }

    public function getName(): string { return $this->name; }
    public function setName(string $name): static { $this->name = $name; return $this; }

    public function getModel(): DeviceModel { return $this->model; }
    public function setModel(DeviceModel $model): static { $this->model = $model; return $this; }

    public function getOrientation(): Orientation { return $this->orientation; }
    public function setOrientation(Orientation $orientation): static { $this->orientation = $orientation; return $this; }

    public function getRotationIntervalMinutes(): int { return $this->rotationIntervalMinutes; }
    public function setRotationIntervalMinutes(int $m): static { $this->rotationIntervalMinutes = $m; return $this; }

    /** @return int[] */
    public function getWakeTimes(): array { return $this->wakeTimes; }

    /**
     * @param int[] $minutes minutes-since-midnight, 0-1439
     */
    public function setWakeTimes(array $minutes): static
    {
        $clean = [];
        foreach ($minutes as $m) {
            $m = (int) $m;
            if ($m >= 0 && $m <= 1439) $clean[$m] = true;
        }
        $clean = array_keys($clean);
        sort($clean);
        $this->wakeTimes = $clean;
        return $this;
    }

    public function getTimezone(): string { return $this->timezone; }
    public function setTimezone(string $tz): static { $this->timezone = $tz; return $this; }

    public function getUniquenessWindow(): int { return $this->uniquenessWindow; }
    public function setUniquenessWindow(int $w): static { $this->uniquenessWindow = $w; return $this; }

    public function getRotationMode(): RotationMode { return $this->rotationMode; }
    public function setRotationMode(RotationMode $m): static { $this->rotationMode = $m; return $this; }

    public function isPrioritizeNeverShown(): bool { return $this->prioritizeNeverShown; }
    public function setPrioritizeNeverShown(bool $v): static { $this->prioritizeNeverShown = $v; return $this; }

    public function getUser(): ?User { return $this->user; }
    public function setUser(?User $user): static { $this->user = $user; return $this; }

    public function getLinkedAt(): \DateTimeImmutable { return $this->linkedAt; }
    public function setLinkedAt(\DateTimeImmutable $dt): static { $this->linkedAt = $dt; return $this; }

    public function getCurrentImage(): ?Image { return $this->currentImage; }
    public function setCurrentImage(?Image $image): static { $this->currentImage = $image; return $this; }

    public function getLockedImage(): ?Image { return $this->lockedImage; }
    public function setLockedImage(?Image $image): static { $this->lockedImage = $image; return $this; }

    public function getLastSeenAt(): ?\DateTimeImmutable { return $this->lastSeenAt; }
    public function markSeen(): static { $this->lastSeenAt = new \DateTimeImmutable(); return $this; }

    public function getNextPollExpectedAt(): ?\DateTimeImmutable { return $this->nextPollExpectedAt; }
    public function setNextPollExpectedAt(?\DateTimeImmutable $t): static { $this->nextPollExpectedAt = $t; return $this; }

    public function getCurrentImageOrientation(): ?Orientation { return $this->currentImageOrientation; }
    public function setCurrentImageOrientation(?Orientation $o): static { $this->currentImageOrientation = $o; return $this; }

    public function getCurrentRenderedAt(): ?\DateTimeImmutable { return $this->currentRenderedAt; }
    public function setCurrentRenderedAt(?\DateTimeImmutable $t): static { $this->currentRenderedAt = $t; return $this; }
}
