/**
 * 
 * @file dfido2-lib.js
 * @description FIDO2 library of amipro FIDO2 Server
 * @version 2025-12-12
 * @author Amipro Co., Ltd. (https://www.amipro.me/)
 * @license Copyright (c) Amipro Co., Ltd. All rights reserved.
 */

const DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION = 'fido2_user_session'
const DFIDO2_LIB_LOCALSTG_NAME_REGISTERED = 'dfido2_lib_registered'
const DFIDO2_LIB_LOCALSTG_NAME_SVR_URL = 'dfido2_lib_svr_url'

let configuredServerUrl = null;

/** ===APIs=== */

if(!localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL)){
    localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, 'https://fido2.amipro.me')
}

function setFidoServerURL(url){
    if (!url || !url.startsWith('https://')) {
        throw new Error('serverUrl must be a valid HTTPS URL');
    }
    configuredServerUrl = url;
    localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, url);
}

function getServerUrl() {
    if (configuredServerUrl) {
        return configuredServerUrl;
    }
    const stored = localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL);
    if (stored) {
        configuredServerUrl = stored;
        return stored;
    }
    return 'https://fido2.amipro.me';
}

function canTryAutoAuthentication(){
    //const session_text = localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
    //alert('canTryAuth:'+session_text+"|"+(null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED)))
    return null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED)
}

/**
 * 
 * @param {String} userId
 * @param {String} rpId
 */
async function authenticateFido2(userId = null, rpId = null) {
    var result
    result = await doAssertion(userId, rpId);
    if(result.status === 'ok'){
        sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION, 
            JSON.stringify({session:result.session, uid:result.username}))
        localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date());
    }
    return result
}

/**
 * 
 * @param {String} userId
 * @param {String} rpId
 */
async function registerFido2(userId, userDisplay, rpId = null) {
    if (isWebAuthnSupported()) {
        const result = await doAttestation(userId, userDisplay, rpId);
        if(result.status === 'ok'){
            localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date());
            sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION, JSON.stringify({session:result.session, uid:result.username}))
        }
        return result
    }else return {status:'failed', errorMessage: getI18NErrorMessage('Fido2LibErr101:')}
}

/**
 * 
 * @param {String} rpId 
 * @returns 
 */
async function listUserDevicesFido2(rpId = null) {
    try {
        const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
        if(!session_text) return {status:'ok', devices:[]}

        const session_data = JSON.parse(session_text)

        let req = {session: session_data.session}
        if (rpId && 0 < rpId.length) {
            req.rp = { id: rpId };
        }

        const response = await fetch(getServerUrl() + "/usr/dvc/lst", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(req)
        });
        const resp = await response.json();
        if ('ok' === resp.status && resp.session === session_data.session) {
            return {status:'ok', devices:resp.devices}
        } else if (resp.errorMessage && resp.errorMessage.includes('No user session')) {
            sessionStorage.removeItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION);
            return {status:'ok', devices:[]}
        } else {
            return {status:'failed', errorMessage: resp.errorMessage}
        }
    } catch (err) {
        console.log(err)
        let msg = err.message ? err.message : err;
        //console.error("Assertion err: ", err);
        var errRtn =  {status:'failed', errorMessage: msg};
        if(err.name) errRtn.name = err.name
        return errRtn;
    }
}

async function delUserDeviceFido2(device_id, rpId = null) {
    try {
        const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
        const session_data = JSON.parse(session_text)

        let req = {session: session_data.session, device_id: device_id}
        if (rpId && 0 < rpId.length) {
            req.rp = { id: rpId };
        }

        const response = await fetch(getServerUrl() + "/usr/dvc/rm", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(req)
        });
        const resp = await response.json();

        return resp
    } catch (err) {
        console.log(err)
        let msg = err.message ? err.message : err;
        //console.error("Assertion err: ", err);
        var errRtn =  {status:'failed', errorMessage: msg};
        if(err.name) errRtn.name = err.name
        return errRtn;
    }
}

function getSessionId() {
    var rtn = null
    try {
        const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
        if(session_text){
            const session_data = JSON.parse(session_text)

            rtn = session_data.session
        }

        return rtn
    } catch (err) {
        console.log(err)
        return null;
    }
}

