"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadFirebaseEnvs = exports.loadUserEnvs = exports.checkForDuplicateKeys = exports.writeUserEnvs = exports.hasUserEnvs = exports.parseStrict = exports.validateKey = exports.KeyValidationError = exports.parse = void 0;
const clc = require("colorette");
const fs = require("fs");
const path = require("path");
const error_1 = require("../error");
const logger_1 = require("../logger");
const utils_1 = require("../utils");
const FUNCTIONS_EMULATOR_DOTENV = ".env.local";
const RESERVED_PREFIXES = ["X_GOOGLE_", "FIREBASE_", "EXT_"];
const RESERVED_KEYS = [
    "FIREBASE_CONFIG",
    "CLOUD_RUNTIME_CONFIG",
    "EVENTARC_CLOUD_EVENT_SOURCE",
    "ENTRY_POINT",
    "GCP_PROJECT",
    "GCLOUD_PROJECT",
    "GOOGLE_CLOUD_PROJECT",
    "FUNCTION_TRIGGER_TYPE",
    "FUNCTION_NAME",
    "FUNCTION_MEMORY_MB",
    "FUNCTION_TIMEOUT_SEC",
    "FUNCTION_IDENTITY",
    "FUNCTION_REGION",
    "FUNCTION_TARGET",
    "FUNCTION_SIGNATURE_TYPE",
    "K_SERVICE",
    "K_REVISION",
    "PORT",
    "K_CONFIGURATION",
];
const LINE_RE = new RegExp("^" +
    "\\s*" +
    "([\\w./]+)" +
    "\\s*=[\\f\\t\\v]*" +
    "(" +
    "\\s*'(?:\\\\'|[^'])*'|" +
    '\\s*"(?:\\\\"|[^"])*"|' +
    "[^#\\r\\n]*" +
    ")?" +
    "\\s*" +
    "(?:#[^\\n]*)?" +
    "$", "gms");
