<?php
// customer/includes/webauthn.php
// ---------------------------------------------------------------------------
// Self-contained WebAuthn (passkey / Face ID / fingerprint) helpers.
//
// Supports the common platform-authenticator case: ES256 (P-256 ECDSA), which
// covers Face ID, Touch ID, Android biometrics and Windows Hello. Verification
// is done with PHP's openssl — no external library required.
//
// Tables (auto-created by wa_ensure_table):
//   webauthn_credentials(id, user_id, credential_id, public_key, sign_count, created_at)
// ---------------------------------------------------------------------------

if (!function_exists('wa_b64url_encode')) {

    function wa_b64url_encode(string $bin): string {
        return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
    }
    function wa_b64url_decode(string $txt): string {
        $txt = strtr($txt, '-_', '+/');
        $pad = strlen($txt) % 4;
        if ($pad) $txt .= str_repeat('=', 4 - $pad);
        return base64_decode($txt) ?: '';
    }

    function wa_ensure_table(mysqli $conn): void {
        @$conn->query(
            "CREATE TABLE IF NOT EXISTS `webauthn_credentials` (
                `id` INT NOT NULL AUTO_INCREMENT,
                `user_id` INT NOT NULL,
                `credential_id` VARCHAR(512) NOT NULL,
                `public_key` TEXT NOT NULL,
                `sign_count` BIGINT NOT NULL DEFAULT 0,
                `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (`id`),
                UNIQUE KEY `uniq_cred` (`credential_id`),
                KEY `idx_user` (`user_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
        );
    }

    /** The Relying Party ID = the registrable domain (host without port). */
    function wa_rp_id(): string {
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        return preg_replace('/:\d+$/', '', $host);
    }
    /** The expected origin, e.g. https://dabil.com.ng */
    function wa_origin(): string {
        $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? '') == 443;
        return ($https ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost');
    }

    // ── Minimal CBOR decoder (enough for WebAuthn attestation + COSE keys) ──
    function wa_cbor_len(string $d, int &$o, int $info): int {
        if ($info < 24) return $info;
        if ($info === 24) return ord($d[$o++]);
        if ($info === 25) { $v = unpack('n', substr($d, $o, 2))[1]; $o += 2; return $v; }
        if ($info === 26) { $v = unpack('N', substr($d, $o, 4))[1]; $o += 4; return $v; }
        if ($info === 27) { $h = unpack('N', substr($d, $o, 4))[1]; $l = unpack('N', substr($d, $o + 4, 4))[1]; $o += 8; return $h * 4294967296 + $l; }
        return 0;
    }
    function wa_cbor_decode(string $d, int &$o) {
        $first = ord($d[$o++]);
        $major = $first >> 5;
        $info  = $first & 0x1f;
        $len   = wa_cbor_len($d, $o, $info);
        switch ($major) {
            case 0: return $len;                 // unsigned int
            case 1: return -1 - $len;            // negative int
            case 2: case 3:                      // byte / text string
                $s = substr($d, $o, $len); $o += $len; return $s;
            case 4:                              // array
                $a = []; for ($i = 0; $i < $len; $i++) $a[] = wa_cbor_decode($d, $o); return $a;
            case 5:                              // map
                $m = []; for ($i = 0; $i < $len; $i++) { $k = wa_cbor_decode($d, $o); $m[$k] = wa_cbor_decode($d, $o); } return $m;
            case 7:
                if ($info === 20) return false;
                if ($info === 21) return true;
                if ($info === 22) return null;
                return $len;
        }
        return null;
    }

    /** Parse authenticatorData → [rpIdHash, flags, signCount, credId, coseKey(map)]. */
    function wa_parse_auth_data(string $ad): array {
        $rpIdHash  = substr($ad, 0, 32);
        $flags     = ord($ad[32]);
        $signCount = unpack('N', substr($ad, 33, 4))[1];
        $out = ['rpIdHash' => $rpIdHash, 'flags' => $flags, 'signCount' => $signCount, 'credId' => null, 'cose' => null];
        if ($flags & 0x40) { // attested credential data present
            $off = 37 + 16;  // skip aaguid
            $credLen = unpack('n', substr($ad, 37 + 16, 2))[1];
            $off += 2;
            $out['credId'] = substr($ad, $off, $credLen);
            $off += $credLen;
            $cborOff = $off;
            $out['cose'] = wa_cbor_decode($ad, $cborOff);
        }
        return $out;
    }

    /** Convert a COSE EC2 P-256 (ES256) key map to a PEM public key. */
    function wa_cose_to_pem(array $cose): ?string {
        // kty(1)=2 EC2, alg(3)=-7 ES256, crv(-1)=1 P-256, x(-2), y(-3)
        if (($cose[1] ?? null) != 2 || ($cose[3] ?? null) != -7) return null;
        $x = $cose[-2] ?? '';
        $y = $cose[-3] ?? '';
        if (strlen($x) !== 32 || strlen($y) !== 32) return null;
        $point  = "\x04" . $x . $y; // uncompressed point
        $der    = hex2bin('3059301306072a8648ce3d020106082a8648ce3d030107034200') . $point;
        $pem    = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END PUBLIC KEY-----\n";
        return $pem;
    }

    /** Verify the clientDataJSON challenge/type/origin. */
    function wa_check_client_data(string $clientDataJSON, string $expectedType, string $expectedChallengeB64): bool {
        $cd = json_decode($clientDataJSON, true);
        if (!is_array($cd)) return false;
        if (($cd['type'] ?? '') !== $expectedType) return false;
        if (!hash_equals($expectedChallengeB64, $cd['challenge'] ?? '')) return false;
        if (($cd['origin'] ?? '') !== wa_origin()) return false;
        return true;
    }

    /**
     * Verify an assertion signature.
     * signedData = authenticatorData || SHA-256(clientDataJSON)
     */
    function wa_verify_signature(string $pem, string $authData, string $clientDataJSON, string $signature): bool {
        $signed = $authData . hash('sha256', $clientDataJSON, true);
        $ok = openssl_verify($signed, $signature, $pem, OPENSSL_ALGO_SHA256);
        return $ok === 1;
    }
}