async function validSession(rpId = null) {
    try {
        const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
        if(!session_text) return false

        const session_data = JSON.parse(session_text)

        let req = {session: session_data.session}
        if (rpId && 0 < rpId.length) {
            req.rp = { id: rpId };
        }

        const response = await fetch(getServerUrl() + "/usr/validsession", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(req)
        });
        const resp = await response.json();

        return resp.status === 'ok'
    } catch (err) {
        console.log(err)
        return false;
    }
}

async function logoutFido2UserSession(){
    const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
    if(!session_text) return

    const session_data = JSON.parse(session_text)
    let req = {session: session_data['session'], username: session_data['uid']}
    const response = await fetch(getServerUrl() + "/usr/delsession", {
        method: "POST",
        cache: "no-cache",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(req)
    });

    sessionStorage.removeItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION);
}

async function getRegistrationUser(reg_session_id){
    try {
        let req = {session_id: reg_session_id}
        const response = await fetch(getServerUrl() + "/reg/username", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(req)
        });
        const resp = await response.json();

        return resp.username
    } catch (err) {
        console.log(err)
        return false;
    }
}

function errProcessFido2(result){
    alert(errMessageFido2(result));
}

function errMessageFido2(result){
    var rtn
    if(result.errCode && fido2LibErrCodes.unknown != result.errCode ){
        switch (result.errCode){
            case fido2LibErrCodes.user_canceled:
                rtn=getI18NErrorMessage('Fido2LibErr102:');
                break;
            case fido2LibErrCodes.timeout:
                rtn=getI18NErrorMessage('Fido2LibErr103:');
                break;
            case fido2LibErrCodes.invalid_state:
                rtn=getI18NErrorMessage('Fido2LibErr105:');
                break;
            case fido2LibErrCodes.not_allowed:
                rtn=getI18NErrorMessage('Fido2LibErr107:');
                break;
            case fido2LibErrCodes.abort:
                rtn=getI18NErrorMessage('Fido2LibErr108:');
                break;
            case fido2LibErrCodes.not_supported:
                rtn=getI18NErrorMessage('Fido2LibErr109:');
                break;
            case fido2LibErrCodes.security:
                rtn=getI18NErrorMessage('Fido2LibErr110:');
                break;
            case fido2LibErrCodes.network:
                rtn=getI18NErrorMessage('Fido2LibErr111:');
                break;
            case fido2LibErrCodes.constraint:
                rtn=getI18NErrorMessage('Fido2LibErr112:');
                break;
            case fido2LibErrCodes.not_readable:
                rtn=getI18NErrorMessage('Fido2LibErr113:');
                break;
            case fido2LibErrCodes.encoding:
                rtn=getI18NErrorMessage('Fido2LibErr114:');
                break;
            case fido2LibErrCodes.data_error:
                rtn=getI18NErrorMessage('Fido2LibErr115:');
                break;
            default:
                rtn=result.errorMessage?result.errorMessage:getI18NErrorMessage('Fido2LibErr104:');
        }
    }else if(result.name && "InvalidStateError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr105:');
    }else if(result.name && "NotAllowedError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr107:');
    }else if(result.name && "AbortError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr108:');
    }else if(result.name && "NotSupportedError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr109:');
    }else if(result.name && "SecurityError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr110:');
    }else if(result.name && "NetworkError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr111:');
    }else if(result.name && "ConstraintError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr112:');
    }else if(result.name && "NotReadableError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr113:');
    }else if(result.name && "EncodingError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr114:');
    }else if(result.name && "DataError" === result.name){
        rtn=getI18NErrorMessage('Fido2LibErr115:');
    }else if(result.errorMessage){
        const msg = getI18NErrorMessage(result.errorMessage);
        rtn=msg?msg:result.errorMessage;
    }else{
        rtn=getI18NErrorMessage('Fido2LibErr104:');
    }

    return rtn;
}

const fido2LibErrCodes = {
    user_canceled   : -101,
    timeout         : -102,
    unknown         : -999,
    invalid_state   : -103,
    not_allowed     : -104,
    abort           : -105,
    not_supported   : -106,
    security        : -107,
    network         : -108,
    constraint      : -109,
    not_readable    : -110,
    encoding        : -111,
    data_error      : -112
}

