<?php
namespace App\Core;

/*
 Simple TOTP (RFC 6238) sans dépendances externes.
 - Secret codé en Base32 (A-Z2-7)
*/
class TOTP
{
    public static function generateSecret(int $length = 16): string
    {
        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $secret = '';
        for ($i = 0; $i < $length; $i++) {
            $secret .= $alphabet[random_int(0, strlen($alphabet) - 1)];
        }
        return $secret;
    }

    public static function getCode(string $secret, int $timeStep = 30, int $digits = 6): string
    {
        $counter = floor(time() / $timeStep);
        $key = self::base32Decode($secret);
        $binCounter = pack('N*', 0) . pack('N*', $counter);
        $hash = hash_hmac('sha1', $binCounter, $key, true);
        $offset = ord($hash[19]) & 0xf;
        $value = (
            ((ord($hash[$offset]) & 0x7f) << 24) |
            ((ord($hash[$offset + 1]) & 0xff) << 16) |
            ((ord($hash[$offset + 2]) & 0xff) << 8) |
            (ord($hash[$offset + 3]) & 0xff)
        );
        $mod = 10 ** $digits;
        return str_pad((string)($value % $mod), $digits, '0', STR_PAD_LEFT);
    }

    public static function verify(string $secret, string $code, int $window = 1, int $timeStep = 30, int $digits = 6): bool
    {
        $currentCounter = floor(time() / $timeStep);
        for ($i = -$window; $i <= $window; $i++) {
            $candidate = self::codeAtCounter($secret, $currentCounter + $i, $digits);
            if (hash_equals($candidate, $code)) return true;
        }
        return false;
    }

    public static function otpauthUrl(string $label, string $secret, string $issuer = 'NOVALINK'): string
    {
        $labelEnc = rawurlencode($label);
        $issuerEnc = rawurlencode($issuer);
        return "otpauth://totp/{$issuerEnc}:{$labelEnc}?secret={$secret}&issuer={$issuerEnc}&digits=6&period=30";
    }

    private static function codeAtCounter(string $secret, int $counter, int $digits = 6): string
    {
        $key = self::base32Decode($secret);
        $binCounter = pack('N*', 0) . pack('N*', $counter);
        $hash = hash_hmac('sha1', $binCounter, $key, true);
        $offset = ord($hash[19]) & 0xf;
        $value = (
            ((ord($hash[$offset]) & 0x7f) << 24) |
            ((ord($hash[$offset + 1]) & 0xff) << 16) |
            ((ord($hash[$offset + 2]) & 0xff) << 8) |
            (ord($hash[$offset + 3]) & 0xff)
        );
        $mod = 10 ** $digits;
        return str_pad((string)($value % $mod), $digits, '0', STR_PAD_LEFT);
    }

    private static function base32Decode(string $b32): string
    {
        $b32 = strtoupper($b32);
        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $buffer = 0; $bitsLeft = 0; $result = '';
        for ($i = 0; $i < strlen($b32); $i++) {
            $val = strpos($alphabet, $b32[$i]);
            if ($val === false) continue;
            $buffer = ($buffer << 5) | $val;
            $bitsLeft += 5;
            if ($bitsLeft >= 8) {
                $bitsLeft -= 8;
                $result .= chr(($buffer >> $bitsLeft) & 0xFF);
            }
        }
        return $result;
    }
}
