class WebAuthnHandler {
    static isWebAuthnSupported() {
        return window.PublicKeyCredential;
    }

    static showErrorMessage(errorMessage) {
        console.log("webauthn error: " + errorMessage);
        const alertElement = document.getElementById("webauthn-error");
        if (alertElement) {
            alertElement.textContent += " (" + errorMessage + ")";
            alertElement.classList.remove("hidden");
        }
    }

    async isConditionalLoginSupported() {
        return WebAuthnHandler.isWebAuthnSupported() &&
            window.PublicKeyCredential.isConditionalMediationAvailable &&
            window.PublicKeyCredential.isConditionalMediationAvailable();
    }

    async conditionalLogin(abortController) {
        if (await this.isConditionalLoginSupported()) {
            this.login("", abortController);
        }
    }

    decodeBuffer(value) {
        return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
    }

    encodeBuffer(value) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=/g, "");
    }

    async post(urlKey, username, data) {
        let url = document.body.dataset[urlKey];
        if (username) {
            url += "?username=" + username;
        }

        return fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-Csrf-Token": getCsrfToken()
            },
            body: JSON.stringify(data),
        });
    }

    async get(urlKey, username) {
        let url = document.body.dataset[urlKey];
        if (username) {
            url += "?username=" + username;
        }
        return fetch(url);
    }

    async removeAllCredentials() {
        try {
            await this.post("webauthnDeleteAllUrl", null, {});
        } catch (err) {
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        window.location.reload();
    }

    async register() {
        let registerBeginResponse;
        try {
            registerBeginResponse = await this.get("webauthnRegisterBeginUrl");
        } catch (err) {
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        const credentialCreationOptions = await registerBeginResponse.json();
        credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge);
        credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
        if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) {
            credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id));
        }

        const attestation = await navigator.credentials.create(credentialCreationOptions);

        let registrationFinishResponse;
        try {
            registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
                id: attestation.id,
                rawId: this.encodeBuffer(attestation.rawId),
                type: attestation.type,
                response: {
                    attestationObject: this.encodeBuffer(attestation.response.attestationObject),
                    clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
                },
            });
        } catch (err) {
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        if (!registrationFinishResponse.ok) {
            throw new Error("Login failed with HTTP status code " + response.status);
        }

        const jsonData = await registrationFinishResponse.json();
        window.location.href = jsonData.redirect;
    }

    async login(username, abortController) {
        let loginBeginResponse;
        try {
            loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
        } catch (err) {
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        const credentialRequestOptions = await loginBeginResponse.json();
        credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);

        if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
            credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id));
        }

        if (abortController) {
            credentialRequestOptions.signal = abortController.signal;
            credentialRequestOptions.mediation = "conditional";
        }

        let assertion;
        try {
            assertion = await navigator.credentials.get(credentialRequestOptions);
        }
        catch (err) {
            // Swallow aborted conditional logins
            if (err instanceof DOMException && err.name == "AbortError") {
                return;
            }
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        if (!assertion) {
            return;
        }

        let loginFinishResponse;
        try {
            loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
                id: assertion.id,
                rawId: this.encodeBuffer(assertion.rawId),
                type: assertion.type,
                response: {
                    authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
                    clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
                    signature: this.encodeBuffer(assertion.response.signature),
                    userHandle: this.encodeBuffer(assertion.response.userHandle),
                },
            });
        } catch (err) {
            WebAuthnHandler.showErrorMessage(err);
            return;
        }

        if (!loginFinishResponse.ok) {
            throw new Error("Login failed with HTTP status code " + loginFinishResponse.status);
        }

        window.location.reload();
    }
}
