"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const os_1 = tslib_1.__importDefault(require("os"));
const tmp_1 = tslib_1.__importDefault(require("tmp"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const pify_1 = tslib_1.__importDefault(require("pify"));
const aws_sdk_1 = tslib_1.__importDefault(require("aws-sdk"));
const mkdirp_1 = tslib_1.__importDefault(require("mkdirp"));
const shelljs_1 = tslib_1.__importDefault(require("shelljs"));
const listr_1 = tslib_1.__importDefault(require("listr"));
const prompts_1 = require("./prompts");
const update_1 = require("./update");
const destroy_1 = require("./destroy");
const restore_1 = require("./restore");
const s3_1 = require("./s3");
const dynamodb_1 = require("./dynamodb");
const kyc_services_1 = require("./kyc-services");
const errors_1 = require("./errors");
const validate = tslib_1.__importStar(require("./validate"));
const utils = tslib_1.__importStar(require("./utils"));
const logger_1 = require("./logger");
const constants_1 = require("./constants");
const fs = tslib_1.__importStar(require("./fs"));
tmp_1.default.setGracefulCleanup();
const mkdirp = pify_1.default(mkdirp_1.default);
const pfs = pify_1.default(fs);
const { prettify, isValidProjectPath, toEnvFile } = utils;
const silentLogger = Object.keys(logger_1.logger).reduce((silent, method) => {
    silent[method] = () => { };
    return silent;
}, {});
const AWS_CONF_PATH = `${os_1.default.homedir()}/.aws/config`;
const getFileNameForItem = item => `${item.id}.json`;
const getLongFunctionName = ({ stackName, functionName }) => {
    if (functionName.lastIndexOf(stackName) === 0)
        return functionName;
    return `${stackName}-${functionName}`;
};
const DEPLOYABLES = [
    'bot',
    'style',
    'models',
    'modelsPack',
    'terms'
];
const getDeployables = opts => lodash_1.default.pick(opts, DEPLOYABLES);
const getDeployablesKeys = opts => Object.keys(getDeployables(opts));
const DEPLOY_ALL_OPTS = DEPLOYABLES.reduce((obj, prop) => {
    obj[prop] = true;
    return obj;
}, {});
const getOptsOnly = opts => lodash_1.default.omit(opts, 'args');
const normalizeDeployOpts = (opts, command = 'deploy') => {
    if (!lodash_1.default.isEmpty(opts.args)) {
        throw new errors_1.Errors.InvalidInput(`unknown arguments: ${opts.args.join(' ')}`);
    }
    const all = opts.all || !lodash_1.default.size(getDeployables(opts));
    if (all) {
        return lodash_1.default.extend({}, DEPLOY_ALL_OPTS, opts);
    }
    if (!lodash_1.default.size(getDeployables(opts))) {
        throw new errors_1.Errors.InvalidInput(`you didn't indicate anything to ${command}!`);
    }
    return opts;
};
const functions = {
    setconf: 'setconf',
    cli: 'cli',
    importDataUtils: 'import_data_utils'
};
const paths = {
    conf: './conf',
    bot: './conf/bot.json',
    style: './conf/style.json',
    terms: './conf/terms-and-conditions.md',
    models: './models',
    lenses: './lenses'
};
const readFile = {
    bot: () => fs.maybeReadJSON(paths.bot),
    style: () => fs.maybeReadJSON(paths.style),
    models: () => fs.readDirOfJSONs(paths.models),
    lenses: () => fs.readDirOfJSONs(paths.lenses),
    terms: () => fs.maybeRead(paths.terms),
};
const createImportDataUtilsMethod = ({ conf, method, props = [], required }) => (data) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    data = lodash_1.default.pick(data, props);
    (required || props).forEach(prop => {
        if (!(prop in data))
            throw new errors_1.Errors.InvalidInput(`expected "${prop}"`);
    });
    return yield conf.invokeAndReturn({
        functionName: functions.importDataUtils,
        arg: { method, data }
    });
});
class Conf {
    constructor(opts) {
        this.deploy = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const items = this.getDeployItems(opts);
            logger_1.logger.info(`deploying: ${getDeployablesKeys(items).join(', ')}`);
            if (opts.dryRun)
                return logger_1.logger.info('dry run, not executing');
            const { error, result } = yield this.invoke({
                functionName: functions.setconf,
                arg: items
            });
            if (error)
                throw error;
            return result;
        });
        this.invoke = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            let { functionName, arg } = opts;
            if (!functionName)
                functionName = yield prompts_1.fn(this, 'which function?');
            let result;
            try {
                const promise = this.remote ? this._invoke(opts) : this._invokeLocal(opts);
                result = yield promise;
            }
            catch (error) {
                return { error };
            }
            return utils.unwrapReturnValue({ result });
        });
        this.invokeAndReturn = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { error, result } = yield this.invoke(opts);
            if (error)
                throw error;
            return result;
        });
        this.getLongFunctionName = (functionName) => getLongFunctionName({
            stackName: this.stackName,
            functionName
        });
        this.getDeployItems = (opts) => {
            opts = normalizeDeployOpts(opts);
            const parts = {};
            if (opts.style) {
                parts.style = readFile.style();
            }
            if (opts.terms) {
                parts.terms = readFile.terms() || null;
            }
            if (opts.models) {
                const models = readFile.models();
                const lenses = readFile.lenses();
                if (models.length || lenses.length) {
                    parts.modelsPack = utils.pack({ models, lenses, namespace: this.namespace });
                }
            }
            if (opts.bot) {
                parts.bot = readFile.bot();
            }
            return parts;
        };
        this.load = (opts = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const opLogger = opts.logger || logger_1.logger;
            opts = normalizeDeployOpts(opts, 'load');
            opLogger.info(`loading: ${getDeployablesKeys(opts).join(', ')}\n`);
            if (opts.dryRun)
                return opLogger.info('dry run, not executing');
            const result = yield this.exec({
                args: ['getconf --conf']
            });
            if (opts.style && result.style) {
                opLogger.debug('loaded remote style');
                fs.write(paths.style, result.style);
            }
            if (opts.bot && result.bot) {
                opLogger.debug('loaded remote bot conf');
                fs.write(paths.bot, result.bot);
            }
            if (opts.terms && result.termsAndConditions) {
                opLogger.debug('loaded remote terms and conditions');
                fs.write(paths.terms, result.termsAndConditions.value);
            }
            if (opts.models && result.modelsPack) {
                opLogger.debug('loaded remote models and lenses');
                this.writeModels(result.modelsPack);
            }
        });
        this.writeModels = (modelsPack) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            yield ['models', 'lenses'].map((prop) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                const arr = modelsPack[prop];
                if (!arr)
                    return;
                yield this.writeToFiles({
                    dir: paths[prop],
                    arr,
                    name: getFileNameForItem
                });
            }));
        });
        this.writeToFiles = ({ dir, name, arr }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            yield mkdirp(dir);
            yield Promise.all(arr.map(item => {
                return fs.pwrite(path_1.default.join(dir, name(item)), item);
            }));
        });
        this.validate = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const items = this.getDeployItems(opts);
            lodash_1.default.each(items, (value, key) => {
                if (!validate[key]) {
                    logger_1.logger.info(`validation for ${key} will be done in cloud-side on deploy`);
                    return;
                }
                if (typeof value !== 'undefined') {
                    logger_1.logger.debug(`validating: ${key}`);
                    validate[key](value);
                }
            });
        });
        this.exec = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { error, result } = yield this.invoke({
                functionName: functions.cli,
                arg: opts.args[0],
                noWarning: opts.noWarning
            });
            if (error)
                throw error;
            if (!result) {
                throw new errors_1.Errors.ServerError('something went wrong, please wait a minute and try again');
            }
            return result;
        });
        this.init = (opts = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { haveLocal, haveRemote, overwriteEnv, region, awsProfile, stack = {}, projectPath, loadCurrentConf, } = yield prompts_1.init(this);
            if (overwriteEnv === false)
                return;
            if (!(haveRemote || projectPath)) {
                logger_1.logger.warn("Aborting. Re-run `tradleconf init` when you've either deployed a MyCloud, or have a local development environment, or both");
                return;
            }
            this.remote = haveRemote;
            this.region = region;
            this.profile = awsProfile;
            this.stackName = stack.name;
            this.stackId = stack.id;
            this.project = projectPath && path_1.default.resolve(process.cwd(), projectPath);
            yield this._init({ loadCurrentConf });
        });
        this._init = ({ loadCurrentConf }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this.client = null;
            const saveEnv = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
                this._saveEnv();
                yield Promise.all([
                    paths.models,
                    paths.lenses,
                    paths.conf
                ].map(dir => mkdirp(dir)));
            });
            const saveEnvTask = {
                title: 'saving .env',
                task: saveEnv
            };
            if (this.remote) {
                const tasks = [
                    {
                        title: 'loading deployment info',
                        task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                            ctx.info = yield this.getEndpointInfo();
                        })
                    },
                    {
                        title: 'initializing local conf',
                        task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                            this.apiBaseUrl = ctx.info.apiBaseUrl;
                            this.namespace = ctx.info.org.domain
                                .split('.')
                                .reverse()
                                .join('.');
                        })
                    },
                    saveEnvTask,
                ];
                if (loadCurrentConf) {
                    tasks.push({
                        title: 'loading remote configuration',
                        task: () => tslib_1.__awaiter(this, void 0, void 0, function* () { return this.load({ all: true, logger: silentLogger }); }),
                    });
                }
                yield new listr_1.default(tasks).run();
            }
            else {
                yield saveEnv();
            }
            logger_1.logger.success('initialization complete!');
        });
        this.createAWSClient = (opts = {}) => {
            const { profile = this.profile || process.env.awsProfile, region = this.region } = opts;
            if (region) {
                aws_sdk_1.default.config.update({ region });
            }
            if (profile) {
                aws_sdk_1.default.config.update({
                    credentials: new aws_sdk_1.default.SharedIniFileCredentials({ profile })
                });
            }
            const s3 = new aws_sdk_1.default.S3();
            const cloudformation = new aws_sdk_1.default.CloudFormation();
            const logs = new aws_sdk_1.default.CloudWatchLogs();
            const lambda = new aws_sdk_1.default.Lambda();
            const ecr = new aws_sdk_1.default.ECR();
            const ec2 = new aws_sdk_1.default.EC2();
            const opsworks = new aws_sdk_1.default.OpsWorks();
            const dynamodb = new aws_sdk_1.default.DynamoDB();
            const kms = new aws_sdk_1.default.KMS();
            const apigateway = new aws_sdk_1.default.APIGateway();
            return {
                s3,
                cloudformation,
                lambda,
                ecr,
                ec2,
                opsworks,
                region: aws_sdk_1.default.config.region,
                dynamodb,
                logs,
                kms,
                apigateway,
            };
        };
        this.getStacks = (opts = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const client = this.createAWSClient(opts);
            return yield utils.listStacks(client.cloudformation);
        });
        this.waitForStackUpdate = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { stackId = this.stackId } = opts || {};
            const client = this.createAWSClient();
            yield utils.awaitStackUpdate(client.cloudformation, stackId);
        });
        this.waitForStackDelete = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { stackId = this.stackId } = opts || {};
            const client = this.createAWSClient();
            yield utils.awaitStackDelete(client.cloudformation, stackId);
        });
        this.createDataBundle = ({ path }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            let bundle;
            try {
                bundle = fs.readJSON(path);
            }
            catch (err) {
                throw new errors_1.Errors.InvalidInput('expected "path" to bundle');
            }
            return yield this.invokeAndReturn({
                functionName: functions.importDataUtils,
                arg: {
                    method: 'createbundle',
                    data: bundle
                }
            });
        });
        this._createDataClaim = createImportDataUtilsMethod({
            conf: this,
            method: 'createclaim',
            props: ['key', 'claimType']
        });
        this.createDataClaim = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { qrCode } = opts;
            if (qrCode && !qrCode.endsWith('.png')) {
                throw new errors_1.Errors.InvalidInput(`expected qr code path to end with .png`);
            }
            const result = yield this._createDataClaim(opts);
            if (qrCode) {
                const { qrData } = result;
                yield utils.createQRCode(qrCode, qrData);
                logger_1.logger.info(`wrote qr code to: ${qrCode}\n`);
            }
            return result;
        });
        this.listDataClaims = createImportDataUtilsMethod({
            conf: this,
            method: 'listclaims',
            props: ['key']
        });
        this.getDataBundle = createImportDataUtilsMethod({
            conf: this,
            method: 'getbundle',
            props: ['key', 'claimId'],
            required: []
        });
        this._ensureRemote = () => {
            if (!this.remote) {
                throw new errors_1.Errors.InvalidInput('not supported for local dev env');
            }
        };
        this.destroy = ({ profile = this.profile, stackArn = this.stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!stackArn) {
                this._ensureStackNameKnown();
                this._ensureRemote();
            }
            yield destroy_1.destroy({
                client: this.createAWSClient({ profile, region: this.region }),
                profile,
                stackId: stackArn,
            });
        });
        this.getApiBaseUrl = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.local) {
                return null;
            }
            return utils.getApiBaseUrl(this.client.cloudformation, this.stackId || this.stackName);
        });
        this.info = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureStackNameKnown();
            return yield this._info();
        });
        this._info = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const getLinks = this.invokeAndReturn({
                functionName: 'cli',
                arg: 'links',
                noWarning: true
            });
            const getInfo = this.remote ? this.getEndpointInfo() : Promise.resolve({});
            const [links, info] = yield Promise.all([getLinks, getInfo]);
            return Object.assign({ links }, lodash_1.default.pick(info, ['version', 'chainKey', 'apiBaseUrl']));
        });
        this.getEndpointInfo = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const getApiBaseUrl = this.getApiBaseUrl();
            const getInfo = this.invokeAndReturn({
                functionName: 'info',
                arg: {},
                noWarning: true
            });
            const [apiBaseUrl, info] = yield Promise.all([getApiBaseUrl, getInfo]);
            if (info.statusCode !== 200) {
                throw new errors_1.Errors.ServerError(info.body);
            }
            if (info.isBase64Encoded) {
                info.body = new Buffer(info.body, 'base64');
            }
            const endpoint = JSON.parse(info.body);
            endpoint.apiBaseUrl = apiBaseUrl;
            return endpoint;
        });
        this.getFunctions = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureStackNameKnown();
            const { client, stackId, stackName } = this;
            return yield utils.listStackFunctionIds(client.cloudformation, stackId || stackName);
        });
        this.getFunctionShortNames = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const functions = yield this.getFunctions();
            return functions.map(f => f.slice(this.stackName.length + 1));
        });
        this.tail = (opts = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            return this.log(Object.assign({ watch: true }, opts));
        });
        this.log = (opts = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureStackNameKnown();
            this._ensureRemote();
            utils.checkCommandInPath('awslogs');
            const { client, stackName } = this;
            let functionName = opts.args[0];
            if (!functionName) {
                functionName = yield prompts_1.fn(this, 'which one do you want to log?');
            }
            const longName = getLongFunctionName({ stackName, functionName });
            const logOpts = getOptsOnly(opts);
            if (!(logOpts.start || logOpts.end)) {
                logOpts.start = '5m';
            }
            if (this.profile)
                logOpts.profile = this.profile;
            if (this.region)
                logOpts['aws-region'] = this.region;
            const passThrough = Object.keys(logOpts);
            const logOptsStr = passThrough
                .map(opt => {
                const key = utils.splitCamelCase(opt)
                    .join('-')
                    .toLowerCase();
                const val = logOpts[opt];
                if (val === true) {
                    return `--${key}`;
                }
                return `--${key}=${val}`;
            })
                .join(' ');
            const cmd = `awslogs get /aws/lambda/${longName} ${logOptsStr}`;
            logger_1.logger.info(cmd);
            shelljs_1.default.exec(cmd);
        });
        this.balance = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            return yield this.exec({
                args: ['balance']
            });
        });
        this.disable = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            return yield this.exec({
                args: ['setenvvar --key DISABLED --value 1']
            });
        });
        this.enable = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            return yield this.exec({
                args: ['setenvvar --key DISABLED']
            });
        });
        this.query = (query) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const queryString = JSON.stringify(query.replace(/[\s]/g, ' '));
            return yield this.exec({
                args: [`graphql --query ${queryString}`]
            });
        });
        this.getMyIdentity = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            return yield this.invokeAndReturn({
                functionName: 'cli',
                arg: 'identity',
                noWarning: true
            });
        });
        this.getMyPermalink = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const identity = yield this.getMyIdentity();
            return identity._permalink;
        });
        this.getCurrentVersion = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const versions = yield this.exec({
                args: ['listmyversions --limit 1'],
                noWarning: true
            });
            return versions[0];
        });
        this.listUpdates = ({ provider } = {}) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            let command = 'listupdates';
            if (provider) {
                command = `${command} --provider ${provider} --provider-permalink ${provider}`;
            }
            return yield this.exec({
                args: [command],
                noWarning: true
            });
        });
        this.listPreviousVersions = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            return yield this.exec({
                args: [`listmyversions`],
                noWarning: true
            });
        });
        this.requestUpdate = ({ tag, provider }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!tag)
                throw new errors_1.Errors.InvalidInput('expected string "tag"');
            this._ensureRemote();
            let command = `getupdate --tag "${tag}"`;
            if (provider)
                command = `${command} --provider ${provider}`;
            yield this.exec({
                args: [command],
                noWarning: true
            });
        });
        this.getUpdateInfo = ({ tag }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            const result = yield this.exec({
                args: [`getupdateinfo --tag "${tag}"`],
                noWarning: true
            });
            if (!result.update) {
                throw new errors_1.Errors.NotFound(`not found: update with version: ${tag}`);
            }
            return result;
        });
        this.update = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            const result = yield update_1.update(this, Object.assign({ stackId: this.stackId }, opts));
            const { recreated, newStackArn } = result;
            if (newStackArn) {
                yield this._updateEnvWithNewStackArn(newStackArn);
            }
        });
        this.updateManually = ({ templateUrl, stackParameters }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            if (stackParameters) {
                if (typeof stackParameters === 'string') {
                    stackParameters = fs.readJSON(stackParameters);
                }
            }
            else {
                const { stackId } = this;
                stackParameters = yield this._genStackParameters({ stackId });
            }
            yield this._confirmAboutToUpdate();
            yield this.applyUpdateAsCurrentUser({
                templateUrl,
                parameters: stackParameters,
                wait: true,
            });
        });
        this.rollback = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            yield update_1.update(this, Object.assign({ stackId: this.stackId, rollback: true }, opts));
        });
        this.applyUpdateAsCurrentUser = (update) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { templateUrl, notificationTopics, wait } = update;
            const { stackId } = this;
            const params = this._getBaseUpdateStackOpts();
            params.TemplateURL = templateUrl;
            params.UsePreviousTemplate = !templateUrl;
            const { cloudformation } = this.client;
            if (update.parameters) {
                params.Parameters = update.parameters;
            }
            else {
                params.Parameters = yield update_1.getDefaultUpdateParameters({ cloudformation, stackId, templateUrl });
            }
            if (notificationTopics) {
                params.NotificationARNs = notificationTopics;
            }
            logger_1.logger.info('updating stack, be patient');
            const waitTillComplete = yield utils.updateStack({ cloudformation, params });
            if (wait)
                yield waitTillComplete();
        });
        this.enableKYCServices = ({ paramInstanceType }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            return this.setKYCServices({
                rankOne: true,
                truefaceSpoof: true,
                paramInstanceType,
            });
        });
        this.disableKYCServices = (_a) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            var { servicesStackArn } = _a, opts = tslib_1.__rest(_a, ["servicesStackArn"]);
            if (servicesStackArn) {
                const task = {
                    title: `delete stack: ${servicesStackArn}`,
                    task: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        yield utils.deleteStackAndWait({
                            cloudformation: this.client.cloudformation,
                            params: {
                                StackName: servicesStackArn
                            },
                        });
                    })
                };
                return yield new listr_1.default([task]).run();
            }
            return kyc_services_1.deleteCorrespondingServicesStack({
                cloudformation: this.client.cloudformation,
                stackId: this.stackId,
            });
        });
        this.setKYCServices = ({ truefaceSpoof, rankOne, paramInstanceType }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureRegionKnown();
            const stackParameters = {};
            if (paramInstanceType) {
                stackParameters.InstanceType = paramInstanceType;
            }
            yield kyc_services_1.configureKYCServicesStack(this, {
                truefaceSpoof,
                rankOne,
                mycloudStackName: this.stackName,
                mycloudRegion: this.region,
                accountId: utils.parseStackArn(this.stackId).accountId,
                client: this.client,
                stackParameters,
            });
        });
        this.getResourceByOutputName = (name) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureStackNameKnown();
            const outputs = yield utils.listOutputResources({
                cloudformation: this.client.cloudformation,
                stackId: this.stackId
            });
            return outputs.find(o => o.name === name).value;
        });
        this.getPrivateConfBucket = () => this.getResourceByOutputName('PrivateConfBucket');
        this.getLogsBucket = () => this.getResourceByOutputName('LogsBucket');
        this.reboot = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            const functions = yield this.getFunctions();
            const DATE_UPDATED = String(Date.now());
            yield utils.updateEnvironments(this.client.lambda, {
                functions,
                transform: ({ name, env }) => (Object.assign({}, env, { DATE_UPDATED }))
            });
            logger_1.logger.info('warming up rebooted functions');
            yield this.warmup();
        });
        this.getStackTemplate = ({ output }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            const template = yield utils.getStackTemplate({
                cloudformation: this.client.cloudformation,
                stackId: this.stackId
            });
            const outputPath = output.startsWith('/') ? output : path_1.default.resolve(process.cwd(), output);
            fs.write(outputPath, template);
        });
        this.warmup = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            return yield this.invokeAndReturn({
                functionName: 'genericJobRunner',
                arg: { name: 'warmup' },
                noWarning: true
            });
        });
        this.restoreFromStack = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureInitialized();
            let { sourceStackArn = this.stackId, newStackName = this.stackName, stackParameters } = opts;
            if (stackParameters) {
                stackParameters = fs.readJSON(stackParameters);
            }
            const newStackArn = yield restore_1.restoreStack(Object.assign({}, opts, { conf: this, sourceStackArn,
                stackParameters,
                newStackName }));
            yield this._updateEnvWithNewStackArn(newStackArn);
        });
        this.restoreResources = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            const { date, sourceStackArn = this.stackId, output } = opts;
            if (!sourceStackArn) {
                this._ensureInitialized();
            }
            const params = yield restore_1.restoreResources({
                client: this.client,
                profile: this.profile,
                region: this.region,
                sourceStackArn,
                date,
            });
            if (output) {
                fs.write(path_1.default.resolve(process.cwd(), output), params);
            }
            else {
                console.log(prettify(params));
            }
        });
        this.restoreBucket = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            opts = Object.assign({}, opts, { profile: this.profile });
            const s3 = s3_1.create(this.client.s3);
            yield s3.assertCanRestoreBucket(opts);
            logger_1.logger.info(`${constants_1.DOT} ok, I'll let you know when I'm done`);
            yield s3.restoreBucket(opts);
            const { destName } = opts;
            logger_1.logger.info(`all done! Make sure to set ${destName} as Existing[BucketLogicalName] in your stack-parameters before you run restore-stack`);
        });
        this.restoreTable = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const dynamodb = dynamodb_1.create(this.client.dynamodb);
            yield dynamodb.assertCanRestoreTable(opts);
            logger_1.logger.info(`${constants_1.DOT} yes sir! I'll let you know when I'm done`);
            const { table, stream } = yield dynamodb.restoreTable(opts);
            const logicalName = stream ? 'ExistingBucket0Table' : 'ExistingEventsTable';
            const params = [
                {
                    ParameterKey: logicalName,
                    ParameterValue: table,
                }
            ];
            if (stream) {
                params.push({
                    ParameterKey: `${logicalName}StreamArn`,
                    ParameterValue: stream,
                });
            }
            return params;
        });
        this.genStackParameters = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            const { sourceStackArn = this.stackId, output } = opts;
            const params = yield this._genStackParameters({ stackId: sourceStackArn });
            if (output) {
                fs.write(path_1.default.resolve(process.cwd(), output), params);
            }
            else {
                console.log(prettify(params));
            }
        });
        this.setSealingMode = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { mode, periodInMinutes } = opts;
            if (!(mode === 'single' || mode === 'batch')) {
                throw new errors_1.Errors.InvalidInput(`expected mode to be 'single' or 'batch'`);
            }
            const params = {
                SealingMode: mode,
            };
            if (mode === 'batch' && periodInMinutes) {
                params.SealBatchingPeriodInMinutes = periodInMinutes;
            }
            yield this._updateWithParameters(params);
        });
        this.setAdminEmail = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { email } = opts;
            if (!email) {
                throw new errors_1.Errors.InvalidInput(`expected string "email"'`);
            }
            yield this._updateWithParameters({
                OrgAdminEmail: email,
            });
        });
        this.setDBAutoscaling = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            if (!utils.xor(opts.onDemand, opts.provisioned)) {
                throw new errors_1.Errors.InvalidInput(`expected --on-demand or --provisioned`);
            }
            const { stackId } = this;
            const { cloudformation, dynamodb } = this.client;
            const stackInfoParams = { cloudformation, stackId };
            const billingMode = opts.provisioned ? 'PROVISIONED' : 'PAY_PER_REQUEST';
            const tables = yield utils.getMyCloudStackTables(stackInfoParams);
            yield Promise.all(tables.map((tableName) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                yield utils.setTableBillingMode({ dynamodb, tableName, billingMode });
            })));
            const currentParams = yield utils.getStackParameters(stackInfoParams);
            const currentParam = currentParams.find(p => p.ParameterKey === 'ProvisionDynamoDBScaling');
            if (!currentParam) {
                logger_1.logger.error(`your stack does not support on-demand autoscaling, perhaps you need to update first?`);
                return;
            }
            const currentVal = currentParam.ParameterValue;
            const newVal = String(!!opts.provisioned);
            if (newVal === currentVal)
                return;
            yield this._updateWithParameters({
                ProvisionDynamoDBScaling: newVal,
            });
        });
        this.setLogsTTL = ({ days }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            days = parseInt(days);
            yield this._updateWithParametersIfChanged({
                LogsTTL: String(days)
            });
            yield this._setLogsTTLOnLogsBucket(days);
        });
        this.setLogsTransition = ({ days }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            days = parseInt(days);
            yield this._updateWithParametersIfChanged({
                LogsDaysBeforeTransitionToGlacier: String(days)
            });
            yield this._setLogsTransitionOnLogsBucket(days);
        });
        this._setLogsTTLOnLogsBucket = (days) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const bucket = yield this.getLogsBucket();
            yield utils.setBucketExpirationDays({ s3: this.client.s3, bucket, days });
        });
        this._setLogsTransitionOnLogsBucket = (days) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const bucket = yield this.getLogsBucket();
            yield utils.setBucketTransitionToGlacier({ s3: this.client.s3, bucket, days });
        });
        this._confirmAboutToUpdate = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            yield prompts_1.confirmOrAbort(`I'm about to update your MyCloud. Continue?`);
        });
        this._confirmAndUpdateWithParameters = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            yield this._confirmAboutToUpdate();
            yield this._updateWithParameters(opts);
        });
        this._updateWithParametersIfChanged = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureRemote();
            this._ensureStackNameKnown();
            const { stackId } = this;
            const { cloudformation } = this.client;
            const currentParams = yield utils.getStackParameters({ cloudformation, stackId });
            const currentParamsObj = utils.paramsToObject(currentParams);
            if (lodash_1.default.isMatch(currentParamsObj, opts)) {
                logger_1.logger.info(`skipping stack update, parameters haven't changed`);
                return false;
            }
            yield this._confirmAndUpdateWithParameters(opts);
            return true;
        });
        this._genStackParameters = ({ stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { region } = utils.parseStackArn(stackId);
            return yield restore_1.deriveParametersFromStack({
                client: this.createAWSClient({ region }),
                stackId,
            });
        });
        this._ensureStackNameKnown = () => {
            if (this.remote && !this.stackName) {
                throw new errors_1.Errors.InvalidInput(`hm...are you sure you're in the right directory?`);
            }
        };
        this._ensureRegionKnown = () => {
            if (this.remote && !this.region) {
                throw new errors_1.Errors.InvalidInput(`please re-run 'tradelconf init', your .env file is outdated`);
            }
        };
        this._ensureInitialized = () => {
            if (!fs.exists(path_1.default.resolve(process.cwd(), '.env'))) {
                throw new errors_1.Errors.InvalidInput(`hm...are you sure you're in the right directory? I don't see a .env file`);
            }
        };
        this._invoke = ({ functionName, arg = {}, noWarning }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!(this.remote || noWarning)) {
                yield prompts_1.confirmOrAbort(`Targeting REMOTE deployment. Continue?`);
            }
            this._ensureStackNameKnown();
            const { StatusCode, Payload, FunctionError } = yield this.client.lambda.invoke({
                InvocationType: 'RequestResponse',
                FunctionName: this.getLongFunctionName(functionName),
                Payload: JSON.stringify(arg)
            }).promise();
            if (FunctionError || StatusCode >= 300) {
                const message = Payload || FunctionError;
                throw new Error(message.toString());
            }
            return JSON.parse(Payload.toString());
        });
        this._invokeLocal = ({ functionName, arg }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { project, nodeFlags } = this;
            const flagsStr = Object.keys(nodeFlags)
                .filter(key => nodeFlags[key])
                .map(key => `--${key}`)
                .join(' ');
            if (typeof arg !== 'string')
                arg = JSON.stringify(arg);
            const tmpInput = tmp_1.default.fileSync({ postfix: '.json' });
            const tmpOutput = tmp_1.default.fileSync({ postfix: '.json' });
            fs.write(tmpInput.name, JSON.stringify(arg));
            const pwd = process.cwd();
            shelljs_1.default.cd(project);
            logger_1.logger.debug('be patient, local invocations can be slow');
            const envVars = lodash_1.default.pick(process.env, ['SERVERLESS_OFFLINE_APIGW']);
            if (!envVars.IS_OFFLINE)
                envVars.IS_OFFLINE = '1';
            const command = `${stringifyEnv(envVars)} node ${flagsStr} \
  "${project}/node_modules/.bin/sls" invoke local \
  -f "${functionName}" \
  -l false \
  --path "${tmpInput.name}" \
  --output "${tmpOutput.name}"`;
            logger_1.logger.debug(`running command: ${command}`);
            const result = shelljs_1.default.exec(command, { silent: true });
            shelljs_1.default.cd(pwd);
            const res = fs.read(tmpOutput.name).trim();
            if (result.code !== 0)
                throw new Error(`invoke failed: ${res || result.stderr}`);
            return res && JSON.parse(res);
        });
        this._saveEnv = () => {
            const env = utils.pickNonNull({
                region: this.region,
                awsProfile: this.profile,
                stackName: this.stackName,
                stackId: this.stackId,
                project: this.project,
                apiBaseUrl: this.apiBaseUrl,
                namespace: this.namespace,
            });
            fs.write('.env', toEnvFile(env));
        };
        this._updateEnvWithNewStackArn = (arn) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { stackName, region } = utils.parseStackArn(arn);
            this.stackId = arn;
            this.stackName = stackName;
            this.region = region;
            yield this._init({ loadCurrentConf: true });
        });
        this._getBaseUpdateStackOpts = () => {
            this._ensureStackNameKnown();
            this._ensureRemote();
            return {
                StackName: this.stackId,
                Capabilities: ['CAPABILITY_NAMED_IAM'],
            };
        };
        this._updateWithParameters = (paramMap) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            this._ensureStackNameKnown();
            this._ensureRemote();
            const { stackId, client } = this;
            const params = yield this._genStackParameters({ stackId });
            for (let key in paramMap) {
                let param = params.find(p => p.ParameterKey === key);
                if (!param) {
                    throw new errors_1.Errors.InvalidInput(`unknown param: ${key}`);
                }
                param.ParameterValue = String(paramMap[key]);
            }
            yield this.applyUpdateAsCurrentUser({
                templateUrl: null,
                parameters: params,
                wait: true,
            });
        });
        const { remote, region, profile, namespace, stackId, stackName, project, nodeFlags = {} } = opts;
        if (typeof remote !== 'boolean') {
            throw new errors_1.Errors.InvalidInput(`expected boolean "remote"`);
        }
        if (remote && !lodash_1.default.isEmpty(nodeFlags)) {
            throw new errors_1.Errors.InvalidInput('node debugging flags are only supported for local operations');
        }
        utils.normalizeNodeFlags(nodeFlags);
        this.nodeFlags = nodeFlags;
        this.region = region;
        this.namespace = namespace;
        this.profile = profile;
        this.stackName = stackName;
        this.stackId = stackId;
        this.project = project;
        this.remote = remote;
        this.local = !remote;
        let client;
        Object.defineProperty(this, 'client', {
            set(value) {
                client = value;
            },
            get() {
                if (!client) {
                    client = this.createAWSClient();
                }
                return client;
            }
        });
    }
}
exports.Conf = Conf;
exports.createConf = (opts) => new Conf(opts);
const stringifyEnv = props => Object.keys(props).map(key => `${key}="${props[key]}"`).join(' ');
//# sourceMappingURL=conf.js.map