<?php

namespace classes;

use Exception;

class totp
{
    private string $base32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    /**
     * Generate a secret
     * @param int $length
     * @return string
     * @throws Exception
     */
    public function generateSecret(int $length = 16): string
    {
        $secret = '';
        for ($i = 0; $i < $length; $i++) {
            $secret .= $this->base32_chars[random_int(0, 31)];
        }
        return $secret;
    }

    /**
     * Get the TOTP code
     * @param string $secret
     * @param int|null $time
     * @return string
     * @throws Exception
     */
    public function getCode(string $secret, int $time = null): string
    {
        if ($time === null) {
            $time = floor(time() / 30);
        }

        $base32_secret = $this->base32Decode($secret);

        // Pack time into binary string
        $time_binary = pack('N*', 0) . pack('N*', $time);

        // HMAC-SHA1
        $hash = hash_hmac('sha1', $time_binary, $base32_secret, true);

        // Dynamic truncation
        $offset = ord($hash[19]) & 0xf;
        $otp = (
                ((ord($hash[$offset + 0]) & 0x7f) << 24) |
                ((ord($hash[$offset + 1]) & 0xff) << 16) |
                ((ord($hash[$offset + 2]) & 0xff) << 8) |
                (ord($hash[$offset + 3]) & 0xff)
            ) % 1000000;

        return str_pad((string)$otp, 6, '0', STR_PAD_LEFT);
    }

    /**
     * Verify the TOTP code
     * @param string $secret
     * @param string $code
     * @param int $discrepancy
     * @return bool
     * @throws Exception
     */
    public function verifyCode(string $secret, string $code, int $discrepancy = 1): bool
    {
        $current_time = floor(time() / 30);

        for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
            if ($this->getCode($secret, $current_time + $i) === $code) {
                return true;
            }
        }

        return false;
    }

    /**
     * Base32 decode
     * @param string $base32
     * @return string
     */
    private function base32Decode(string $base32): string
    {
        $base32 = strtoupper($base32);
        if (!preg_match('/^[A-Z2-7]+$/', $base32)) {
            return '';
        }

        $binary = '';
        foreach (str_split($base32) as $char) {
            $binary .= str_pad(decbin(strpos($this->base32_chars, $char)), 5, '0', STR_PAD_LEFT);
        }

        $binary_chunks = str_split($binary, 8);
        $result = '';
        foreach ($binary_chunks as $chunk) {
            if (strlen($chunk) === 8) {
                $result .= chr(bindec($chunk));
            }
        }

        return $result;
    }

    /**
     * Generate a QR code URL
     * @param string $secret
     * @param string $name
     * @param string $issuer
     * @return string
     */
    public function getQrCodeUrl(string $secret, string $name, string $issuer): string
    {
        return 'otpauth://totp/' . rawurlencode($issuer) . ':' . rawurlencode($name) . '?secret=' . $secret . '&issuer=' . rawurlencode($issuer);
    }
}
