"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const path_1 = tslib_1.__importDefault(require("path"));
const fs_1 = tslib_1.__importDefault(require("fs"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const pify_1 = tslib_1.__importDefault(require("pify"));
const shelljs_1 = tslib_1.__importDefault(require("shelljs"));
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const aws_sdk_1 = tslib_1.__importDefault(require("aws-sdk"));
const js_yaml_1 = tslib_1.__importDefault(require("js-yaml"));
const listr_1 = tslib_1.__importDefault(require("listr"));
const models_pack_1 = tslib_1.__importDefault(require("@tradle/models-pack"));
const errors_1 = tslib_1.__importDefault(require("@tradle/errors"));
const amazon_s3_uri_1 = tslib_1.__importDefault(require("amazon-s3-uri"));
exports.parseS3Url = amazon_s3_uri_1.default;
const empty_bucket_1 = require("./empty-bucket");
const logger_1 = require("./logger");
const errors_2 = require("./errors");
const constants_1 = require("./constants");
exports.MY_CLOUD_STACK_NAME_REGEX = /^tdl-([a-zA-Z0-9-]*?)-ltd-([a-z]+)$/;
exports.MY_CLOUD_STACK_NAME_STRICTER_REGEX = /^tdl-([a-z0-9]*?)-ltd-([a-z]+)$/;
const NOT_FOUND_ERRORS = [
    { code: 'NotFound' },
    { code: 'NoSuchBucket' },
    { code: 'NoSuchKey' },
    { code: 'NotFoundException' },
    { code: 'ResourceNotFoundException' },
];
const ignoreNotFound = err => errors_1.default.ignore(err, NOT_FOUND_ERRORS);
exports.get = (url) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const res = yield node_fetch_1.default(url);
    if (res.statusCode > 300) {
        throw new Error(res.statusText);
    }
    return yield res.json();
});
exports.getNamespace = ({ models, lenses }) => {
    const model = lodash_1.default.values(models)[0] || lodash_1.default.values(lenses)[0];
    return model && models_pack_1.default.getNamespace(model.id);
};
exports.pack = ({ namespace, models, lenses }) => {
    if (!(lodash_1.default.size(models) || lodash_1.default.size(lenses))) {
        throw new errors_2.Errors.InvalidInput('expected "models" and/or "lenses"');
    }
    if (!namespace) {
        namespace = exports.getNamespace({ models, lenses });
    }
    const modelsPack = models_pack_1.default.pack({ namespace, models, lenses });
    return modelsPack;
};
exports.prettify = obj => {
    return typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
};
exports.isValidProjectPath = project => {
    return fs_1.default.existsSync(path_1.default.resolve(project, 'serverless.yml'));
};
exports.toEnvFile = obj => Object.keys(obj)
    .map(key => `${key}="${obj[key]}"`)
    .join('\n');
const acceptAll = (item) => true;
const isRetained = ({ DeletionPolicy }) => DeletionPolicy === 'Retain';
const getRetainedResources = (template) => Object.keys(template.Resources)
    .filter(logicalId => isRetained(template.Resources[logicalId]));
