<?php

if (!defined('ABSPATH')) {
    exit;
}

class BS2FA_TOTP {
    public static function generate_secret($length = 20) {
        return self::base32_encode(random_bytes($length));
    }

    public static function provisioning_uri($user, $secret) {
        $issuer = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
        $label  = rawurlencode($issuer . ':' . $user->user_login);
        $issuer_encoded = rawurlencode($issuer);
        return sprintf('otpauth://totp/%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30', $label, rawurlencode($secret), $issuer_encoded);
    }

    public static function verify($secret, $code, $window = 1) {
        $code = preg_replace('/\D+/', '', (string) $code);
        if (strlen($code) !== 6) {
            return false;
        }

        $time_slice = (int) floor(time() / 30);
        for ($i = -$window; $i <= $window; $i++) {
            $calculated = self::code($secret, $time_slice + $i);
            if (hash_equals($calculated, $code)) {
                return true;
            }
        }

        return false;
    }

    public static function code($secret, $time_slice = null) {
        if (null === $time_slice) {
            $time_slice = (int) floor(time() / 30);
        }

        $secret_key = self::base32_decode($secret);
        if ($secret_key === false) {
            return '';
        }

        $binary_time = pack('N*', 0) . pack('N*', $time_slice);
        $hash = hash_hmac('sha1', $binary_time, $secret_key, 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)
        );

        $otp = $truncated % 1000000;
        return str_pad((string) $otp, 6, '0', STR_PAD_LEFT);
    }

    public static function generate_backup_codes($count = 8) {
        $codes = [];
        for ($i = 0; $i < $count; $i++) {
            $codes[] = strtoupper(substr(BS2FA_Helpers::base64url_encode(random_bytes(9)), 0, 10));
        }
        return $codes;
    }

    public static function hash_backup_codes($codes) {
        $hashed = [];
        foreach ((array) $codes as $code) {
            $normalized = strtoupper(preg_replace('/[^A-Z0-9]/', '', $code));
            $hashed[] = wp_hash_password($normalized);
        }
        return $hashed;
    }

    public static function verify_backup_code($user_id, $code) {
        $normalized = strtoupper(preg_replace('/[^A-Z0-9]/', '', (string) $code));
        $stored = get_user_meta($user_id, 'bs2fa_backup_codes', true);

        if (!is_array($stored) || !$normalized) {
            return false;
        }

        foreach ($stored as $index => $hash) {
            if (wp_check_password($normalized, $hash)) {
                unset($stored[$index]);
                update_user_meta($user_id, 'bs2fa_backup_codes', array_values($stored));
                return true;
            }
        }

        return false;
    }

    public static function base32_encode($data) {
        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $binary = '';
        foreach (str_split($data) as $char) {
            $binary .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
        }

        $chunks = str_split($binary, 5);
        $output = '';
        foreach ($chunks as $chunk) {
            if (strlen($chunk) < 5) {
                $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
            }
            $output .= $alphabet[bindec($chunk)];
        }

        return $output;
    }

    public static function base32_decode($secret) {
        $secret = strtoupper(preg_replace('/[^A-Z2-7]/', '', (string) $secret));
        if ($secret === '') {
            return false;
        }

        $alphabet = array_flip(str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'));
        $binary = '';
        foreach (str_split($secret) as $char) {
            if (!isset($alphabet[$char])) {
                return false;
            }
            $binary .= str_pad(decbin($alphabet[$char]), 5, '0', STR_PAD_LEFT);
        }

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

        return $output;
    }
}