const errMsgs = new Map();
const fido2LibErrMsgLanguages = {
    english: 'en-US',
    japanese: 'ja',
    chinese_cn: 'zh-CN',
    //chinese_tw: 'zh-TW',
}
errMsgs.set(fido2LibErrMsgLanguages.english, new Map());
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr101:', 'Unregistered enterprise authenticator aaguid!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr102:', 'Unable to authenticate with a unique device binding key from another device!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr103:', 'Unable to verify signature!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr104:', 'Key not found!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr105:', 'Username does not exist!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr106:', 'Unique Device ID is null!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr107:', '/attestation/result request body has no ID field!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr108:', 'ID field is not Base64Url encoded in /attestation/result request body!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr109:', '/attestation/result request body has no TYPE field!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr110:', 'TYPE field is not a DOMString in /attestation/result request body!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr111:', 'The TYPE field is not a public key in the /attestation/result request body!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr112:', 'ID field is not a DOMString in /attestation/result request body!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr115:', 'authenticatorData not found!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr116:', 'authenticatorData is not base64 URL encoded!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr117:', 'Signature not found!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr118:', 'Signature is not base64 URL encoded!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr119:', 'No user session!');
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr120:', 'User has reached the device limit!');

errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr101:', 'Your browser does not support FIDO2/WebAuthn.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr102:', 'The operation was canceled by the user.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr103:', 'The operation timed out.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr104:', 'A system error occurred.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr105:', 'The authenticator contains credentials that are already registered with this website.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr106:', 'Another request is already in progress.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr107:', 'The operation was not allowed.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr108:', 'The operation was aborted.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr109:', 'This operation is not supported on your device.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr110:', 'A security error occurred.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr111:', 'A network error occurred.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr112:', 'The operation failed due to a constraint violation.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr113:', 'Could not read the credential.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr114:', 'The data format is invalid.');
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr115:', 'A data error occurred.');

errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr106:', 'Another request is already in progress.');

errMsgs.set(fido2LibErrMsgLanguages.japanese, new Map());
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr101:', '登録されていないエンタープライズ認証デバイス aaguid!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr102:', '別のデバイスからの一意のデバイス バインド キーで認証できません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr103:', '署名を認証できません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr104:', 'キーが見つかりません！');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr105:', 'ユーザー名は存在しません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr106:', '固有のデバイス ID が null です!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr107:', '/attestation/result request の本文に ID フィールドがありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr108:', 'ID フィールドは、/attestation/result リクエストの本文でエンコードされた Base64Url ではありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr109:', '/attestation/result リクエストのボディに TYPE フィールドがありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr110:', 'TYPE フィールドは、/attestation/result リクエストの本文の DOMString ではありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr111:', 'TYPE フィールドは、/attestation/result リクエストの本文の公開鍵ではありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr112:', 'ID フィールドは、/attestation/result リクエストの本文の DOMString ではありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr115:', 'authenticatorData が見つかりません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr116:', 'authenticatorData は base64 URL エンコードされていません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr117:', '署名が見つかりません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr118:', '署名は base64 URL エンコードされていません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr119:', 'ユーザーセッションがありません!');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr120:', 'ユーザーはデバイスの制限数に達しました!');

errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr101:', 'お使いのブラウザは FIDO2/WebAuthn をサポートしていません。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr102:', 'ユーザーによって操作がキャンセルされました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr103:', '操作がタイムアウトしました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr104:', 'システムエラーが発生しました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr105:', 'この認証デバイスには、すでにこの网站に登録されている憑拠が含まれています。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr106:', '別のリクエストがすでに進行中です。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr107:', '操作が許可されませんでした。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr108:', '操作が中止されました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr109:', 'この機器ではこの操作はサポートされていません。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr110:', 'セキュリティエラーが発生しました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr111:', 'ネットワークエラーが発生しました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr112:', '制約違反のため操作に失敗しました。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr113:', '憑拠を読み取れませんでした。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr114:', 'データ形式が無効です。');
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr115:', 'データエラーが発生しました。');

errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr106:', '別のリクエストがすでに進行中です。');

