<?php
// customer/includes/twofa.php
// ---------------------------------------------------------------------------
// Shared two-factor-authentication helpers used by login.php and
// two-factor-verify.php. Supports three verification methods:
//   • Emailed 6-digit login code (twofa_enabled)  — reuses the otp_codes table
//   • Authenticator app TOTP      (totp_enabled)   — RFC 6238, matches two_factor.php
//   • One-time backup codes       (backup_codes)   — password_hash()'d in users
// ---------------------------------------------------------------------------

if (!function_exists('twofa_status')) {

    /** Return a user's 2FA configuration. */
    function twofa_status(mysqli $conn, int $uid): array {
        $out = ['email' => false, 'totp' => false, 'totp_secret' => null, 'email_addr' => '', 'name' => ''];
        if ($q = $conn->prepare("SELECT email, name, twofa_enabled, totp_secret, totp_enabled FROM users WHERE id=? LIMIT 1")) {
            $q->bind_param("i", $uid);
            $q->execute();
            if ($r = $q->get_result()->fetch_assoc()) {
                $out['email']       = (bool)($r['twofa_enabled'] ?? 0);
                $out['totp']        = (bool)($r['totp_enabled'] ?? 0);
                $out['totp_secret'] = $r['totp_secret'] ?? null;
                $out['email_addr']  = (string)($r['email'] ?? '');
                $out['name']        = (string)($r['name'] ?? '');
            }
        }
        return $out;
    }

    /** Is 2FA active in any form for this user? */
    function twofa_required(array $status): bool {
        return !empty($status['email']) || (!empty($status['totp']) && !empty($status['totp_secret']));
    }

    /**
     * Generate, store and email a 6-digit login OTP (10-min expiry).
     * Honours a 45-second resend cooldown. Sets &$wait to the remaining
     * cooldown seconds when it declines to resend.
     */
    function twofa_send_login_otp(PDO $pdo, string $email, string $name, int &$wait = 0): bool {
        require_once __DIR__ . '/../mailer.php'; // load PHPMailer only when actually emailing
        $wait = 0;
        $st = $pdo->prepare("SELECT created_at FROM otp_codes WHERE email=:e AND purpose='login' ORDER BY created_at DESC LIMIT 1");
        $st->execute(['e' => $email]);
        if ($last = $st->fetch()) {
            $since = time() - strtotime($last['created_at']);
            if ($since < 45) { $wait = 45 - $since; return false; }
        }

        $code = (string) random_int(100000, 999999);
        $pdo->prepare("UPDATE otp_codes SET is_used=1 WHERE email=:e AND purpose='login' AND is_used=0")->execute(['e' => $email]);
        $exp = date('Y-m-d H:i:s', time() + 600);
        $pdo->prepare(
            "INSERT INTO otp_codes (email, otp_code, purpose, attempts, is_used, expires_at, created_at)
             VALUES (:e, :c, 'login', 0, 0, :x, NOW())"
        )->execute(['e' => $email, 'c' => $code, 'x' => $exp]);

        $res = send_mail($email, $name, 'Your login code — Dabil', otp_email_body($name, $code, 10));
        if (empty($res['success'])) {
            // roll back so the user can retry cleanly
            $pdo->prepare("DELETE FROM otp_codes WHERE email=:e AND otp_code=:c AND purpose='login'")->execute(['e' => $email, 'c' => $code]);
            return false;
        }
        return true;
    }

    /** Verify and consume an emailed login OTP. */
    function twofa_check_email_otp(PDO $pdo, string $email, string $code): bool {
        if (!preg_match('/^\d{6}$/', $code)) return false;
        $st = $pdo->prepare(
            "SELECT id, otp_code, attempts, expires_at, is_used FROM otp_codes
             WHERE email=:e AND purpose='login' ORDER BY created_at DESC LIMIT 1"
        );
        $st->execute(['e' => $email]);
        $row = $st->fetch();
        if (!$row || (int)$row['is_used'] === 1) return false;
        if (strtotime($row['expires_at']) < time()) return false;
        if ((int)$row['attempts'] >= 5) return false;
        if (!hash_equals((string)$row['otp_code'], $code)) {
            $pdo->prepare("UPDATE otp_codes SET attempts = attempts + 1 WHERE id=:id")->execute(['id' => $row['id']]);
            return false;
        }
        $pdo->prepare("UPDATE otp_codes SET is_used = 1 WHERE id=:id")->execute(['id' => $row['id']]);
        return true;
    }

    /** RFC 4648 base32 decode (uppercase A-Z2-7) — matches two_factor.php. */
    function twofa_base32_decode(string $b32): string {
        $map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $b32 = strtoupper($b32); $bits = ''; $out = '';
        for ($i = 0, $n = strlen($b32); $i < $n; $i++) {
            $v = strpos($map, $b32[$i]);
            if ($v === false) continue;
            $bits .= str_pad(decbin($v), 5, '0', STR_PAD_LEFT);
        }
        for ($i = 0; $i + 8 <= strlen($bits); $i += 8) $out .= chr(bindec(substr($bits, $i, 8)));
        return $out;
    }

    function twofa_hotp(string $secret, int $counter): int {
        $key = twofa_base32_decode($secret);
        $bin = pack('N*', 0) . pack('N*', $counter);
        $h   = hash_hmac('sha1', $bin, $key, true);
        $o   = ord($h[19]) & 0xf;
        $code = ((ord($h[$o]) & 0x7f) << 24) | ((ord($h[$o + 1]) & 0xff) << 16) | ((ord($h[$o + 2]) & 0xff) << 8) | (ord($h[$o + 3]) & 0xff);
        return $code % 1000000;
    }

    /** Verify an authenticator (TOTP) code, ±1 time-step window. */
    function twofa_check_totp(?string $secret, string $code): bool {
        if (!$secret) return false;
        $code = preg_replace('/\D/', '', $code);
        if (strlen($code) !== 6) return false;
        $t = (int) floor(time() / 30);
        for ($w = -1; $w <= 1; $w++) {
            if (str_pad((string) twofa_hotp($secret, $t + $w), 6, '0', STR_PAD_LEFT) === $code) return true;
        }
        return false;
    }

    /** Verify and consume a one-time backup code. */
    function twofa_check_backup(mysqli $conn, int $uid, string $code): bool {
        $code = strtoupper(trim($code));
        if ($code === '') return false;
        if (!($q = $conn->prepare("SELECT backup_codes FROM users WHERE id=? LIMIT 1"))) return false;
        $q->bind_param("i", $uid);
        $q->execute();
        $row    = $q->get_result()->fetch_assoc();
        $hashes = json_decode($row['backup_codes'] ?? '[]', true);
        if (!is_array($hashes)) return false;
        foreach ($hashes as $i => $h) {
            if (is_string($h) && password_verify($code, $h)) {
                unset($hashes[$i]);
                $json = json_encode(array_values($hashes));
                if ($u = $conn->prepare("UPDATE users SET backup_codes=? WHERE id=?")) {
                    $u->bind_param("si", $json, $uid);
                    $u->execute();
                }
                return true;
            }
        }
        return false;
    }
}
