<?php

namespace Core;

readonly class TOTP
{
    private const BASE32CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // RFC 4648 Base32

    public function __construct(private string $secret) {}

    public static function otpSecret($uniqueString, $length = 20): string
    {
        // Уникальная строка + текущее время в микросекундах
        $seed = $uniqueString . microtime(true);

        // Хэшируем сид для более случайного распределения
        $seed = hash('sha256', $seed);

        $secret = '';
        $max = strlen(self::BASE32CHARS) - 1;

        // Генерируем случайную строку нужной длины
        for ($i = 0; $i < $length; $i++) {
            $index = hexdec(substr($seed, $i, 2)) % $max; // Получаем индекс символа из хэшированного сида
            $secret .= self::BASE32CHARS[$index];
        }

        return $secret;
    }

    public function generate(int $digits = 6, int $timeStep = 30): string
    {
        $time = floor(time() / $timeStep);
        $secretKey = $this->base32_decode($this->secret);
        $binaryTime = pack('N*', 0) . pack('N*', $time);
        $hash = hash_hmac('sha1', $binaryTime, $secretKey, true);
        $offset = ord($hash[strlen($hash) - 1]) & 0x0F;
        $otp = (
                ((ord($hash[$offset]) & 0x7F) << 24) |
                ((ord($hash[$offset + 1]) & 0xFF) << 16) |
                ((ord($hash[$offset + 2]) & 0xFF) << 8) |
                (ord($hash[$offset + 3]) & 0xFF)
            ) % 10 ** $digits;
        return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT);
    }

    private function base32_decode(string $input): string
    {
        $base32charsFlipped = array_flip(str_split(self::BASE32CHARS));
        $output = '';
        $v = 0;
        $vbits = 0;

        for ($i = 0, $j = strlen($input); $i < $j; $i++) {
            $v <<= 5;
            if ($input[$i] == '=') continue;
            $v += $base32charsFlipped[$input[$i]];
            $vbits += 5;

            if ($vbits >= 8) {
                $vbits -= 8;
                $output .= chr(($v & (0xFF << $vbits)) >> $vbits);
            }
        }
        return $output;
    }
}