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

    static showErrorMessage(errorMessage) {
        console.log("webauthn error: " + errorMessage);

        const alertElement = document.getElementById("webauthn-error-alert");
        if (alertElement) {
            alertElement.remove();
        }

        const alertTemplateElement = document.getElementById("webauthn-error");
        if (alertTemplateElement) {
            const clonedElement = alertTemplateElement.content.cloneNode(true);
            const errorMessageElement = clonedElement.getElementById("webauthn-error-message");
            if (errorMessageElement) {
                errorMessageElement.textContent = errorMessage;
            }
            alertTemplateElement.parentNode.insertBefore(clonedElement, alertTemplateElement);
        }
    }

    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();
    }
}
