<?php
declare(strict_types=1);

function mocker_base32_decode(string $input): string
{
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $map = [];
    for ($i = 0; $i < strlen($alphabet); $i++) {
        $map[$alphabet[$i]] = $i;
    }
    $clean = strtoupper(preg_replace('/[^A-Z2-7]/', '', $input) ?? '');
    $buffer = 0;
    $bitsLeft = 0;
    $out = '';
    $len = strlen($clean);
    for ($i = 0; $i < $len; $i++) {
        $ch = $clean[$i];
        if (!isset($map[$ch])) {
            continue;
        }
        $buffer = ($buffer << 5) | $map[$ch];
        $bitsLeft += 5;
        while ($bitsLeft >= 8) {
            $bitsLeft -= 8;
            $out .= chr(($buffer >> $bitsLeft) & 0xFF);
        }
    }

    return $out;
}

function mocker_base32_encode(string $bytes): string
{
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $buffer = 0;
    $bitsLeft = 0;
    $out = '';
    $len = strlen($bytes);
    for ($i = 0; $i < $len; $i++) {
        $buffer = ($buffer << 8) | ord($bytes[$i]);
        $bitsLeft += 8;
        while ($bitsLeft >= 5) {
            $bitsLeft -= 5;
            $out .= $alphabet[($buffer >> $bitsLeft) & 31];
        }
    }
    if ($bitsLeft > 0) {
        $out .= $alphabet[($buffer << (5 - $bitsLeft)) & 31];
    }

    return $out;
}

function mocker_admin_twofa_generate_secret(int $bytes = 20): string
{
    $bytes = max(10, min(64, $bytes));

    return mocker_base32_encode(random_bytes($bytes));
}

function mocker_admin_twofa_totp(string $secret, ?int $time = null, int $period = 30, int $digits = 6): string
{
    $secretBytes = mocker_base32_decode($secret);
    if ($secretBytes === '') {
        return '';
    }
    $time = $time ?? time();
    $counter = intdiv(max(0, $time), $period);
    $binCounter = pack('N*', 0) . pack('N*', $counter);
    $hash = hash_hmac('sha1', $binCounter, $secretBytes, true);
    $offset = ord(substr($hash, -1)) & 0x0F;
    $truncated = (
        ((ord($hash[$offset]) & 0x7F) << 24)
        | ((ord($hash[$offset + 1]) & 0xFF) << 16)
        | ((ord($hash[$offset + 2]) & 0xFF) << 8)
        | (ord($hash[$offset + 3]) & 0xFF)
    );
    $code = $truncated % (10 ** $digits);

    return str_pad((string) $code, $digits, '0', STR_PAD_LEFT);
}

function mocker_admin_twofa_verify_code(string $secret, string $code, int $window = 1): bool
{
    $code = preg_replace('/\D+/', '', $code) ?? '';
    if (strlen($code) !== 6) {
        return false;
    }
    $now = time();
    for ($i = -$window; $i <= $window; $i++) {
        $candidate = mocker_admin_twofa_totp($secret, $now + ($i * 30));
        if ($candidate !== '' && hash_equals($candidate, $code)) {
            return true;
        }
    }

    return false;
}

function mocker_admin_twofa_make_otpauth_uri(string $issuer, string $accountName, string $secret): string
{
    $label = rawurlencode($issuer . ':' . $accountName);
    $params = http_build_query([
        'secret' => $secret,
        'issuer' => $issuer,
        'algorithm' => 'SHA1',
        'digits' => 6,
        'period' => 30,
    ], '', '&', PHP_QUERY_RFC3986);

    return "otpauth://totp/{$label}?{$params}";
}

function mocker_admin_twofa_generate_backup_codes(int $count = 8): array
{
    $count = max(4, min(20, $count));
    $codes = [];
    for ($i = 0; $i < $count; $i++) {
        $num = random_int(0, 99999999);
        $codes[] = substr(str_pad((string) $num, 8, '0', STR_PAD_LEFT), 0, 4) . '-' . substr(str_pad((string) $num, 8, '0', STR_PAD_LEFT), 4, 4);
    }

    return $codes;
}

function mocker_admin_twofa_hash_backup_codes(array $codes): array
{
    $out = [];
    foreach ($codes as $code) {
        $plain = strtoupper(trim((string) $code));
        if ($plain === '') {
            continue;
        }
        $out[] = password_hash($plain, PASSWORD_DEFAULT);
    }

    return $out;
}

function mocker_admin_twofa_consume_backup_code(array $hashes, string $code): array
{
    $normalized = strtoupper(trim($code));
    if ($normalized === '') {
        return [false, $hashes];
    }
    $next = [];
    $used = false;
    foreach ($hashes as $hash) {
        $hashStr = (string) $hash;
        if (!$used && $hashStr !== '' && password_verify($normalized, $hashStr)) {
            $used = true;
            continue;
        }
        $next[] = $hashStr;
    }

    return [$used, $next];
}
