"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.functionFromEndpoint = exports.endpointFromFunction = exports.listAllFunctions = exports.listFunctions = exports.deleteFunction = exports.updateFunction = exports.setInvokerUpdate = exports.setInvokerCreate = exports.getIamPolicy = exports.setIamPolicy = exports.createFunction = exports.generateUploadUrl = exports.API_VERSION = void 0;
const clc = require("colorette");
const error_1 = require("../error");
const logger_1 = require("../logger");
const backend = require("../deploy/functions/backend");
const utils = require("../utils");
const proto = require("./proto");
const runtimes = require("../deploy/functions/runtimes");
const projectConfig = require("../functions/projectConfig");
const apiv2_1 = require("../apiv2");
const api_1 = require("../api");
const constants_1 = require("../functions/constants");
exports.API_VERSION = "v1";
const client = new apiv2_1.Client({ urlPrefix: api_1.functionsOrigin, apiVersion: exports.API_VERSION });
function functionsOpLogReject(funcName, type, err) {
    var _a, _b, _c, _d;
    if (((_b = (_a = err === null || err === void 0 ? void 0 : err.context) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.statusCode) === 429) {
        utils.logWarning(`${clc.bold(clc.yellow("functions:"))} got "Quota Exceeded" error while trying to ${type} ${funcName}. Waiting to retry...`);
    }
    else {
        utils.logWarning(clc.bold(clc.yellow("functions:")) + " failed to " + type + " function " + funcName);
    }
    throw new error_1.FirebaseError(`Failed to ${type} function ${funcName}`, {
        original: err,
        status: (_d = (_c = err === null || err === void 0 ? void 0 : err.context) === null || _c === void 0 ? void 0 : _c.response) === null || _d === void 0 ? void 0 : _d.statusCode,
        context: { function: funcName },
    });
}
async function generateUploadUrl(projectId, location) {
    const parent = "projects/" + projectId + "/locations/" + location;
    const endpoint = `/${parent}/functions:generateUploadUrl`;
    try {
        const res = await client.post(endpoint, {}, { retryCodes: [503] });
        return res.body.uploadUrl;
    }
    catch (err) {
        logger_1.logger.info("\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support.");
        throw err;
    }
}
exports.generateUploadUrl = generateUploadUrl;
async function createFunction(cloudFunction) {
    const apiPath = cloudFunction.name.substring(0, cloudFunction.name.lastIndexOf("/"));
    const endpoint = `/${apiPath}`;
    cloudFunction.buildEnvironmentVariables = Object.assign(Object.assign({}, cloudFunction.buildEnvironmentVariables), { GOOGLE_NODE_RUN_SCRIPTS: "" });
    try {
        const res = await client.post(endpoint, cloudFunction);
        return {
            name: res.body.name,
            type: "create",
            done: false,
        };
    }
    catch (err) {
        throw functionsOpLogReject(cloudFunction.name, "create", err);
    }
}
exports.createFunction = createFunction;
async function setIamPolicy(options) {
    var _a, _b;
    const endpoint = `/${options.name}:setIamPolicy`;
    try {
        await client.post(endpoint, {
            policy: options.policy,
            updateMask: Object.keys(options.policy).join(","),
        });
    }
    catch (err) {
        throw new error_1.FirebaseError(`Failed to set the IAM Policy on the function ${options.name}`, {
            original: err,
            status: (_b = (_a = err === null || err === void 0 ? void 0 : err.context) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.statusCode,
        });
    }
}
exports.setIamPolicy = setIamPolicy;
async function getIamPolicy(fnName) {
    const endpoint = `/${fnName}:getIamPolicy`;
    try {
        const res = await client.get(endpoint);
        return res.body;
    }
    catch (err) {
        throw new error_1.FirebaseError(`Failed to get the IAM Policy on the function ${fnName}`, {
            original: err,
        });
    }
}
exports.getIamPolicy = getIamPolicy;
async function setInvokerCreate(projectId, fnName, invoker) {
    if (invoker.length === 0) {
        throw new error_1.FirebaseError("Invoker cannot be an empty array");
    }
    const invokerMembers = proto.getInvokerMembers(invoker, projectId);
    const invokerRole = "roles/cloudfunctions.invoker";
    const bindings = [{ role: invokerRole, members: invokerMembers }];
    const policy = {
        bindings: bindings,
        etag: "",
        version: 3,
    };
    await setIamPolicy({ name: fnName, policy: policy });
}
exports.setInvokerCreate = setInvokerCreate;
async function setInvokerUpdate(projectId, fnName, invoker) {
    var _a;
    if (invoker.length === 0) {
        throw new error_1.FirebaseError("Invoker cannot be an empty array");
    }
    const invokerMembers = proto.getInvokerMembers(invoker, projectId);
    const invokerRole = "roles/cloudfunctions.invoker";
    const currentPolicy = await getIamPolicy(fnName);
    const currentInvokerBinding = (_a = currentPolicy.bindings) === null || _a === void 0 ? void 0 : _a.find((binding) => binding.role === invokerRole);
    if (currentInvokerBinding &&
        JSON.stringify(currentInvokerBinding.members.sort()) === JSON.stringify(invokerMembers.sort())) {
        return;
    }
    const bindings = (currentPolicy.bindings || []).filter((binding) => binding.role !== invokerRole);
    bindings.push({
        role: invokerRole,
        members: invokerMembers,
    });
    const policy = {
        bindings: bindings,
        etag: currentPolicy.etag || "",
        version: 3,
    };
    await setIamPolicy({ name: fnName, policy: policy });
}
exports.setInvokerUpdate = setInvokerUpdate;
async function updateFunction(cloudFunction) {
    const endpoint = `/${cloudFunction.name}`;
    const fieldMasks = proto.fieldMasks(cloudFunction, "labels", "environmentVariables", "secretEnvironmentVariables");
    cloudFunction.buildEnvironmentVariables = Object.assign(Object.assign({}, cloudFunction.buildEnvironmentVariables), { GOOGLE_NODE_RUN_SCRIPTS: "" });
    fieldMasks.push("buildEnvironmentVariables");
    try {
        const res = await client.patch(endpoint, cloudFunction, {
            queryParams: {
                updateMask: fieldMasks.join(","),
            },
        });
        return {
            done: false,
            name: res.body.name,
            type: "update",
        };
    }
    catch (err) {
        throw functionsOpLogReject(cloudFunction.name, "update", err);
    }
}
exports.updateFunction = updateFunction;
async function deleteFunction(name) {
    const endpoint = `/${name}`;
    try {
        const res = await client.delete(endpoint);
        return {
            done: false,
            name: res.body.name,
            type: "delete",
        };
    }
    catch (err) {
        throw functionsOpLogReject(name, "delete", err);
    }
}
exports.deleteFunction = deleteFunction;
async function list(projectId, region) {
    const endpoint = "/projects/" + projectId + "/locations/" + region + "/functions";
    try {
        const res = await client.get(endpoint);
        if (res.body.unreachable && res.body.unreachable.length > 0) {
            logger_1.logger.debug(`[functions] unable to reach the following regions: ${res.body.unreachable.join(", ")}`);
        }
        return {
            functions: res.body.functions || [],
            unreachable: res.body.unreachable || [],
        };
    }
    catch (err) {
        logger_1.logger.debug(`[functions] failed to list functions for ${projectId}`);
        logger_1.logger.debug(`[functions] ${err === null || err === void 0 ? void 0 : err.message}`);
        throw new error_1.FirebaseError(`Failed to list functions for ${projectId}`, {
            original: err,
            status: err instanceof error_1.FirebaseError ? err.status : undefined,
        });
    }
}
async function listFunctions(projectId, region) {
    const res = await list(projectId, region);
    return res.functions;
}
exports.listFunctions = listFunctions;
async function listAllFunctions(projectId) {
    return list(projectId, "-");
}
exports.listAllFunctions = listAllFunctions;
function endpointFromFunction(gcfFunction) {
    var _a, _b, _c, _d, _e, _f, _g, _h;
    const [, project, , region, , id] = gcfFunction.name.split("/");
    let trigger;
    let uri;
    let securityLevel;
    if ((_a = gcfFunction.labels) === null || _a === void 0 ? void 0 : _a["deployment-scheduled"]) {
        trigger = {
            scheduleTrigger: {},
        };
    }
    else if ((_b = gcfFunction.labels) === null || _b === void 0 ? void 0 : _b["deployment-taskqueue"]) {
        trigger = {
            taskQueueTrigger: {},
        };
    }
    else if (((_c = gcfFunction.labels) === null || _c === void 0 ? void 0 : _c["deployment-callable"]) ||
        ((_d = gcfFunction.labels) === null || _d === void 0 ? void 0 : _d["deployment-callabled"])) {
        trigger = {
            callableTrigger: {},
        };
    }
    else if ((_e = gcfFunction.labels) === null || _e === void 0 ? void 0 : _e[constants_1.BLOCKING_LABEL]) {
        trigger = {
            blockingTrigger: {
                eventType: constants_1.BLOCKING_LABEL_KEY_TO_EVENT[gcfFunction.labels[constants_1.BLOCKING_LABEL]],
            },
        };
    }
    else if (gcfFunction.httpsTrigger) {
        trigger = { httpsTrigger: {} };
    }
    else {
        trigger = {
            eventTrigger: {
                eventType: gcfFunction.eventTrigger.eventType,
                eventFilters: { resource: gcfFunction.eventTrigger.resource },
                retry: !!((_f = gcfFunction.eventTrigger.failurePolicy) === null || _f === void 0 ? void 0 : _f.retry),
            },
        };
    }
    if (gcfFunction.httpsTrigger) {
        uri = gcfFunction.httpsTrigger.url;
        securityLevel = gcfFunction.httpsTrigger.securityLevel;
    }
    if (!runtimes.isValidRuntime(gcfFunction.runtime)) {
        logger_1.logger.debug("GCFv1 function has a deprecated runtime:", JSON.stringify(gcfFunction, null, 2));
    }
    const endpoint = Object.assign(Object.assign({ platform: "gcfv1", id,
        project,
        region }, trigger), { entryPoint: gcfFunction.entryPoint, runtime: gcfFunction.runtime });
    if (uri) {
        endpoint.uri = uri;
    }
    if (securityLevel) {
        endpoint.securityLevel = securityLevel;
    }
    proto.copyIfPresent(endpoint, gcfFunction, "minInstances", "maxInstances", "ingressSettings", "labels", "environmentVariables", "secretEnvironmentVariables", "sourceUploadUrl");
    proto.renameIfPresent(endpoint, gcfFunction, "serviceAccount", "serviceAccountEmail");
    proto.convertIfPresent(endpoint, gcfFunction, "availableMemoryMb", (raw) => raw);
    proto.convertIfPresent(endpoint, gcfFunction, "timeoutSeconds", "timeout", (dur) => dur === null ? null : proto.secondsFromDuration(dur));
    if (gcfFunction.vpcConnector) {
        endpoint.vpc = { connector: gcfFunction.vpcConnector };
        proto.convertIfPresent(endpoint.vpc, gcfFunction, "egressSettings", "vpcConnectorEgressSettings", (raw) => raw);
    }
    endpoint.codebase = ((_g = gcfFunction.labels) === null || _g === void 0 ? void 0 : _g[constants_1.CODEBASE_LABEL]) || projectConfig.DEFAULT_CODEBASE;
    if ((_h = gcfFunction.labels) === null || _h === void 0 ? void 0 : _h[constants_1.HASH_LABEL]) {
        endpoint.hash = gcfFunction.labels[constants_1.HASH_LABEL];
    }
    return endpoint;
}
exports.endpointFromFunction = endpointFromFunction;
function functionFromEndpoint(endpoint, sourceUploadUrl) {
    var _a, _b;
    if (endpoint.platform !== "gcfv1") {
        throw new error_1.FirebaseError("Trying to create a v1 CloudFunction with v2 API. This should never happen");
    }
    if (!runtimes.isValidRuntime(endpoint.runtime)) {
        throw new error_1.FirebaseError("Failed internal assertion. Trying to deploy a new function with a deprecated runtime." +
            " This should never happen");
    }
    const gcfFunction = {
        name: backend.functionName(endpoint),
        sourceUploadUrl: sourceUploadUrl,
        entryPoint: endpoint.entryPoint,
        runtime: endpoint.runtime,
        dockerRegistry: "ARTIFACT_REGISTRY",
    };
    if (typeof endpoint.labels !== "undefined") {
        gcfFunction.labels = Object.assign({}, endpoint.labels);
    }
    if (backend.isEventTriggered(endpoint)) {
        if (!((_a = endpoint.eventTrigger.eventFilters) === null || _a === void 0 ? void 0 : _a.resource)) {
            throw new error_1.FirebaseError("Cannot create v1 function from an eventTrigger without a resource");
        }
        gcfFunction.eventTrigger = {
            eventType: endpoint.eventTrigger.eventType,
            resource: endpoint.eventTrigger.eventFilters.resource,
        };
        gcfFunction.eventTrigger.failurePolicy = endpoint.eventTrigger.retry
            ? { retry: {} }
            : undefined;
    }
    else if (backend.isScheduleTriggered(endpoint)) {
        const id = backend.scheduleIdForFunction(endpoint);
        gcfFunction.eventTrigger = {
            eventType: "google.pubsub.topic.publish",
            resource: `projects/${endpoint.project}/topics/${id}`,
        };
        gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { "deployment-scheduled": "true" });
    }
    else if (backend.isTaskQueueTriggered(endpoint)) {
        gcfFunction.httpsTrigger = {};
        gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { "deployment-taskqueue": "true" });
    }
    else if (backend.isBlockingTriggered(endpoint)) {
        gcfFunction.httpsTrigger = {};
        gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [constants_1.BLOCKING_LABEL]: constants_1.BLOCKING_EVENT_TO_LABEL_KEY[endpoint.blockingTrigger.eventType] });
    }
    else {
        gcfFunction.httpsTrigger = {};
        if (backend.isCallableTriggered(endpoint)) {
            gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { "deployment-callable": "true" });
        }
        if (endpoint.securityLevel) {
            gcfFunction.httpsTrigger.securityLevel = endpoint.securityLevel;
        }
    }
    proto.copyIfPresent(gcfFunction, endpoint, "minInstances", "maxInstances", "ingressSettings", "environmentVariables", "secretEnvironmentVariables");
    proto.renameIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "serviceAccount");
    proto.convertIfPresent(gcfFunction, endpoint, "availableMemoryMb", (mem) => mem);
    proto.convertIfPresent(gcfFunction, endpoint, "timeout", "timeoutSeconds", (sec) => sec ? proto.durationFromSeconds(sec) : null);
    if (endpoint.vpc) {
        proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnector", "connector");
        proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnectorEgressSettings", "egressSettings");
    }
    else if (endpoint.vpc === null) {
        gcfFunction.vpcConnector = null;
        gcfFunction.vpcConnectorEgressSettings = null;
    }
    const codebase = endpoint.codebase || projectConfig.DEFAULT_CODEBASE;
    if (codebase !== projectConfig.DEFAULT_CODEBASE) {
        gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [constants_1.CODEBASE_LABEL]: codebase });
    }
    else {
        (_b = gcfFunction.labels) === null || _b === void 0 ? true : delete _b[constants_1.CODEBASE_LABEL];
    }
    if (endpoint.hash) {
        gcfFunction.labels = Object.assign(Object.assign({}, gcfFunction.labels), { [constants_1.HASH_LABEL]: endpoint.hash });
    }
    return gcfFunction;
}
exports.functionFromEndpoint = functionFromEndpoint;