const ESCAPE_SEQUENCES_TO_CHARACTERS = {
    "\\n": "\n",
    "\\r": "\r",
    "\\t": "\t",
    "\\v": "\v",
    "\\\\": "\\",
    "\\'": "'",
    '\\"': '"',
};
const ALL_ESCAPE_SEQUENCES_RE = /\\[nrtv\\'"]/g;
const CHARACTERS_TO_ESCAPE_SEQUENCES = {
    "\n": "\\n",
    "\r": "\\r",
    "\t": "\\t",
    "\v": "\\v",
    "\\": "\\\\",
    "'": "\\'",
    '"': '\\"',
};
const ALL_ESCAPABLE_CHARACTERS_RE = /[\n\r\t\v\\'"]/g;
function parse(data) {
    const envs = {};
    const errors = [];
    data = data.replace(/\r\n?/, "\n");
    let match;
    while ((match = LINE_RE.exec(data))) {
        let [, k, v] = match;
        v = (v || "").trim();
        let quotesMatch;
        if ((quotesMatch = /^(["'])(.*)\1$/ms.exec(v)) != null) {
            v = quotesMatch[2];
            if (quotesMatch[1] === '"') {
                v = v.replace(ALL_ESCAPE_SEQUENCES_RE, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]);
            }
        }
        envs[k] = v;
    }
    const nonmatches = data.replace(LINE_RE, "");
    for (let line of nonmatches.split(/[\r\n]+/)) {
        line = line.trim();
        if (line.startsWith("#")) {
            continue;
        }
        if (line.length)
            errors.push(line);
    }
    return { envs, errors };
}
exports.parse = parse;
class KeyValidationError extends Error {
    constructor(key, message) {
        super(`Failed to validate key ${key}: ${message}`);
        this.key = key;
        this.message = message;
    }
}
exports.KeyValidationError = KeyValidationError;
function validateKey(key) {
    if (RESERVED_KEYS.includes(key)) {
        throw new KeyValidationError(key, `Key ${key} is reserved for internal use.`);
    }
    if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
        throw new KeyValidationError(key, `Key ${key} must start with an uppercase ASCII letter or underscore` +
            ", and then consist of uppercase ASCII letters, digits, and underscores.");
    }
    if (RESERVED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
        throw new KeyValidationError(key, `Key ${key} starts with a reserved prefix (${RESERVED_PREFIXES.join(" ")})`);
    }
}
exports.validateKey = validateKey;
function parseStrict(data) {
    const { envs, errors } = parse(data);
    if (errors.length) {
        throw new error_1.FirebaseError(`Invalid dotenv file, error on lines: ${errors.join(",")}`);
    }
    const validationErrors = [];
    for (const key of Object.keys(envs)) {
        try {
            validateKey(key);
        }
        catch (err) {
            logger_1.logger.debug(`Failed to validate key ${key}: ${err}`);
            if (err instanceof KeyValidationError) {
                validationErrors.push(err);
            }
            else {
                throw err;
            }
        }
    }
    if (validationErrors.length > 0) {
        throw new error_1.FirebaseError("Validation failed", { children: validationErrors });
    }
    return envs;
}
exports.parseStrict = parseStrict;
function findEnvfiles(functionsSource, projectId, projectAlias, isEmulator) {
    const files = [".env"];
    files.push(`.env.${projectId}`);
    if (projectAlias) {
        files.push(`.env.${projectAlias}`);
    }
    if (isEmulator) {
        files.push(FUNCTIONS_EMULATOR_DOTENV);
    }
    return files
        .map((f) => path.join(functionsSource, f))
        .filter(fs.existsSync)
        .map((p) => path.basename(p));
}
function hasUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
    return findEnvfiles(functionsSource, projectId, projectAlias, isEmulator).length > 0;
}
exports.hasUserEnvs = hasUserEnvs;
function writeUserEnvs(toWrite, envOpts) {
    if (Object.keys(toWrite).length === 0) {
        return;
    }
    const { functionsSource, projectId, projectAlias, isEmulator } = envOpts;
    const allEnvFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
    const targetEnvFile = envOpts.isEmulator
        ? FUNCTIONS_EMULATOR_DOTENV
        : `.env.${envOpts.projectId}`;
    const targetEnvFileExists = allEnvFiles.includes(targetEnvFile);
    if (!targetEnvFileExists) {
        fs.writeFileSync(path.join(envOpts.functionsSource, targetEnvFile), "", { flag: "wx" });
        (0, utils_1.logBullet)(clc.yellow(clc.bold("functions: ")) +
            `Created new local file ${targetEnvFile} to store param values. We suggest explicitly adding or excluding this file from version control.`);
    }
    const fullEnvs = loadUserEnvs(envOpts);
    const prodEnvs = isEmulator
        ? loadUserEnvs(Object.assign(Object.assign({}, envOpts), { isEmulator: false }))
        : loadUserEnvs(envOpts);
    checkForDuplicateKeys(isEmulator || false, Object.keys(toWrite), fullEnvs, prodEnvs);
    for (const k of Object.keys(toWrite)) {
        validateKey(k);
    }
    (0, utils_1.logBullet)(clc.cyan(clc.bold("functions: ")) + `Writing new parameter values to disk: ${targetEnvFile}`);
    let lines = "";
    for (const k of Object.keys(toWrite)) {
        lines += formatUserEnvForWrite(k, toWrite[k]);
    }
    fs.appendFileSync(path.join(functionsSource, targetEnvFile), lines);
}
exports.writeUserEnvs = writeUserEnvs;
function checkForDuplicateKeys(isEmulator, keys, fullEnv, envsWithoutLocal) {
    for (const key of keys) {
        const definedInEnv = fullEnv.hasOwnProperty(key);
        if (definedInEnv) {
            if (envsWithoutLocal && isEmulator && envsWithoutLocal.hasOwnProperty(key)) {
                (0, utils_1.logWarning)(clc.cyan(clc.yellow("functions: ")) +
                    `Writing parameter ${key} to emulator-specific config .env.local. This will overwrite your existing definition only when emulating.`);
                continue;
            }
            throw new error_1.FirebaseError(`Attempted to write param-defined key ${key} to .env files, but it was already defined.`);
        }
    }
}
exports.checkForDuplicateKeys = checkForDuplicateKeys;
function formatUserEnvForWrite(key, value) {
    const escapedValue = value.replace(ALL_ESCAPABLE_CHARACTERS_RE, (match) => CHARACTERS_TO_ESCAPE_SEQUENCES[match]);
    if (escapedValue !== value) {
        return `${key}="${escapedValue}"\n`;
    }
    return `${key}=${escapedValue}\n`;
}
function loadUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
    var _a;
    const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
    if (envFiles.length === 0) {
        return {};
    }
    if (projectAlias) {
        if (envFiles.includes(`.env.${projectId}`) && envFiles.includes(`.env.${projectAlias}`)) {
            throw new error_1.FirebaseError(`Can't have both dotenv files with projectId (env.${projectId}) ` +
                `and projectAlias (.env.${projectAlias}) as extensions.`);
        }
    }
    let envs = {};
    for (const f of envFiles) {
        try {
            const data = fs.readFileSync(path.join(functionsSource, f), "utf8");
            envs = Object.assign(Object.assign({}, envs), parseStrict(data));
        }
        catch (err) {
            throw new error_1.FirebaseError(`Failed to load environment variables from ${f}.`, {
                exit: 2,
                children: ((_a = err.children) === null || _a === void 0 ? void 0 : _a.length) > 0 ? err.children : [err],
            });
        }
    }
    (0, utils_1.logBullet)(clc.cyan(clc.bold("functions: ")) + `Loaded environment variables from ${envFiles.join(", ")}.`);
    return envs;
}
exports.loadUserEnvs = loadUserEnvs;
function loadFirebaseEnvs(firebaseConfig, projectId) {
    return {
        FIREBASE_CONFIG: JSON.stringify(firebaseConfig),
        GCLOUD_PROJECT: projectId,
    };
}
exports.loadFirebaseEnvs = loadFirebaseEnvs;