<?php

namespace App\Support;

use App\Models\User;

class TwoFactorAuthenticator
{
    private const SECRET_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

    private const RECOVERY_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

    private const CODE_LENGTH = 6;

    private const PERIOD_SECONDS = 30;

    public function generateSecret(int $length = 32): string
    {
        $secret = '';
        $maxIndex = strlen(self::SECRET_ALPHABET) - 1;

        for ($i = 0; $i < $length; $i++) {
            $secret .= self::SECRET_ALPHABET[random_int(0, $maxIndex)];
        }

        return $secret;
    }

    /**
     * @return list<string>
     */
    public function generateRecoveryCodes(int $count = 8): array
    {
        $codes = [];

        for ($i = 0; $i < $count; $i++) {
            $codes[] = $this->randomRecoverySegment().'-'.$this->randomRecoverySegment();
        }

        return $codes;
    }

    public function verifyCode(string $secret, string $code, int $window = 1): bool
    {
        $normalized = strtoupper(str_replace([' ', '-'], '', trim($code)));
        if (! preg_match('/^\d{6}$/', $normalized)) {
            return false;
        }

        $currentSlice = intdiv(time(), self::PERIOD_SECONDS);

        for ($offset = -$window; $offset <= $window; $offset++) {
            $expected = $this->codeForTimeslice($secret, $currentSlice + $offset);

            if (hash_equals($expected, $normalized)) {
                return true;
            }
        }

        return false;
    }

    public function currentCode(string $secret): string
    {
        return $this->codeForTimeslice($secret, intdiv(time(), self::PERIOD_SECONDS));
    }

    public function otpauthUri(User $user, string $secret): string
    {
        $appName = config('app.name', 'CRM');
        $issuer = rawurlencode($appName);
        $label = rawurlencode($appName.':'.$user->email);

        return "otpauth://totp/{$label}?secret={$secret}&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";
    }

    public function qrCodeUrl(User $user, string $secret): string
    {
        $uri = $this->otpauthUri($user, $secret);

        return 'https://api.qrserver.com/v1/create-qr-code/?size=220x220&data='.rawurlencode($uri);
    }

    private function codeForTimeslice(string $secret, int $timeSlice): string
    {
        if ($timeSlice < 0) {
            return str_repeat('0', self::CODE_LENGTH);
        }

        $secretBytes = $this->base32Decode($secret);
        if ($secretBytes === '') {
            return str_repeat('0', self::CODE_LENGTH);
        }

        $counter = pack('N2', 0, $timeSlice);
        $hash = hash_hmac('sha1', $counter, $secretBytes, true);
        $offset = ord($hash[strlen($hash) - 1]) & 0x0f;
        $binary = substr($hash, $offset, 4);
        $unpacked = unpack('N', $binary);
        $value = ($unpacked[1] ?? 0) & 0x7fffffff;
        $modulo = 10 ** self::CODE_LENGTH;
        $code = (string) ($value % $modulo);

        return str_pad($code, self::CODE_LENGTH, '0', STR_PAD_LEFT);
    }

    private function base32Decode(string $secret): string
    {
        $clean = strtoupper(str_replace(['=', ' '], '', $secret));
        if ($clean === '') {
            return '';
        }

        $buffer = 0;
        $bufferBits = 0;
        $output = '';

        foreach (str_split($clean) as $char) {
            $index = strpos(self::SECRET_ALPHABET, $char);
            if ($index === false) {
                continue;
            }

            $buffer = ($buffer << 5) | $index;
            $bufferBits += 5;

            while ($bufferBits >= 8) {
                $bufferBits -= 8;
                $output .= chr(($buffer >> $bufferBits) & 0xff);
            }
        }

        return $output;
    }

    private function randomRecoverySegment(int $length = 4): string
    {
        $segment = '';
        $maxIndex = strlen(self::RECOVERY_ALPHABET) - 1;

        for ($i = 0; $i < $length; $i++) {
            $segment .= self::RECOVERY_ALPHABET[random_int(0, $maxIndex)];
        }

        return $segment;
    }
}