exports.getStackTemplates = (cloudformation, stackId) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const main = yield exports.getStackTemplate({ cloudformation, stackId });
    const nested = yield exports.listSubstacks(cloudformation, stackId);
    if (!nested.length)
        return [main];
    const nestedTemplates = lodash_1.default.flatten(yield Promise.all(nested.map(n => exports.getStackTemplates(cloudformation, n.PhysicalResourceId))));
    return [main].concat(nestedTemplates);
});
exports.isV1Stack = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const childStacks = yield exports.listSubstacks(cloudformation, stackId);
    return childStacks.length === 0;
});
exports.isV2Template = (template) => {
    return lodash_1.default.some(template.Resources, (value) => value.Type === 'AWS::CloudFormation::Stack');
};
exports.getStackInfo = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Stacks, } = yield cloudformation.describeStacks({ StackName: stackId }).promise();
    return Stacks[0];
});
exports.getStackOutputs = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Outputs } = yield exports.getStackInfo({ cloudformation, stackId });
    return Outputs;
});
exports.getMyCloudStackTables = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const outputs = yield exports.getStackOutputs({ cloudformation, stackId });
    return outputs.filter(o => o.OutputKey.endsWith('Table')).map(o => o.OutputValue);
});
exports.getStackParameters = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Parameters } = yield exports.getStackInfo(opts);
    return Parameters;
});
exports.mergeParameters = (base, more) => {
    const map = {};
    const set = (p) => {
        if (p.ParameterValue !== '') {
            map[p.ParameterKey] = p.ParameterValue;
        }
    };
    base.forEach(p => map[p.ParameterKey] = p.ParameterValue);
    more.forEach(p => map[p.ParameterKey] = p.ParameterValue);
    return lodash_1.default.transform(map, (result, ParameterValue, ParameterKey) => {
        result.push({ ParameterKey, ParameterValue });
    }, []);
};
exports.listOutputResources = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const outputs = yield exports.getStackOutputs({ cloudformation, stackId });
    return outputs.map(({ OutputKey, OutputValue }) => {
        const match = OutputKey.match(/(Bucket|Table|Key|LogGroup|RestApi)$/);
        if (match) {
            const type = match[1].toLowerCase();
            return { type, name: OutputKey, value: OutputValue };
        }
    })
        .filter(lodash_1.default.identity);
});
exports.listStackResources = (cloudformation, StackName, filter = acceptAll) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    let resources = [];
    const opts = { StackName };
    while (true) {
        let { StackResourceSummaries, NextToken } = yield cloudformation.listStackResources(opts).promise();
        resources = resources.concat(StackResourceSummaries.filter(filter));
        opts.NextToken = NextToken;
        if (!opts.NextToken)
            break;
    }
    return resources;
});
exports.listStackResourcesByType = (type, cloudformation, stackName) => {
    return exports.listStackResources(cloudformation, stackName, ({ ResourceType }) => type === ResourceType);
};
exports.listStackResourceIdsByType = (type, cloudformation, stackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const resources = yield exports.listStackResourcesByType(type, cloudformation, stackName);
    return resources.map(r => r.PhysicalResourceId);
});
exports.listStackBuckets = (cloudformation, stackName) => exports.listStackResourcesByType('AWS::S3::Bucket', cloudformation, stackName);
exports.listStackBucketIds = (cloudformation, stackName) => exports.listStackResourceIdsByType('AWS::S3::Bucket', cloudformation, stackName);
exports.listStackFunctions = (cloudformation, stackName) => exports.listStackResourcesByType('AWS::Lambda::Function', cloudformation, stackName);
exports.listStackFunctionIds = (cloudformation, stackName) => exports.listStackResourceIdsByType('AWS::Lambda::Function', cloudformation, stackName);
exports.listStackTables = (cloudformation, stackName) => exports.listStackResourcesByType('AWS::DynamoDB::Table', cloudformation, stackName);
exports.listStackTableIds = (cloudformation, stackName) => exports.listStackResourceIdsByType('AWS::DynamoDB::Table', cloudformation, stackName);
exports.listSubstacks = (cloudformation, stackName) => exports.listStackResourcesByType('AWS::CloudFormation::Stack', cloudformation, stackName);
exports.listSubstackIds = (cloudformation, stackName) => exports.listStackResourceIdsByType('AWS::CloudFormation::Stack', cloudformation, stackName);
exports.destroyBucket = (s3, Bucket) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    yield empty_bucket_1.emptyBucket(s3, Bucket);
    try {
        yield s3.deleteBucket({ Bucket }).promise();
    }
    catch (err) {
        ignoreNotFound(err);
    }
});
exports.markBucketForDeletion = (s3, bucket) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield exports.setBucketExpirationDays({ s3, bucket, days: 1 });
    }
    catch (err) {
        ignoreNotFound(err);
        throw new errors_2.Errors.NotFound(`bucket: ${bucket}`);
    }
});
exports.deleteTable = ({ dynamodb, tableName }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield dynamodb.deleteTable({ TableName: tableName }).promise();
    }
    catch (err) {
        ignoreNotFound(err);
        throw new errors_2.Errors.NotFound(`table not found: ${tableName}`);
    }
});
exports.deleteKey = ({ kms, keyId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield kms.disableKey({ KeyId: keyId }).promise();
    }
    catch (err) {
        errors_1.default.ignore(err, /pending deletion/);
        throw new errors_2.Errors.NotFound(`key not found with id: ${keyId}`);
    }
    try {
        yield kms.scheduleKeyDeletion({ KeyId: keyId }).promise();
    }
    catch (err) {
        errors_1.default.ignore(err, /pending deletion/);
        throw new errors_2.Errors.NotFound(`key not found with id: ${keyId}`);
    }
});
exports.deleteLogGroup = ({ logs, name }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield logs.deleteLogGroup({ logGroupName: name }).promise();
    }
    catch (err) {
        ignoreNotFound(err);
        throw new errors_2.Errors.NotFound(`log group with name: ${name}`);
    }
});
exports.deleteRestApi = ({ apigateway, apiId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield apigateway.deleteRestApi({ restApiId: apiId }).promise();
    }
    catch (err) {
        ignoreNotFound(err);
    }
});
exports.getNotDeletedStack = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Stacks } = yield cloudformation.describeStacks({ StackName: stackId }).promise();
    const notDeleted = Stacks.filter(s => s.StackStatus !== 'DELETE_COMPLETE');
    if (!notDeleted.length) {
        throw new errors_2.Errors.NotFound(`stack not found: ${stackId}`);
    }
    return notDeleted[0];
});
exports.assertStackExistsAndIsNotDeleted = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    yield exports.getNotDeletedStack(opts);
});
exports.disableStackTerminationProtection = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    yield exports.assertStackExistsAndIsNotDeleted({ cloudformation, stackId });
    return yield cloudformation.updateTerminationProtection({
        StackName: stackId,
        EnableTerminationProtection: false,
    }).promise();
});
exports.deleteStack = ({ cloudformation, params }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { StackName } = params;
    yield exports.assertStackExistsAndIsNotDeleted({ cloudformation, stackId: params.StackName });
    while (true) {
        try {
            yield cloudformation.deleteStack(params).promise();
            break;
        }
        catch (err) {
            if (!err.message.includes('UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS')) {
                throw err;
            }
            yield exports.wait(5000);
        }
    }
    return () => tslib_1.__awaiter(this, void 0, void 0, function* () {
        yield exports.wait(5000);
        yield exports.awaitStackDelete(cloudformation, params.StackName);
    });
});
exports.createStackInRegion = ({ region, params }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return exports.createStack({
        cloudformation: new aws_sdk_1.default.CloudFormation({ region }),
        params,
    });
});
exports.updateStackInRegion = ({ region, params }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return exports.updateStack({
        cloudformation: new aws_sdk_1.default.CloudFormation({ region }),
        params,
    });
});
exports.createStack = ({ cloudformation, params }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { StackId } = yield cloudformation.createStack(params).promise();
    return () => tslib_1.__awaiter(this, void 0, void 0, function* () {
        yield exports.wait(5000);
        yield exports.awaitStackCreate(cloudformation, params.StackName);
        return StackId;
    });
});
exports.updateStack = ({ cloudformation, params }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { StackId } = yield cloudformation.updateStack(params).promise();
    return () => tslib_1.__awaiter(this, void 0, void 0, function* () {
        yield exports.wait(5000);
        yield exports.awaitStackUpdate(cloudformation, params.StackName);
        return StackId;
    });
});
exports.updateStackAndWait = (opts) => exports.updateStack(opts).then(wait => wait());
exports.updateStackInRegionAndWait = (opts) => exports.updateStackInRegion(opts).then(wait => wait());
exports.createStackAndWait = (opts) => exports.createStack(opts).then(wait => wait());
exports.createStackInRegionAndWait = (opts) => exports.createStackInRegion(opts).then(wait => wait());
exports.deleteStackAndWait = (opts) => exports.deleteStack(opts).then(wait => wait());
const isUpdateableStatus = (status) => {
    return status.endsWith('_COMPLETE') && !status.startsWith('DELETE_');
};
exports.getStackId = (cloudformation, stackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const stacks = yield exports.listStacks(cloudformation, ({ StackName, StackStatus }) => StackName === stackName && isUpdateableStatus(StackStatus));
    return stacks[0] && stacks[0].id;
});
exports.getStackTemplateParameters = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const template = yield exports.getStackTemplate({ cloudformation, stackId });
    return template.Parameters || {};
});
exports.getReuseParameters = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const parameters = yield exports.getStackTemplateParameters({ cloudformation, stackId });
    return Object.keys(parameters).map(p => ({
        ParameterKey: p,
        UsePreviousValue: true,
    }));
});
exports.getStackTemplate = ({ cloudformation, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { TemplateBody } = yield cloudformation.getTemplate({
        StackName: stackId
    }).promise();
    if (TemplateBody.trim().startsWith('{')) {
        return JSON.parse(TemplateBody);
    }
    return js_yaml_1.default.safeLoad(TemplateBody);
});
exports.awaitStackCreate = (cloudformation, stackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return exports.waitFor({ cloudformation, event: 'stackCreateComplete', stackName });
});
exports.awaitStackDelete = (cloudformation, stackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return exports.waitFor({ cloudformation, event: 'stackDeleteComplete', stackName });
});
exports.awaitStackUpdate = (cloudformation, stackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return exports.waitFor({ cloudformation, event: 'stackUpdateComplete', stackName });
});
exports.waitFor = ({ cloudformation, stackName, event }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        return yield cloudformation.waitFor(event, { StackName: stackName }).promise();
    }
    catch (err) {
        const url = exports.getConsoleLinkForStacksInRegion({ region: cloudformation.config.region, status: 'failed' });
        throw new errors_2.Errors.ServerError(`operation may have failed. Check your stacks here: ${url}`);
    }
});
exports.listStacks = (cloudformation, filter = acceptAll) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const listStacksOpts = {
        StackStatusFilter: ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE']
    };
    let stackInfos = [];
    let keepGoing = true;
    while (keepGoing) {
        let { NextToken, StackSummaries } = yield cloudformation.listStacks(listStacksOpts).promise();
        stackInfos = stackInfos.concat(StackSummaries.filter(filter).map(({ StackId, StackName }) => ({
            id: StackId,
            name: StackName
        })));
        listStacksOpts.NextToken = NextToken;
        keepGoing = !!NextToken;
    }
    return stackInfos;
});
exports.getApiBaseUrl = (cloudformation, StackName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const result = yield cloudformation.describeStacks({ StackName }).promise();
    const { Outputs } = result.Stacks[0];
    const endpoint = Outputs.find(x => /^ServiceEndpoint/.test(x.OutputKey));
    return endpoint.OutputValue;
});
exports.splitCamelCase = (str) => str.split(/(?=[A-Z])/g);
exports.splitCamelCaseToString = (str, delimiter = ' ') => exports.splitCamelCase(str).join(delimiter);
exports.checkCommandInPath = cmd => {
    const { code } = shelljs_1.default.exec(`command -v ${cmd}`);
    if (code !== 0) {
        throw new errors_2.Errors.InvalidEnvironment(`Please install: ${cmd}`);
    }
};
exports.wait = millis => new Promise(resolve => setTimeout(resolve, millis));
exports.normalizeNodeFlags = flags => {
    if (!(flags.inspect || flags['inspect-brk'] || flags.debug || flags['debug-brk']))
        return;
    const nodeVersion = Number(process.version.slice(1, 2));
    if (nodeVersion >= 8) {
        if (flags.debug) {
            delete flags.debug;
            flags.inspect = true;
        }
        if (flags['debug-brk']) {
            delete flags['debug-brk'];
            flags['inspect-brk'] = true;
        }
    }
    else {
        if (flags.debug || flags['debug-brk']) {
            flags.inspect = true;
        }
    }
};
exports.pickNonNull = obj => lodash_1.default.pickBy(obj, val => val != null);
const sha256 = (str, enc = 'hex') => {
    return crypto_1.default.createHash('sha256').update(str).digest(enc);
};
const shortenString = (str, maxLength) => {
    const tooLongBy = str.length - maxLength;
    if (tooLongBy <= 0)
        return str;
    if (str.length < 6)
        throw new Error(`string is too short: ${str}`);
    return str.slice(0, maxLength - 6) + sha256(str).slice(0, 6);
};
const parseMyCloudStackName = (name) => {
    const [shortName, stage] = name.match(exports.MY_CLOUD_STACK_NAME_REGEX).slice(1);
    return {
        shortName,
        stage,
    };
};
exports.isMyCloudStackName = (name) => exports.MY_CLOUD_STACK_NAME_REGEX.test(name);
exports.validateNewMyCloudStackName = (name) => {
    if (!exports.MY_CLOUD_STACK_NAME_STRICTER_REGEX.test(name)) {
        throw new errors_2.Errors.InvalidInput(`"newStackName" must adhere to regex: ${exports.MY_CLOUD_STACK_NAME_STRICTER_REGEX}`);
    }
};
exports.getServicesStackName = (stackName) => {
    const { shortName } = parseMyCloudStackName(stackName);
    const shorterName = shortenString(shortName, 14);
    return `${shorterName}-srvcs`;
};
exports.canAccessECRRepos = (ecr, { accountId, repoNames }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield ecr.describeRepositories({
            registryId: accountId,
            repositoryNames: repoNames
        }).promise();
        return true;
    }
    catch (err) {
        if (err.name === 'AccessDeniedException') {
            return false;
        }
        throw err;
    }
});
exports.doKeyPairsExist = (ec2, names) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const params = {
        KeyNames: names
    };
    try {
        yield ec2.describeKeyPairs(params).promise();
        return true;
    }
    catch (err) {
        return false;
    }
});
exports.listKeyPairs = (ec2) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { KeyPairs } = yield ec2.describeKeyPairs().promise();
    return KeyPairs.map(k => k.KeyName);
});
exports.listAZs = ({ region }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const ec2 = new aws_sdk_1.default.EC2({ region });
    const { AvailabilityZones } = yield ec2.describeAvailabilityZones({
        Filters: [
            {
                Name: 'region-name',
                Values: [region]
            }
        ]
    }).promise();
    return AvailabilityZones
        .filter(a => a.RegionName === region)
        .map(a => a.ZoneName);
});
exports.getUsedEIPCount = (ec2) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Addresses } = yield ec2.describeAddresses().promise();
    return Addresses.length;
});
exports.getConsoleLinkForStacksInRegion = ({ region, status = 'active' }) => {
    return `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks?filter=${status}`;
};
exports.updateEnvironments = (lambda, { functions, transform }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const confs = yield Promise.all(functions.map(FunctionName => lambda.getFunctionConfiguration({ FunctionName }).promise()));
    yield Promise.all(functions.map((FunctionName, i) => tslib_1.__awaiter(this, void 0, void 0, function* () {
        const conf = confs[i];
        const Variables = transform({
            name: FunctionName,
            env: conf.Environment.Variables
        });
        yield lambda.updateFunctionConfiguration({
            FunctionName,
            Environment: { Variables }
        }).promise();
    })));
});
exports.xor = (...args) => args.reduce((acc, next) => {
    return (acc || next) && !(acc && next);
}, false);
const isLocal = ({ local, remote, project }) => {
    if (local && remote) {
        throw new errors_2.Errors.InvalidInput('expected "local" or "remote" but not both');
    }
    if (local) {
        if (!project) {
            throw new errors_2.Errors.InvalidInput('expected "project", the path to your local serverless project');
        }
        if (!exports.isValidProjectPath(project)) {
            throw new errors_2.Errors.InvalidInput('expected "project" to point to serverless project dir');
        }
        return true;
    }
    if (typeof remote === 'boolean') {
        return !remote;
    }
    return !!project;
};
exports.normalizeConfOpts = (opts) => {
    const local = isLocal(opts);
    return Object.assign({}, opts, { local, remote: !local });
};
exports.isRemoteOnlyCommand = commandName => constants_1.REMOTE_ONLY_COMMANDS.includes(commandName);
exports.isSafeRemoteCommand = commandName => constants_1.SAFE_REMOTE_COMMANDS.includes(commandName);
exports.createQRCode = (...args) => {
    const QRCode = require('qrcode');
    const toFile = pify_1.default(QRCode.toFile.bind(QRCode));
    return toFile(...args);
};
const normalizeError = err => {
    if (err instanceof Error)
        return err;
    const { name, message = 'unspecified' } = err;
    let normalized;
    if (name && name in global) {
        const ctor = global[name];
        try {
            normalized = new ctor(name);
        }
        catch (err) { }
    }
    if (!normalized) {
        normalized = new Error(message);
    }
    lodash_1.default.extend(normalized, err);
    return normalized;
};
exports.unwrapReturnValue = ret => {
    const { error, result } = ret;
    if (error) {
        return Object.assign({}, ret, { error: normalizeError(error) });
    }
    if (result) {
        if (result.error || result.result) {
            return exports.unwrapReturnValue(result);
        }
    }
    return ret;
};
exports.series = (arr, exec) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    for (const item of arr) {
        yield exec(item);
    }
});
exports.requireOption = (obj, option, type) => {
    if (typeof obj[option] !== type) {
        throw new errors_2.Errors.InvalidInput(`expected ${type} "${option}"`);
    }
};
exports.requireOptions = (obj, typeMap) => {
    for (let opt in typeMap) {
        exports.requireOption(obj, opt, typeMap[opt]);
    }
};
exports.parseStackArn = (arn) => {
    const [region, accountId, more,] = arn.split(':').slice(3);
    const [stackName, id,] = more.split('/').slice(1);
    return { region, accountId, stackName };
};
exports.splitOnCharAtIdx = (str, idx) => {
    return [
        str.slice(0, idx),
        str.slice(idx + 1),
    ];
};
const capFirst = (str) => str[0].toUpperCase() + str.slice(1);
exports.toCamelCase = (str, delimiter = ' ') => {
    return str.split(delimiter)
        .filter(lodash_1.default.identity)
        .map((part, i) => i === 0 ? part : capFirst(part))
        .join('');
};
const createClientOpts = ({ profile, region }) => {
    const opts = {};
    if (region) {
        opts.region = region;
    }
    if (profile) {
        opts.credentials = new aws_sdk_1.default.SharedIniFileCredentials({ profile });
    }
    return opts;
};
exports.createCloudFormationClient = (opts) => new aws_sdk_1.default.CloudFormation(createClientOpts(opts));
exports.createDynamoDBClient = (opts) => new aws_sdk_1.default.DynamoDB(createClientOpts(opts));
exports.createS3Client = (opts) => new aws_sdk_1.default.S3(createClientOpts(opts));
exports.execWithListr = ({ title, fn }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    let result;
    yield new listr_1.default([
        {
            title,
            task: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
                result = yield fn();
            }),
        }
    ]).run();
    return result;
});
exports.validateISODate = (dateString) => {
    const date = new Date(dateString);
    let expectedString;
    try {
        expectedString = date.toISOString();
    }
    catch (err) { }
    if (expectedString !== dateString) {
        throw new errors_2.Errors.InvalidInput(`expected iso date, e.g. ${new Date('2000-11-30').toISOString()} ?`);
    }
};
const ALPHA_NUMERIC = 'abcdefghijklmnopqrstuvwxyz0123456789';
exports.randomAlphaNumericString = (length) => {
    const bytes = crypto_1.default.randomBytes(length);
    const letters = [].slice.call(bytes).map(b => ALPHA_NUMERIC[b % ALPHA_NUMERIC.length]);
    return letters.join('');
};
exports.doesResourceExist = ({ client, resource }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { type, name, value } = resource;
    switch (type) {
        case 'bucket':
            return exports.doesBucketExist({ s3: client.s3, bucket: value });
        case 'key':
            return exports.doesKeyExist({ kms: client.kms, keyId: value });
        case 'table':
            return exports.doesTableExist({ dynamodb: client.dynamodb, table: value });
        case 'loggroup':
            return exports.doesLogGroupExist({ logs: client.logs, name: value });
        case 'restapi':
            return exports.doesApiGatewayRestApiExist({ apigateway: client.apigateway, apiId: value });
        default:
            throw new errors_2.Errors.InvalidInput(`unimplemented existence check for resource type: ${type}`);
    }
});
exports.doesBucketExist = ({ s3, bucket }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield s3.headBucket({ Bucket: bucket }).promise();
        return true;
    }
    catch (err) {
        return false;
    }
});
exports.doesTableExist = ({ dynamodb, table }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield dynamodb.describeTable({ TableName: table }).promise();
        return true;
    }
    catch (err) {
        ignoreNotFound(err);
        return false;
    }
});
exports.doesLogGroupExist = ({ logs, name }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield logs.describeLogStreams({ logGroupName: name }).promise();
        return true;
    }
    catch (err) {
        ignoreNotFound(err);
        return false;
    }
});
exports.doesApiGatewayRestApiExist = ({ apigateway, apiId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield apigateway.getRestApi({ restApiId: apiId }).promise();
        return true;
    }
    catch (err) {
        ignoreNotFound(err);
        return false;
    }
});
exports.doesKeyExist = ({ kms, keyId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        const { KeyMetadata } = yield kms.describeKey({ KeyId: keyId }).promise();
        return !KeyMetadata.DeletionDate;
    }
    catch (err) {
        ignoreNotFound(err);
        return false;
    }
});
exports.getLockedParameterValues = ({ Parameters }) => {
    return lodash_1.default.transform(Parameters, (result, { AllowedValues = [] }, key) => {
        if (AllowedValues.length === 1) {
            result[key] = AllowedValues[0];
        }
    }, {});
};
exports.lockImmutableParameters = ({ template, parameters }) => {
    const locked = exports.getLockedParameterValues(template);
    parameters.forEach(p => {
        const key = p.ParameterKey;
        const value = p.ParameterValue;
        if (key in locked && value !== locked[key]) {
            logger_1.logger.debug(`locking parameter: ${key}`);
            p.ParameterValue = locked[key];
        }
    });
};
exports.getMissingParameters = ({ template, parameters }) => {
    return lodash_1.default.transform(template.Parameters, (result, value, key) => {
        if (!('Default' in value)) {
            if (!parameters.some(p => p.ParameterKey === key)) {
                result.push(Object.assign({}, value, { Name: key }));
            }
        }
    }, []);
};
exports.getParameterDescription = ({ Name, Label, Description }) => {
    return Description || Label || exports.splitCamelCaseToString(Name);
};
exports.getRestApiRootResourceId = ({ apigateway, apiId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { items } = yield apigateway.getResources({ restApiId: apiId }).promise();
    return items.find(i => i.path === '/').id;
});
exports.s3HeadObject = ({ s3, bucket, key }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        return yield s3.headObject({ Bucket: bucket, Key: key }).promise();
    }
    catch (err) {
        ignoreNotFound(err);
        throw new errors_2.Errors.NotFound(`object not found. bucket: ${bucket}, key: ${key}`);
    }
});
exports.doesS3ObjectExist = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    try {
        yield exports.s3HeadObject(opts);
        return true;
    }
    catch (err) {
        errors_1.default.ignore(err, errors_2.Errors.NotFound);
        return false;
    }
});
exports.assertS3ObjectExists = exports.s3HeadObject;
const reverseString = (str) => str.split('').reverse().join('');
exports.sortParameters = (params) => lodash_1.default.sortBy(params, p => reverseString(p.ParameterKey));
exports.getBucketEncryptionKey = ({ s3, kms, bucket }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    let ServerSideEncryptionConfiguration;
    try {
        ({ ServerSideEncryptionConfiguration } = yield s3.getBucketEncryption({ Bucket: bucket }).promise());
    }
    catch (err) {
        errors_1.default.ignore(err, { code: 'ServerSideEncryptionConfigurationNotFoundError' });
        return;
    }
    if (!ServerSideEncryptionConfiguration)
        return;
    const { Rules = [] } = ServerSideEncryptionConfiguration;
    if (!Rules.length)
        return;
    const kmsRule = Rules.find(r => !!r.ApplyServerSideEncryptionByDefault.KMSMasterKeyID);
    if (!kmsRule)
        return;
    const id = kmsRule.ApplyServerSideEncryptionByDefault.KMSMasterKeyID;
    if (id.startsWith('key/')) {
        return id;
    }
    const { KeyMetadata } = yield kms.describeKey({ KeyId: id }).promise();
    return `key/${KeyMetadata.KeyId}`;
});
exports.setBucketExpirationDays = ({ s3, bucket, days }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return yield exports.setBucketLifeCycleRule({
        s3,
        bucket,
        filter: r => r.Prefix === '' && !!r.Expiration,
        update: r => {
            r.Expiration.Days = days;
            r.NoncurrentVersionExpiration.NoncurrentDays = days;
        },
        create: () => ({
            ID: 'bucket-level-expiration',
            Status: 'Enabled',
            Expiration: {
                Days: days
            },
            NoncurrentVersionExpiration: {
                NoncurrentDays: days
            }
        })
    });
});
exports.setBucketLifeCycleRule = ({ s3, bucket, filter, create }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Rules } = yield s3.getBucketLifecycleConfiguration({ Bucket: bucket }).promise();
    let rule = Rules.find(filter);
    if (!rule) {
        rule = {};
        Rules.push(rule);
    }
    lodash_1.default.merge(rule, create());
    yield s3.putBucketLifecycleConfiguration({
        Bucket: bucket,
        LifecycleConfiguration: { Rules }
    }).promise();
});
exports.setBucketTransitionToGlacier = ({ s3, bucket, days }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    return yield exports.setBucketLifeCycleRule({
        s3,
        bucket,
        filter: r => r.Prefix === '' &&
            r.Transitions &&
            r.Transitions.some(t => t.StorageClass === 'GLACIER'),
        update: r => {
            r.Transitions.find(r => r.StorageClass === 'GLACIER').Days = days;
        },
        create: () => ({
            Status: 'Enabled',
            Transitions: [
                {
                    StorageClass: 'GLACIER',
                    Days: days,
                }
            ],
        })
    });
});
exports.describeTable = ({ dynamodb, tableName }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { Table } = yield dynamodb.describeTable({ TableName: tableName }).promise();
    return Table;
});
exports.setTableBillingMode = ({ dynamodb, tableName, billingMode }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const info = yield exports.describeTable({ dynamodb, tableName });
    const currentBillingMode = info.BillingModeSummary ? info.BillingModeSummary.BillingMode : 'PROVISIONED';
    if (currentBillingMode === billingMode)
        return;
    const ProvisionedThroughput = {
        ReadCapacityUnits: 10,
        WriteCapacityUnits: 10,
    };
    const params = {
        TableName: tableName,
        BillingMode: billingMode,
    };
    if (billingMode === 'PROVISIONED') {
        const { GlobalSecondaryIndexes = [] } = info;
        if (GlobalSecondaryIndexes.length) {
            params.GlobalSecondaryIndexUpdates = GlobalSecondaryIndexes.map(i => ({
                Update: {
                    IndexName: i.IndexName,
                    ProvisionedThroughput,
                }
            }));
        }
        params.ProvisionedThroughput = ProvisionedThroughput;
    }
    yield dynamodb.updateTable(params).promise();
});
exports.paramsToObject = (params) => params.reduce((obj, { ParameterKey, ParameterValue }) => {
    obj[ParameterKey] = ParameterValue;
    return obj;
}, {});
//# sourceMappingURL=utils.js.map