"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
const promise_retry_1 = tslib_1.__importDefault(require("promise-retry"));
const lexicographic_semver_1 = require("lexicographic-semver");
const errors_1 = tslib_1.__importDefault(require("@tradle/errors"));
const errors_2 = require("./errors");
const logger_1 = require("./logger");
const prompts_1 = require("./prompts");
const restore_1 = require("./restore");
const utils = tslib_1.__importStar(require("./utils"));
const constants_1 = require("./constants");
const fs = tslib_1.__importStar(require("./fs"));
const USE_CURRENT_USER_ROLE = true;
const VERSION_MIN = '1.1.15';
const VERSION_V2 = '2.0.0';
const VERSION_V2_TRANS = '2.0.0-trans.0';
const isUpdatingToV2 = (fromTag, toTag) => {
    return lexicographic_semver_1.compareTags(fromTag, VERSION_V2) < 0 && lexicographic_semver_1.compareTags(toTag, VERSION_V2) >= 0;
};
class Updater {
    constructor({ conf, opts, currentVersion, updates }) {
        this.update = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { targetTag, currentVersion, updates, opts } = this;
            const { provider, rollback } = opts;
            if (!currentVersion) {
                this.currentVersion = yield this.conf.getCurrentVersion();
            }
            if (!updates) {
                this.updates = yield this._loadUpdates();
            }
            if (targetTag && rollback) {
                const idx = this.updates.findIndex(lodash_1.default.matches({ tag: targetTag }));
                if (idx === -1) {
                    const previousTags = updates.map(u => u.tag);
                    throw new Error(`your MyCloud was never previously deployed with version ${targetTag}

Your previously deployed versions are (most recently deployed at the top):

${previousTags.join('\n')}`);
                }
            }
            return yield this._update();
        });
        this._loadUpdates = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { conf, opts, currentVersion } = this;
            const { rollback, provider } = opts;
            const getUpdates = rollback
                ? conf.listPreviousVersions()
                : conf.listUpdates({ provider });
            let updates = yield getUpdates;
            updates = lodash_1.default.sortBy(updates, 'sortableTag');
            updates = updates.filter(u => u.tag !== currentVersion.tag);
            if (rollback) {
                updates = updates.reverse().filter(u => u.sortableTag < currentVersion.sortableTag);
            }
            return updates;
        });
        this._loadTargetTag = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { verb, updates, opts } = this;
            const { rollback, showReleaseCandidates } = opts;
            let choices = updates.slice();
            const noRC = !rollback && !showReleaseCandidates;
            if (noRC) {
                choices = choices.filter(update => !(isReleaseCandidateTag(update.tag) || isTransitionReleaseTag(update.tag)));
            }
            if (!choices.length) {
                throw new Error(`no ${verb} available`);
            }
            let message;
            if (rollback) {
                message = `Choose a version to roll back to (most recently deployed at the top)`;
            }
            else {
                message = `Choose a version to update to`;
            }
            const result = yield inquirer_1.default.prompt([{
                    type: 'rawlist',
                    pageSize: Infinity,
                    name: 'tag',
                    message,
                    choices: choices.map(({ tag }) => ({
                        name: getChoiceTextForTag(tag),
                        value: tag
                    })),
                }]);
            this.targetTag = result.tag;
        });
        this._update = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { conf, opts, currentVersion, updates, verb } = this;
            const { stackId, provider, showReleaseCandidates, force, rollback, } = opts;
            const { cloudformation } = conf.client;
            try {
                yield utils.assertStackExistsAndIsNotDeleted({ cloudformation, stackId });
            }
            catch (err) {
                throw new errors_2.Errors.InvalidEnvironment(`stack not found: ${stackId}

This can happen if your .env file is out of date. Re-run "tradleconf init" and try again.
`);
            }
            if (lexicographic_semver_1.compareTags(currentVersion.tag, VERSION_MIN) < 0) {
                throw new Error(`you have an old version of MyCloud which doesn't support the new update mechanism
  Please update manually this one time. See instructions on https://github.com/tradle/serverless`);
            }
            if (!this.targetTag) {
                yield this._loadTargetTag();
            }
            if (isUpdatingToV2(currentVersion.tag, this.targetTag)) {
                logger_1.logger.warnBold(`Updating to version ${this.targetTag} will require me to DELETE and RECREATE your stack

Your data should not be harmed in the process
`);
                yield prompts_1.confirmOrAbort('Continue?', false);
            }
            if (!rollback && currentVersion.templateUrl) {
                const currentParams = yield utils.getStackParameters({ cloudformation, stackId });
                const paramsRelPath = `params-${currentVersion.tag}-${Date.now()}.json`;
                const paramsAbsPath = path_1.default.resolve(process.cwd(), paramsRelPath);
                fs.write(paramsAbsPath, currentParams);
                logger_1.logger.info(`
Updating! To roll back, run:

tradleconf update-manually --template-url "${currentVersion.templateUrl}" --stack-parameters "${paramsRelPath}"
`);
            }
            const tag = this.targetTag;
            const tasks = [
                {
                    title: `loading release ${tag}, grab a coffee`,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        let resp;
                        try {
                            resp = yield this._getUpdateWithRetry();
                            if (!resp)
                                return;
                        }
                        catch (err) {
                            errors_1.default.ignore(err, errors_2.Errors.NotFound);
                            throw new Error(`failed to fetch version ${tag}. If you're confident it exists, please try again.`);
                        }
                        const { update, upToDate } = resp;
                        ctx.upToDate = upToDate;
                        ctx.update = update;
                        ctx.willUpdate = force || rollback || !upToDate;
                    })
                },
                {
                    title: 'checking for required transitional releases',
                    skip: ctx => force || !ctx.willUpdate,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        yield this._applyPrerequisiteTransitionTags();
                    })
                },
                {
                    title: `checking for any required transitional changes`,
                    skip: ctx => !ctx.willUpdate,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        const { update } = ctx;
                        const v1ToV2 = yield this._maybeTransitionV1ToV2({ update, stackId });
                        if (v1ToV2) {
                            ctx.willUpdate = false;
                            ctx.willRecreate = true;
                        }
                    })
                },
                {
                    title: 'killing and resurrecting',
                    skip: ctx => !ctx.willRecreate,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        const update = ctx.update;
                        const { parameters, templateUrl, notificationTopics } = update;
                        const { region, stackName } = utils.parseStackArn(stackId);
                        logger_1.logger.info(`if the the process is interrupted after the original stack is deleted, restore manually using:

tradleconf restore-stack --template-url "${templateUrl}"`);
                        yield utils.deleteStackAndWait({ cloudformation, params: { StackName: stackId } });
                        ctx.newStackArn = yield restore_1.createStack({
                            region,
                            templateUrl,
                            profile: this.opts.profile,
                            stackName,
                            parameters,
                            notificationTopics,
                        });
                    })
                },
                {
                    title: `triggering update`,
                    skip: ctx => !ctx.willUpdate,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        const update = ctx.update;
                        try {
                            yield this._triggerUpdate(update);
                        }
                        catch (err) {
                            if (err.code === 'ValidationError' && err.message.includes('UPDATE_IN_PROGRESS')) {
                                throw new Error('stack is currently updating, please wait for it to finish before applying another update');
                            }
                            throw err;
                        }
                    })
                },
                {
                    title: `waiting for ${verb} to complete, be patient, or else`,
                    skip: ctx => !ctx.willUpdate,
                    task: (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        try {
                            yield conf.waitForStackUpdate({ stackId });
                        }
                        catch (err) {
                            if (err.code === 'ResourceNotReady') {
                                throw new Error(`failed to apply ${verb}`);
                            }
                            throw err;
                        }
                    })
                }
            ];
            let ctx = {};
            for (const { title, skip, task } of tasks) {
                if (skip && skip(ctx))
                    continue;
                logger_1.logger.info(`${constants_1.DOT} ${title}...`);
                yield task(ctx);
            }
            if (ctx.upToDate && !ctx.willUpdate) {
                if (tag === currentVersion.tag) {
                    throw new Error(`your MyCloud is already up to date!`);
                }
                this._throwBackwardsError();
            }
            return {
                updated: ctx.willUpdate,
                recreated: ctx.willRecreate,
                newStackArn: ctx.newStackArn,
            };
        });
        this._maybeTransitionV1ToV2 = ({ stackId, update }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { conf } = this;
            const { cloudformation } = conf.client;
            const isV1Stack = yield utils.isV1Stack({ cloudformation, stackId });
            if (!isV1Stack)
                return;
            const parameters = yield this._getV1ToV2Parameters({ stackId, update });
            if (!parameters)
                return;
            update.parameters = parameters;
            return true;
        });
        this._getV1ToV2Parameters = ({ stackId, update }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { conf } = this;
            const { client } = conf;
            const { cloudformation } = conf.client;
            const [parameters, currentTemplate, newTemplate] = yield Promise.all([
                restore_1.deriveParametersFromStack({ client, stackId }),
                utils.getStackTemplate({ cloudformation, stackId }),
                utils.get(update.templateUrl),
            ]);
            if (!utils.isV2Template(newTemplate))
                return;
            const missing = yield restore_1.getBlanksForMissingParameters({ template: newTemplate, parameters });
            return parameters.concat(missing);
        });
        this._throwBackwardsError = () => {
            const { currentVersion, targetTag } = this;
            throw new Error(`your MyCloud is of a more recent version: ${currentVersion.tag}

To force deploy ${targetTag}, run: tradleconf update --tag ${targetTag} --force`);
        };
        this._getUpdateWithRetry = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const tag = this.targetTag;
            const { conf, opts } = this;
            const { provider } = opts;
            let requested;
            return promise_retry_1.default((retry, number) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    return yield conf.getUpdateInfo({ tag });
                }
                catch (err) {
                    errors_1.default.rethrow(err, ['developer', errors_2.Errors.InvalidInput]);
                    if (!requested && errors_1.default.matches(err, errors_2.Errors.NotFound)) {
                        requested = true;
                        yield conf.requestUpdate({ tag, provider });
                    }
                    retry(err);
                }
            }), {
                maxTimeout: 10000,
                minTimeout: 5000,
                retries: 10
            });
        });
        this._applyPrerequisiteTransitionTags = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { updates, currentVersion, targetTag } = this;
            const idx = updates.findIndex(update => update.tag === targetTag);
            const updatesBeforeTag = idx === -1 ? updates : updates.slice(0, idx);
            const transition = updatesBeforeTag.find(update => isTransitionReleaseTag(update.tag));
            if (!transition) {
                if (isUpdatingToV2(currentVersion.tag, targetTag) && lexicographic_semver_1.compareTags(currentVersion.tag, VERSION_V2_TRANS) < 0) {
                    throw new Error(`please run this first: tradleconf update --tag ${VERSION_V2_TRANS}`);
                }
                return;
            }
            logger_1.logger.warnBold(`you must apply the transition version first: ${transition.tag}`);
            yield prompts_1.confirmOrAbort(`apply transition tag ${transition.tag} now?`);
            yield exports.update(this.conf, Object.assign({}, this.opts, { force: true, tag: transition.tag, currentVersion,
                updates }));
        });
        this._triggerUpdate = (update) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { conf } = this;
            if (USE_CURRENT_USER_ROLE) {
                yield conf.applyUpdateAsCurrentUser(update);
            }
            else {
                throw new errors_2.Errors.InvalidInput(`updating via lambda is not supported`);
            }
        });
        this.conf = conf;
        this.opts = opts;
        this.currentVersion = currentVersion;
        this.updates = updates;
        this.targetTag = opts.tag;
        this.verb = opts.rollback ? 'rollback' : 'update';
    }
}
const isReleaseCandidateTag = (tag) => /-rc\.\d+$/.test(tag);
const isTransitionReleaseTag = (tag) => /-trans/.test(tag);
const getChoiceTextForTag = (tag) => {
    if (isTransitionReleaseTag(tag)) {
        return `${tag} (transition version, must be applied before the version that follows)`;
    }
    if (isReleaseCandidateTag(tag)) {
        return `${tag} (release candidate with experimental fixes / features)`;
    }
    return tag;
};
exports.update = (conf, opts) => new Updater({ conf, opts }).update();
exports.getDefaultUpdateParameters = ({ cloudformation, stackId, templateUrl }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const template = yield utils.get(templateUrl);
    const allParamNames = Object.keys(template.Parameters);
    let reused = yield utils.getReuseParameters({ cloudformation, stackId });
    reused = reused.filter(({ ParameterKey }) => allParamNames.includes(ParameterKey));
    const { bucket } = utils.parseS3Url(templateUrl);
    const source = reused.find(p => p.ParameterKey === 'SourceDeploymentBucket');
    if (source) {
        delete source.UsePreviousValue;
        source.ParameterValue = bucket;
    }
    return reused;
});
//# sourceMappingURL=update.js.map