errMsgs.set(fido2LibErrMsgLanguages.chinese_cn, new Map());
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr101:', '未注册的企业认证器 aaguid!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr102:', '无法使用来自其他设备的唯一设备绑定密钥进行身份验证！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr103:', '无法验证签名！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr104:', '认证Key未找到!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr105:', '用户名不存在！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr106:', 'Unique Device ID 为 null!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr107:', '/attestation/result请求体没有ID字段！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr108:', 'ID字段不是/attestation/result请求体中编码的Base64Url！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr109:', '/attestation/result请求体没有TYPE字段！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr110:', '/attestation/result 请求正文中的 TYPE 字段不是 DOMString!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr111:', 'TYPE字段不是/attestation/result请求体中的公钥！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr112:', 'ID 字段不是 /attestation/result 请求体中的 DOMString！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr115:', 'authenticatorData 未找到!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr116:', 'authenticatorData 不是 base64 URL 编码！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr117:', '未找到签名！');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr118:', '签名不是 base64 URL 编码!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr119:', '未建立用户会话!');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr120:', '用户已达到设备限制数!');

errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr106:', '另一个请求正在进行中。');

errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr101:', '您的浏览器不支持FIDO2/WebAuthn。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr102:', '操作已被用户取消。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr103:', '操作超时。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr104:', '发生系统错误。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr105:', '您的认证器包含已在此网站注册的凭据。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr106:', '另一个请求正在进行中。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr107:', '操作不被允许。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr108:', '操作已中止。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr109:', '您的设备不支持此操作。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr110:', '发生安全错误。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr111:', '发生网络错误。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr112:', '由于约束冲突，操作失败。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr113:', '无法读取凭据。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr114:', '数据格式无效。');
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr115:', '发生数据错误。');

/**
 * 
 * @param {String} errorMessage 
 * @param {errMsgLanguages} language
 */
function getI18NErrorMessage(errorMessage, language = null){
    var lang = language ?  language : window.navigator.language
    var msgs = errMsgs.get(lang)
    if(!msgs)msgs = errMsgs.get(fido2LibErrMsgLanguages.english)    
    if(errorMessage){
        const msgHeader = 0<errorMessage.indexOf(':')?errorMessage.substring(0, errorMessage.indexOf(':')):errorMessage
        const msg = msgs.get(msgHeader+":")
        return msg?msgHeader+":"+msg:errorMessage;
    } else return errorMessage;
}

/** ===utils=== */

function isWebAuthnSupported() {
    if (window.PublicKeyCredential) {
        return true;
    } else {
        return false;
    }
}

function makePublicKey(attOptsResp) {
    if (attOptsResp.excludeCredentials) {
        attOptsResp.excludeCredentials = attOptsResp.excludeCredentials.map(
            function (cred) {
                cred.id = _base64ToArrayBuffer(_fromBase64URL(cred.id));
                cred.transports = ["internal", "usb", "ble", "nfc"];
                return cred;
            }
        );

        //console.log("Attestation Options:");
        //console.log(attOptsResp);
    }

    const keys = {
        publicKey: {
            attestation: attOptsResp.attestation,
            authenticatorSelection: attOptsResp.authenticatorSelection,
            excludeCredentials: attOptsResp.excludeCredentials,
            rp: attOptsResp.rp,
            user: {
                id: _stringToArrayBuffer(attOptsResp.user.id), //_base64ToArrayBuffer(_fromBase64URL(attOptsResp.user.id)),
                name: attOptsResp.user.name || attOptsResp.user.id || 'user',
                displayName: attOptsResp.user.displayName || attOptsResp.user.name || attOptsResp.user.id || 'User',
            },
            pubKeyCredParams: attOptsResp.pubKeyCredParams,
            timeout: attOptsResp.timeout,
            challenge: _base64ToArrayBuffer(_fromBase64URL(attOptsResp.challenge)),
        }
    }
    return keys;
}

