<?php
// =============================================================
//  include/CheckinToken.php
//
//  Rotating per-unit token for QR self-check-in. Deliberately NOT stored
//  in the database — derived on demand from (unit_id + time window +
//  a server-side secret), verified the same way. This means:
//    - No new "secret" column/table needed for the token mechanism itself
//    - The QR code goes stale automatically every PERIOD seconds, so a
//      screenshotted/shared code stops working shortly after — the main
//      anti-spoofing property, without needing GPS/geofencing.
//
//  Requires CHECKIN_HMAC_SECRET in .env — see .env.example.
// =============================================================

final class CheckinToken
{
    private const PERIOD = 60; // seconds per rotation — long enough for a human to scan

    /**
     * @param string|null $secretOverride Only for tests — normal callers always
     *   let this read CHECKIN_HMAC_SECRET from .env. Keeps the test suite from
     *   depending on a real secret being configured (CI copies .env.example,
     *   which deliberately leaves this blank — same as every other real credential).
     * @throws RuntimeException if CHECKIN_HMAC_SECRET isn't configured and no override was given
     */
    public static function generate(int $unitId, ?int $timeWindow = null, ?string $secretOverride = null): string
    {
        $secret = $secretOverride ?? env('CHECKIN_HMAC_SECRET', '');
        if ($secret === '') {
            throw new RuntimeException('CHECKIN_HMAC_SECRET is not configured in .env');
        }
        $window = $timeWindow ?? (int)floor(time() / self::PERIOD);
        $hash   = hash_hmac('sha256', $unitId . ':' . $window, $secret);
        return strtoupper(substr($hash, 0, 8));
    }

    /** Accepts the current window and the previous one, so a scan right at a rotation boundary still works. */
    public static function verify(int $unitId, string $token, ?string $secretOverride = null): bool
    {
        $token  = strtoupper(trim($token));
        $window = (int)floor(time() / self::PERIOD);
        foreach ([$window, $window - 1] as $w) {
            if (hash_equals(self::generate($unitId, $w, $secretOverride), $token)) return true;
        }
        return false;
    }

    /** Seconds remaining until the current token rotates — for the display page's countdown/refresh. */
    public static function secondsUntilRotation(): int
    {
        return self::PERIOD - (time() % self::PERIOD);
    }
}
