/*
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
 *
 *  Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

'use strict'

class WebauthnTFA {

    _b64UrlSafeEncode = (str) => {
        const b64 = btoa(str);
        return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
    }

    // Decodes a Base64Url string
    _b64UrlSafeDecode = (input) => {
        input = input
            .replace(/-/g, '+')
            .replace(/_/g, '/');

        const pad = input.length % 4;
        if (pad) {
            if (pad === 1) {
                throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
            }
            input += new Array(5-pad).join('=');
        }

        return window.atob(input);
    };

    // Converts an array of bytes into a Base64Url string
    _arrayToBase64String = (a) => {
        const str = String.fromCharCode(...a);
        return this._b64UrlSafeEncode(str);
    }

    // Prepares the public key options object returned by the Webauthn Framework
    _preparePublicKeyOptions = publicKey => {
        //Convert challenge from Base64Url string to Uint8Array
        publicKey.challenge = Uint8Array.from(
            this._b64UrlSafeDecode(publicKey.challenge),
            c => c.charCodeAt(0)
        );

        //Convert the user ID from Base64 string to Uint8Array
        if (publicKey.user !== undefined) {
            publicKey.user = {
                ...publicKey.user,
                id: Uint8Array.from(
                    window.atob(publicKey.user.id),
                    c => c.charCodeAt(0)
                ),
            };
        }

        //If excludeCredentials is defined, we convert all IDs to Uint8Array
        if (publicKey.excludeCredentials !== undefined) {
            publicKey.excludeCredentials = publicKey.excludeCredentials.map(
                data => {
                    return {
                        ...data,
                        id: Uint8Array.from(
                            this._b64UrlSafeDecode(data.id),
                            c => c.charCodeAt(0)
                        ),
                    };
                }
            );
        }

        if (publicKey.allowCredentials !== undefined) {
            publicKey.allowCredentials = publicKey.allowCredentials.map(
                data => {
                    return {
                        ...data,
                        id: Uint8Array.from(
                            this._b64UrlSafeDecode(data.id),
                            c => c.charCodeAt(0)
                        ),
                    };
                }
            );
        }

        return publicKey;
    };

// Prepares the public key credentials object returned by the authenticator
    _preparePublicKeyCredentials = data => {
        const publicKeyCredential = {
            id: data.id,
            type: data.type,
            rawId: this._arrayToBase64String(new Uint8Array(data.rawId)),
            response: {
                clientDataJSON: this._arrayToBase64String(
                    new Uint8Array(data.response.clientDataJSON)
                ),
            },
        };

        if (data.response.attestationObject !== undefined) {
            publicKeyCredential.response.attestationObject = this._arrayToBase64String(
                new Uint8Array(data.response.attestationObject)
            );
        }

        if (data.response.authenticatorData !== undefined) {
            publicKeyCredential.response.authenticatorData = this._arrayToBase64String(
                new Uint8Array(data.response.authenticatorData)
            );
        }

        if (data.response.signature !== undefined) {
            publicKeyCredential.response.signature = this._arrayToBase64String(
                new Uint8Array(data.response.signature)
            );
        }

        if (data.response.userHandle !== undefined) {
            publicKeyCredential.response.userHandle = this._arrayToBase64String(
                new Uint8Array(data.response.userHandle)
            );
        }

        return publicKeyCredential;
    };


    constructor()
    {
        const register_dom_ready = (fn) => {
            document.addEventListener('turbo:load', fn)
        }

        register_dom_ready(() => {
            this.registerForms();
        });
    }

    registerForms()
    {
        //Find all forms which have an data-webauthn-tfa-action attribute
        const forms = document.querySelectorAll('form[data-webauthn-tfa-action]');

        forms.forEach((form) => {
            console.debug('Found webauthn TFA form with action: ' + form.getAttribute('data-webauthn-tfa-action'), form);
            //Ensure that the form has webauthn data

            const dataString = form.getAttribute('data-webauthn-tfa-data')
            const action = form.getAttribute('data-webauthn-tfa-action');

            if (!dataString) {
                console.error('Form does not have webauthn data, can not continue!', form);
                return;
            }

            //Convert dataString to the needed dataObject
            const dataObject = JSON.parse(dataString);
            const options = this._preparePublicKeyOptions(dataObject);


            if(action === 'authenticate'){
                this.authenticate(form, {publicKey: options});
            }

            if(action === 'register'){
                //Register submit action, so we can do the registration on submit
                form.addEventListener('submit', (e) => {
                    e.preventDefault();
                    this.register(form, {publicKey: options});
                });
            }
        });
    }

    /**
     * Submit the form with the given result data
     * @param form
     * @param data
     * @private
     */
    _submit(form, data)
    {
        const resultField = document.getElementById('_auth_code');
        resultField.value = JSON.stringify(data)
        form.submit();
    }

    authenticate(form, authData)
    {
        navigator.credentials.get(authData)
            .then((credential) => {
                //Convert our credential to a form which can be JSON encoded
                let data = this._preparePublicKeyCredentials(credential);

                this._submit(form, data)
            })
            .catch((error) => {
                console.error("WebAuthn Authentication error: ", error);
                alert("Error: " + error)
            });
    }

    register(form, authData)
    {
        navigator.credentials.create(authData)
            .then((credential) => {
                //Convert our credential to a form which can be JSON encoded
                let data = this._preparePublicKeyCredentials(credential);

                this._submit(form, data)
            })
            .catch((error) => {
                console.error("WebAuthn Registration error: ", error);
                alert("Error: " + error)
            });
    }
}

window.webauthnTFA = new WebauthnTFA();