"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMissingBucketFiles = exports.deploymentComparator = exports.getStageDirectory = exports.getAddressDirectory = exports.translatePathsToDirectoryStructure = exports.FileSystemType = exports.generateBucketName = exports.BUCKET_NAME_REGEX = exports.MultiMessageError = exports.logAndReturnError = exports.isCloudFunction = exports.formatTerraformArguments = void 0;
const crypto_1 = require("crypto");
const isArray_1 = __importDefault(require("lodash/isArray"));
const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
const difference_1 = __importDefault(require("lodash/difference"));
const compare_versions_1 = require("compare-versions");
const logger = __importStar(require("./logger"));
const DEPLOYMENT_REQUIRED_FILE_NAMES = ['config.json', 'secrets.env', 'default.tfstate'];
function formatTerraformArguments(args) {
    return args
        .map((arg) => {
        if (!(0, isArray_1.default)(arg)) {
            return arg;
        }
        if (arg.length === 2) {
            return `${arg[0]}=${arg[1]}`;
        }
        return `${arg[0]}="${arg[1]}=${arg[2]}"`;
    })
        .map((arg) => `-${arg}`);
}
exports.formatTerraformArguments = formatTerraformArguments;
/**
 * Checks if the environment is a GCP or AWS cloud function
 */
const isCloudFunction = () => process.env.LAMBDA_TASK_ROOT || process.env.FUNCTION_TARGET;
exports.isCloudFunction = isCloudFunction;
const logAndReturnError = (message) => {
    logger.fail(message);
    return new Error(message);
};
exports.logAndReturnError = logAndReturnError;
class MultiMessageError extends Error {
    constructor(messages) {
        super(messages.join('\n'));
        this.messages = messages;
    }
}
exports.MultiMessageError = MultiMessageError;
exports.BUCKET_NAME_REGEX = /^airnode-[a-f0-9]{12}$/;
const generateBucketName = () => `airnode-${(0, crypto_1.randomBytes)(6).toString('hex')}`;
exports.generateBucketName = generateBucketName;
var FileSystemType;
(function (FileSystemType) {
    FileSystemType["Directory"] = "Directory";
    FileSystemType["File"] = "File";
})(FileSystemType || (exports.FileSystemType = FileSystemType = {}));
const translatePathsToDirectoryStructure = (paths) => {
    const directoryStructure = {};
    paths.forEach((path) => {
        // Split on each '/' except for the one on the end if there is such (for identifying directories).
        // E.g. for path '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace/dev/1662730904/' it's ['0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace', 'dev', '1662730904/']
        //
        // Reduce is used to traverse deeper and deeper through the directory structure, adding levels during the first pass (first path) and reusing them for the rest (paths with the common parts, path prefix).
        // The same `directoryStructure` variable is used as a starting point for each path so we end up with one fully populated directory structure.
        path.split(/\/(?!$)/).reduce((directoryStructureAtGivenDepth, name, index, splitParts) => {
            // Bucket key of an item is its path within the directory structure
            const bucketKey = splitParts.slice(0, index + 1).join('/');
            let sanitizedName = name;
            if (name.endsWith('/')) {
                sanitizedName = name.slice(0, -1);
            }
            // If there's not yet an item in the directory structure at given path we add it.
            // This is necessary as we potentially go through multiple items of the same path.
            // E.g. '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace/dev/1662730904/config.json' and '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace/dev/1662730904/secrets.env'
            if (!directoryStructureAtGivenDepth[sanitizedName]) {
                let newItem = { type: FileSystemType.Directory, bucketKey, children: {} };
                // If we're at the end of the path and the item name doesn't end with a '/' it's a file, not a directory.
                if (index === splitParts.length - 1 && !name.endsWith('/')) {
                    newItem = { type: FileSystemType.File, bucketKey };
                }
                // eslint-disable-next-line functional/immutable-data
                directoryStructureAtGivenDepth[sanitizedName] = newItem;
            }
            const currentItem = directoryStructureAtGivenDepth[sanitizedName];
            // If we're processing a directory within the path, we return a directory structure one level deeper.
            return currentItem.type === FileSystemType.Directory ? currentItem.children : {};
        }, directoryStructure);
    });
    return directoryStructure;
};
exports.translatePathsToDirectoryStructure = translatePathsToDirectoryStructure;
const getAddressDirectory = (directoryStructure, airnodeAddress) => {
    const addressDirectory = directoryStructure[airnodeAddress];
    if (!addressDirectory) {
        return null;
    }
    if (addressDirectory.type !== FileSystemType.Directory) {
        throw new Error(`Invalid directory structure, '${addressDirectory.bucketKey}' should be a directory`);
    }
    if (Object.keys(addressDirectory.children).length === 0) {
        throw new Error(`Invalid directory structure, '${addressDirectory.bucketKey}' should not be empty`);
    }
    return addressDirectory;
};
exports.getAddressDirectory = getAddressDirectory;
const getStageDirectory = (directoryStructure, airnodeAddress, stage) => {
    const addressDirectory = (0, exports.getAddressDirectory)(directoryStructure, airnodeAddress);
    if (!addressDirectory) {
        return null;
    }
    const stageDirectory = addressDirectory.children[stage];
    if (!stageDirectory) {
        return null;
    }
    if (stageDirectory.type !== FileSystemType.Directory) {
        throw new Error(`Invalid directory structure, '${stageDirectory.bucketKey}' should be a directory`);
    }
    if (Object.keys(stageDirectory.children).length === 0) {
        throw new Error(`Invalid directory structure, '${stageDirectory.bucketKey}' should not be empty`);
    }
    return stageDirectory;
};
exports.getStageDirectory = getStageDirectory;
const deploymentComparator = (a, b) => {
    const typeComparison = a.cloudProvider.type.localeCompare(b.cloudProvider.type);
    if (typeComparison !== 0)
        return typeComparison;
    // We will never get to compare two different cloud providers here, that's handled above
    const projectIdComparison = a.cloudProvider.type === 'gcp' && b.cloudProvider.type === 'gcp'
        ? a.cloudProvider.projectId.localeCompare(b.cloudProvider.projectId)
        : 0;
    if (projectIdComparison !== 0)
        return projectIdComparison;
    const regionComparison = a.cloudProvider.region.localeCompare(b.cloudProvider.region);
    if (regionComparison !== 0)
        return regionComparison;
    const addressComparison = a.airnodeAddress.localeCompare(b.airnodeAddress);
    if (addressComparison !== 0)
        return addressComparison;
    const stageComparison = a.stage.localeCompare(b.stage);
    if (stageComparison !== 0)
        return stageComparison;
    return (0, compare_versions_1.compareVersions)(a.airnodeVersion, b.airnodeVersion);
};
exports.deploymentComparator = deploymentComparator;
const getMissingBucketFiles = (directoryStructure) => Object.entries(directoryStructure).reduce((acc, [airnodeAddress, addressDirectory]) => {
    if (addressDirectory.type !== FileSystemType.Directory) {
        return acc;
    }
    const checkedAddressDirectory = Object.entries(addressDirectory.children).reduce((acc, [stage, stageDirectory]) => {
        if (stageDirectory.type !== FileSystemType.Directory) {
            return acc;
        }
        const latestDeployment = Object.keys(stageDirectory.children).sort().reverse()[0];
        const latestDepolymentFileNames = Object.keys(stageDirectory.children[latestDeployment]?.children || {});
        const missingRequiredFiles = (0, difference_1.default)(DEPLOYMENT_REQUIRED_FILE_NAMES, latestDepolymentFileNames);
        if ((0, isEmpty_1.default)(missingRequiredFiles)) {
            return { ...acc, [airnodeAddress]: { [stage]: [] } };
        }
        logger.warn(`Airnode '${airnodeAddress}' with stage '${stage}' is missing files: ${missingRequiredFiles.join(', ')}. Deployer commands may fail and manual removal may be necessary.`);
        return { ...acc, [airnodeAddress]: { [stage]: missingRequiredFiles } };
    }, {});
    return { ...acc, ...checkedAddressDirectory };
}, {});
exports.getMissingBucketFiles = getMissingBucketFiles;
//# sourceMappingURL=infrastructure.js.map