async function doAttestation(username, displayName, rpId, userVerification = 'preferred') {
    var process_time_limit = Number.MAX_SAFE_INTEGER
    if (window._fido2_pending_request) {
        return {status:'failed', errorMessage: 'Fido2LibErr106:A request is already pending'};
    }
    window._fido2_pending_request = true;
    try {
        const attestationOptions = {
            username: username,
            displayName: encodeURIComponent(displayName),
            authenticatorSelection: {
                //authenticatorAttachment: "platform",
                userVerification: userVerification,
                requireResidentKey: false,
            },
            //attestation: "none",
        };

        if (rpId && 0 < rpId.length) {
            attestationOptions.rp = { id: rpId }
        }

        const svrUrl = getServerUrl()
        const response = await fetch(svrUrl + "/attestation/options", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(attestationOptions),
        });

        const resp = await response.json();
        if (resp.status === "failed") {
            return {status:'failed', errorMessage: resp.errorMessage}
        } else {
            process_time_limit = (new Date()).getTime() + resp.timeout;
            const res = await navigator.credentials.create(makePublicKey(resp));
            if (res) {
                let attResult = {
                    id: res.id,
                    rawId: _toBase64URL(btoa(_bufferToString(res.rawId)))
                    ,
                    type: "public-key",
                    response: {
                        clientDataJSON: _toBase64URL(btoa(_bufferToString(res.response.clientDataJSON)))
                        ,
                        attestationObject: _toBase64URL(btoa(_bufferToString(res.response.attestationObject)))
                        ,
                    },
                };

                // Capture transports if available (WebAuthn Level 2+)
                if (res.response.getTransports) {
                    attResult.transports = res.response.getTransports();
                }

                const result = await fetch(getServerUrl() + "/attestation/result", {
                    method: "POST",
                    cache: "no-cache",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify(attResult),
                });

                const respResult = await result.json();
                if (respResult) {
                    if (respResult.status === "ok") {
                        return respResult
                    } else {
                        return {status:'failed', errorMessage: respResult.errorMessage}
                    }
                } else {
                    return {status:'failed', errorMessage: 'Fido2LibErr999:Svr result error'}
                }
            } else {
                return {status:'failed', errorMessage: 'Fido2LibErr999:Undefined Result'};
            }
        }
    } catch (err) {
        var errRtn =  {status:'failed', errorMessage: err.message};
        if(err.name) errRtn.name = err.name

        if(err.name && 'NotAllowedError' === err.name){
            const nowtm = (new Date()).getTime()
            if(nowtm > process_time_limit){
                errRtn.errCode = fido2LibErrCodes.timeout
            }else{
                errRtn.errCode = fido2LibErrCodes.user_canceled
            }
        } else if(err.name && 'InvalidStateError' === err.name){
            errRtn.errCode = fido2LibErrCodes.invalid_state
        } else if(err.name && 'AbortError' === err.name){
            errRtn.errCode = fido2LibErrCodes.abort
        } else if(err.name && 'NotSupportedError' === err.name){
            errRtn.errCode = fido2LibErrCodes.not_supported
        } else if(err.name && 'SecurityError' === err.name){
            errRtn.errCode = fido2LibErrCodes.security
        } else if(err.name && 'NetworkError' === err.name){
            errRtn.errCode = fido2LibErrCodes.network
        } else if(err.name && 'ConstraintError' === err.name){
            errRtn.errCode = fido2LibErrCodes.constraint
        } else if(err.name && 'NotReadableError' === err.name){
            errRtn.errCode = fido2LibErrCodes.not_readable
        } else if(err.name && 'EncodingError' === err.name){
            errRtn.errCode = fido2LibErrCodes.encoding
        } else if(err.name && 'DataError' === err.name){
            errRtn.errCode = fido2LibErrCodes.data_error
        } else {
            errRtn.errCode = fido2LibErrCodes.unknown
        }

        return errRtn;
    } finally {
        window._fido2_pending_request = false;
    }
}

