<?php

declare(strict_types=1);

class TwoFactor
{
    private const PERIOD = 30;
    private const DIGITS = 6;
    private const SECRET_BYTES = 10;
    private const BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    public static function generateSecret(): string
    {
        return self::base32Encode(random_bytes(self::SECRET_BYTES));
    }

    

    public static function verify(string $secret, string $code, int $window = 1, ?int $lastCounter = null): int
    {
        if (!preg_match('/^\d{6}$/', $code)) {
            return 0;
        }

        $secretBytes = self::base32Decode($secret);
        $currentPeriod = (int) floor(time() / self::PERIOD);

        for ($i = -$window; $i <= $window; $i++) {
            $counter = $currentPeriod + $i;
            if (hash_equals(self::generateCode($secretBytes, $counter), $code)) {
                 
                if ($lastCounter !== null && $counter <= $lastCounter) {
                    return 0;
                }
                return $counter;
            }
        }

        return 0;
    }

    public static function getQrUri(string $label, string $secret, string $issuer = 'TubePress'): string
    {
        return 'otpauth://totp/' . rawurlencode($issuer . ':' . $label)
            . '?secret=' . $secret
            . '&issuer=' . rawurlencode($issuer)
            . '&algorithm=SHA1'
            . '&digits=' . self::DIGITS
            . '&period=' . self::PERIOD;
    }

    private static function generateCode(string $secretBytes, int $counter): string
    {
         
        $counterBytes = pack('N*', 0, $counter);
        $hash = hash_hmac('sha1', $counterBytes, $secretBytes, true);

         
        $offset = ord($hash[19]) & 0x0F;
        $binary = (
            ((ord($hash[$offset]) & 0x7F) << 24) |
            ((ord($hash[$offset + 1]) & 0xFF) << 16) |
            ((ord($hash[$offset + 2]) & 0xFF) << 8) |
            (ord($hash[$offset + 3]) & 0xFF)
        );

        return str_pad((string) ($binary % (10 ** self::DIGITS)), self::DIGITS, '0', STR_PAD_LEFT);
    }

    private static function base32Encode(string $data): string
    {
        $binary = '';
        foreach (str_split($data) as $char) {
            $binary .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
        }

        $result = '';
        $chunks = str_split($binary, 5);
        foreach ($chunks as $chunk) {
            $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
            $result .= self::BASE32_CHARS[bindec($chunk)];
        }

        return $result;
    }

    private static function base32Decode(string $data): string
    {
        $data = strtoupper($data);
        $binary = '';
        foreach (str_split($data) as $char) {
            $pos = strpos(self::BASE32_CHARS, $char);
            if ($pos === false) {
                continue;
            }
            $binary .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT);
        }

        $result = '';
        $bytes = str_split($binary, 8);
        foreach ($bytes as $byte) {
            if (strlen($byte) < 8) {
                break;
            }
            $result .= chr((int) bindec($byte));
        }

        return $result;
    }
}
