<?php

declare(strict_types=1);

namespace App\Domain\Auth;

/**
 * Generates raw token strings in the form `irdb_<kind3>_<32-char-base32>`.
 *
 * 160 bits of entropy from random_bytes(20), encoded with RFC 4648 base32
 * (A–Z, 2–7) without padding — exactly 32 base32 chars. Returns the raw
 * token; hashing for storage is the caller's responsibility.
 */
final class TokenIssuer
{
    private const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    /** Required input length for the base32 encoder. 20 × 8 = 160 = 32 × 5. */
    private const ENTROPY_BYTES = 20;

    public function issue(TokenKind $kind): string
    {
        return sprintf('irdb_%s_%s', $kind->code(), self::base32Encode(random_bytes(self::ENTROPY_BYTES)));
    }

    /**
     * Encodes exactly 20 raw bytes (160 bits) as 32 base32 chars.
     *
     * 160 ÷ 5 = 32 with zero remainder — every base32 char in the output
     * carries 5 useful bits, there is no trailing-bit ambiguity, no
     * padding, and exactly one canonical encoding per input. SEC_REVIEW
     * F39 noted a "the trailing 5-bit chunk is zero-padded" concern; the
     * concern was based on the previous unreachable `if (strlen($chunk)
     * < 5)` branch in this method, which never fires for a 20-byte
     * input. We now assert the length up front and refuse anything else
     * — any future caller that passes the wrong length crashes loudly
     * rather than silently emitting a non-canonical / shorter encoding.
     */
    private static function base32Encode(string $bytes): string
    {
        if (strlen($bytes) !== self::ENTROPY_BYTES) {
            throw new \InvalidArgumentException(sprintf(
                'TokenIssuer::base32Encode requires exactly %d bytes; got %d',
                self::ENTROPY_BYTES,
                strlen($bytes),
            ));
        }

        $bits = '';
        for ($i = 0; $i < self::ENTROPY_BYTES; ++$i) {
            $bits .= str_pad(decbin(ord($bytes[$i])), 8, '0', STR_PAD_LEFT);
        }

        $out = '';
        // 160 bits / 5 = 32 iterations exactly; no partial trailing chunk.
        for ($i = 0; $i < 160; $i += 5) {
            $out .= self::BASE32_ALPHABET[bindec(substr($bits, $i, 5))];
        }

        return $out;
    }
}