async function doAssertion(username = null, rpId = null, userVerification = 'preferred') {
    var process_time_limit = Number.MAX_SAFE_INTEGER
    if (window._fido2_pending_request) {
        return {status:'failed', errorMessage: 'Fido2LibErr106:A request is already pending'};
    }
    window._fido2_pending_request = true;
    try {
        let authnOptions;
        /*
        Tested the codes below, but most browsers cannot find correct account when username is null.
        if (!username || 0 === username.length) {
            authnOptions = {
                authenticatorSelection: {
                    //authenticatorAttachment: "platform",
                    userVerification: userVerification
                }
            };
        } else {
            authnOptions = {
                username: username,
                authenticatorSelection: {
                    //authenticatorAttachment: "platform",
                    userVerification: userVerification
                }
            };
        }*/
        authnOptions = {
            username: username,
            authenticatorSelection: {
                //authenticatorAttachment: "platform",
                userVerification: userVerification
            }
        };

        if (rpId && 0 < rpId.length) {
            authnOptions.rp = { id: rpId };
        }

        const response = await fetch(getServerUrl() + "/assertion/options", {
            method: "POST",
            cache: "no-cache",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(authnOptions)
        });
        const resp = await response.json();
        if ('ok' === resp.status) {
            process_time_limit = (new Date()).getTime() + resp.timeout;
            resp.allowCredentials = resp.allowCredentials || [];
            let mappedAllowCreds = resp.allowCredentials.map(x => {
                return {
                    id: _base64ToArrayBuffer(_fromBase64URL(x.id)),
                    type: x.type,
                    transports: x.transports // can set like ['internal', 'usb'] to override server settings
                };
            });

            const cred = await navigator.credentials.get({
                publicKey: {
                    challenge: _base64ToArrayBuffer(_fromBase64URL(resp.challenge)),
                    timeout: resp.timeout,
                    rpId: resp.rpId,
                    userVerification: resp.userVerification,
                    allowCredentials: mappedAllowCreds
                }
            });

            if (cred) {
                let authRequest = {
                    id: cred.id,
                    rawId: Array.from(new Uint8Array(cred.rawId)),
                    type: cred.type,
                    response: {
                        authenticatorData: _toBase64URL(btoa(_bufferToString(cred.response.authenticatorData))),
                        clientDataJSON: _toBase64URL(btoa(_bufferToString(cred.response.clientDataJSON))),
                        signature: _toBase64URL(btoa(_bufferToString(cred.response.signature))),
                        userHandle: _toBase64URL(btoa(_bufferToString(cred.response.userHandle))) //_toBase64URL(btoa(_bufferToString(cred.response.userHandle)))
                    }
                };
                const res = await fetch(getServerUrl() + "/assertion/result", {
                    method: "POST",
                    cache: "no-cache",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify(authRequest)
                });
                const result = await res.json();
                if (result.status === 'ok') {
                    return result
                } else {
                    return {status:'failed', errorMessage: result.errorMessage}
                }
            } else {
                return {status:'failed', errorMessage: 'Fido2LibErr999:Undefined Result'};
            }
        } else {
            return {status:'failed', errorMessage: resp.errorMessage}
        }
    } catch (err) {
        var errRtn =  {status:'failed', errorMessage: err.message};
        if(err.name) errRtn.name = err.name

        if(err.name && 'NotAllowedError' === err.name){
            const nowtm = (new Date()).getTime()
            if(nowtm > process_time_limit){
                errRtn.errCode = fido2LibErrCodes.timeout
            }else{
                errRtn.errCode = fido2LibErrCodes.user_canceled
            }
        } else if(err.name && 'InvalidStateError' === err.name){
            errRtn.errCode = fido2LibErrCodes.invalid_state
        } else if(err.name && 'AbortError' === err.name){
            errRtn.errCode = fido2LibErrCodes.abort
        } else if(err.name && 'NotSupportedError' === err.name){
            errRtn.errCode = fido2LibErrCodes.not_supported
        } else if(err.name && 'SecurityError' === err.name){
            errRtn.errCode = fido2LibErrCodes.security
        } else if(err.name && 'NetworkError' === err.name){
            errRtn.errCode = fido2LibErrCodes.network
        } else if(err.name && 'ConstraintError' === err.name){
            errRtn.errCode = fido2LibErrCodes.constraint
        } else if(err.name && 'NotReadableError' === err.name){
            errRtn.errCode = fido2LibErrCodes.not_readable
        } else if(err.name && 'EncodingError' === err.name){
            errRtn.errCode = fido2LibErrCodes.encoding
        } else if(err.name && 'DataError' === err.name){
            errRtn.errCode = fido2LibErrCodes.data_error
        } else {
            errRtn.errCode = fido2LibErrCodes.unknown
        }

        return errRtn;
    } finally {
        window._fido2_pending_request = false;
    }
}

function _toBase64URL(s) {
    return (s = (s = (s = s.split("=")[0]).replace(/\+/g, "-")).replace(/\//g, "_"));
}

function _base64ToArrayBuffer(base64) {
    var binary_string = window.atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes;
}

function _stringToArrayBuffer(src) {
    return (new Uint8Array([].map.call(src, function (c) {
        return c.charCodeAt(0)
    }))).buffer;
}

function _fromBase64URL(s) {
    var chk = (s = s.replace(/-/g, "+").replace(/_/g, "/")).length % 4;
    if (chk) {
        if (1 === chk) throw new Error("Base64url string is wrong.");
        s += new Array(5 - chk).join("=");
    }
    return s;
}

function _bufferToString(s) {
    return new Uint8Array(s).reduce((s, e) => s + String.fromCodePoint(e), "");
}
