"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.serialImportUsers = exports.validateUserJson = exports.validateOptions = exports.transArrayToUser = void 0;
const clc = require("colorette");
const apiv2_1 = require("./apiv2");
const api_1 = require("./api");
const logger_1 = require("./logger");
const error_1 = require("./error");
const utils = require("./utils");
const apiClient = new apiv2_1.Client({
    urlPrefix: api_1.googleOrigin,
});
const ALLOWED_JSON_KEYS = [
    "localId",
    "email",
    "emailVerified",
    "passwordHash",
    "salt",
    "displayName",
    "photoUrl",
    "createdAt",
    "lastSignedInAt",
    "providerUserInfo",
    "phoneNumber",
    "disabled",
    "customAttributes",
];
const ALLOWED_JSON_KEYS_RENAMING = {
    lastSignedInAt: "lastLoginAt",
};
const ALLOWED_PROVIDER_USER_INFO_KEYS = ["providerId", "rawId", "email", "displayName", "photoUrl"];
const ALLOWED_PROVIDER_IDS = ["google.com", "facebook.com", "twitter.com", "github.com"];
function isValidBase64(str) {
    const expected = Buffer.from(str, "base64").toString("base64");
    if (str.length < expected.length && !str.endsWith("=")) {
        str += "=".repeat(expected.length - str.length);
    }
    return expected === str;
}
function toWebSafeBase64(data) {
    return data.replace(/\//g, "_").replace(/\+/g, "-");
}
function addProviderUserInfo(user, providerId, arr) {
    if (arr[0]) {
        user.providerUserInfo.push({
            providerId: providerId,
            rawId: arr[0],
            email: arr[1],
            displayName: arr[2],
            photoUrl: arr[3],
        });
    }
}
function genUploadAccountPostBody(projectId, accounts, hashOptions) {
    const postBody = {
        users: accounts.map((account) => {
            if (account.passwordHash) {
                account.passwordHash = toWebSafeBase64(account.passwordHash);
            }
            if (account.salt) {
                account.salt = toWebSafeBase64(account.salt);
            }
            for (const [key, value] of Object.entries(ALLOWED_JSON_KEYS_RENAMING)) {
                if (account[key]) {
                    account[value] = account[key];
                    delete account[key];
                }
            }
            return account;
        }),
    };
    if (hashOptions.hashAlgo) {
        postBody.hashAlgorithm = hashOptions.hashAlgo;
    }
    if (hashOptions.hashKey) {
        postBody.signerKey = toWebSafeBase64(hashOptions.hashKey);
    }
    if (hashOptions.saltSeparator) {
        postBody.saltSeparator = toWebSafeBase64(hashOptions.saltSeparator);
    }
    if (hashOptions.rounds) {
        postBody.rounds = hashOptions.rounds;
    }
    if (hashOptions.memCost) {
        postBody.memoryCost = hashOptions.memCost;
    }
    if (hashOptions.cpuMemCost) {
        postBody.cpuMemCost = hashOptions.cpuMemCost;
    }
    if (hashOptions.parallelization) {
        postBody.parallelization = hashOptions.parallelization;
    }
    if (hashOptions.blockSize) {
        postBody.blockSize = hashOptions.blockSize;
    }
    if (hashOptions.dkLen) {
        postBody.dkLen = hashOptions.dkLen;
    }
    if (hashOptions.passwordHashOrder) {
        postBody.passwordHashOrder = hashOptions.passwordHashOrder;
    }
    postBody.targetProjectId = projectId;
    return postBody;
}
function transArrayToUser(arr) {
    const user = {
        localId: arr[0],
        email: arr[1],
        emailVerified: arr[2] === "true",
        passwordHash: arr[3],
        salt: arr[4],
        displayName: arr[5],
        photoUrl: arr[6],
        createdAt: arr[23],
        lastLoginAt: arr[24],
        phoneNumber: arr[25],
        providerUserInfo: [],
        disabled: arr[26],
        customAttributes: arr[27],
    };
    addProviderUserInfo(user, "google.com", arr.slice(7, 11));
    addProviderUserInfo(user, "facebook.com", arr.slice(11, 15));
    addProviderUserInfo(user, "twitter.com", arr.slice(15, 19));
    addProviderUserInfo(user, "github.com", arr.slice(19, 23));
    if (user.passwordHash && !isValidBase64(user.passwordHash)) {
        return {
            error: "Password hash should be base64 encoded.",
        };
    }
    if (user.salt && !isValidBase64(user.salt)) {
        return {
            error: "Password salt should be base64 encoded.",
        };
    }
    return user;
}
exports.transArrayToUser = transArrayToUser;
function validateOptions(options) {
    const hashOptions = validateRequiredParameters(options);
    if (!hashOptions.valid) {
        return hashOptions;
    }
    const hashInputOrder = options.hashInputOrder ? options.hashInputOrder.toUpperCase() : undefined;
    if (hashInputOrder) {
        if (hashInputOrder !== "SALT_FIRST" && hashInputOrder !== "PASSWORD_FIRST") {
            throw new error_1.FirebaseError("Unknown password hash order flag");
        }
        else {
            hashOptions["passwordHashOrder"] =
                hashInputOrder === "SALT_FIRST" ? "SALT_AND_PASSWORD" : "PASSWORD_AND_SALT";
        }
    }
    return hashOptions;
}
exports.validateOptions = validateOptions;
function validateRequiredParameters(options) {
    if (!options.hashAlgo) {
        utils.logWarning("No hash algorithm specified. Password users cannot be imported.");
        return { valid: true };
    }
    const hashAlgo = options.hashAlgo.toUpperCase();
    let roundsNum;
    switch (hashAlgo) {
        case "HMAC_SHA512":
        case "HMAC_SHA256":
        case "HMAC_SHA1":
        case "HMAC_MD5":
            if (!options.hashKey || options.hashKey === "") {
                throw new error_1.FirebaseError("Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo);
            }
            return { hashAlgo: hashAlgo, hashKey: options.hashKey, valid: true };
        case "MD5":
        case "SHA1":
        case "SHA256":
        case "SHA512":
            roundsNum = parseInt(options.rounds, 10);
            const minRounds = hashAlgo === "MD5" ? 0 : 1;
            if (isNaN(roundsNum) || roundsNum < minRounds || roundsNum > 8192) {
                throw new error_1.FirebaseError(`Must provide valid rounds(${minRounds}..8192) for hash algorithm ${options.hashAlgo}`);
            }
            return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true };
        case "PBKDF_SHA1":
        case "PBKDF2_SHA256":
            roundsNum = parseInt(options.rounds, 10);
            if (isNaN(roundsNum) || roundsNum < 0 || roundsNum > 120000) {
                throw new error_1.FirebaseError("Must provide valid rounds(0..120000) for hash algorithm " + options.hashAlgo);
            }
            return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true };
        case "SCRYPT":
            if (!options.hashKey || options.hashKey === "") {
                throw new error_1.FirebaseError("Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo);
            }
            roundsNum = parseInt(options.rounds, 10);
            if (isNaN(roundsNum) || roundsNum <= 0 || roundsNum > 8) {
                throw new error_1.FirebaseError("Must provide valid rounds(1..8) for hash algorithm " + options.hashAlgo);
            }
            const memCost = parseInt(options.memCost, 10);
            if (isNaN(memCost) || memCost <= 0 || memCost > 14) {
                throw new error_1.FirebaseError("Must provide valid memory cost(1..14) for hash algorithm " + options.hashAlgo);
            }
            let saltSeparator = "";
            if (options.saltSeparator) {
                saltSeparator = options.saltSeparator;
            }
            return {
                hashAlgo: hashAlgo,
                hashKey: options.hashKey,
                saltSeparator: saltSeparator,
                rounds: options.rounds,
                memCost: options.memCost,
                valid: true,
            };
        case "BCRYPT":
            return { hashAlgo: hashAlgo, valid: true };
        case "STANDARD_SCRYPT":
            const cpuMemCost = parseInt(options.memCost, 10);
            const parallelization = parseInt(options.parallelization, 10);
            const blockSize = parseInt(options.blockSize, 10);
            const dkLen = parseInt(options.dkLen, 10);
            return {
                hashAlgo: hashAlgo,
                valid: true,
                cpuMemCost: cpuMemCost,
                parallelization: parallelization,
                blockSize: blockSize,
                dkLen: dkLen,
            };
        default:
            throw new error_1.FirebaseError("Unsupported hash algorithm " + clc.bold(options.hashAlgo));
    }
}
function validateProviderUserInfo(providerUserInfo) {
    if (!ALLOWED_PROVIDER_IDS.includes(providerUserInfo.providerId)) {
        return {
            error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported providerId",
        };
    }
    const keydiff = Object.keys(providerUserInfo).filter((k) => !ALLOWED_PROVIDER_USER_INFO_KEYS.includes(k));
    if (keydiff.length) {
        return {
            error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported keys: " + keydiff.join(","),
        };
    }
    return {};
}
function validateUserJson(userJson) {
    const keydiff = Object.keys(userJson).filter((k) => !ALLOWED_JSON_KEYS.includes(k));
    if (keydiff.length) {
        return {
            error: JSON.stringify(userJson, null, 2) + " has unsupported keys: " + keydiff.join(","),
        };
    }
    if (userJson.providerUserInfo) {
        for (let i = 0; i < userJson.providerUserInfo.length; i++) {
            const res = validateProviderUserInfo(userJson.providerUserInfo[i]);
            if (res.error) {
                return res;
            }
        }
    }
    const badFormat = JSON.stringify(userJson, null, 2) + " has invalid data format: ";
    if (userJson.passwordHash && !isValidBase64(userJson.passwordHash)) {
        return {
            error: badFormat + "Password hash should be base64 encoded.",
        };
    }
    if (userJson.salt && !isValidBase64(userJson.salt)) {
        return {
            error: badFormat + "Password salt should be base64 encoded.",
        };
    }
    return {};
}
exports.validateUserJson = validateUserJson;
async function sendRequest(projectId, userList, hashOptions) {
    logger_1.logger.info("Starting importing " + userList.length + " account(s).");
    const postData = genUploadAccountPostBody(projectId, userList, hashOptions);
    return apiClient
        .post("/identitytoolkit/v3/relyingparty/uploadAccount", postData, {
        skipLog: { body: true },
    })
        .then((ret) => {
        if (ret.body.error) {
            logger_1.logger.info("Encountered problems while importing accounts. Details:");
            logger_1.logger.info(ret.body.error.map((rawInfo) => {
                return {
                    account: JSON.stringify(userList[parseInt(rawInfo.index, 10)], null, 2),
                    reason: rawInfo.message,
                };
            }));
        }
        else {
            utils.logSuccess("Imported successfully.");
        }
        logger_1.logger.info();
    });
}
function serialImportUsers(projectId, hashOptions, userListArr, index) {
    return sendRequest(projectId, userListArr[index], hashOptions).then(() => {
        if (index < userListArr.length - 1) {
            return serialImportUsers(projectId, hashOptions, userListArr, index + 1);
        }
    });
}
exports.serialImportUsers = serialImportUsers;
