"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
const listr_1 = tslib_1.__importDefault(require("listr"));
const errors_1 = tslib_1.__importDefault(require("@tradle/errors"));
const errors_2 = require("./errors");
const logger_1 = require("./logger");
const dynamodb_1 = require("./dynamodb");
const s3_1 = require("./s3");
const utils = tslib_1.__importStar(require("./utils"));
const constants_1 = require("./constants");
const IS_RESTORABLE = {
    table: true,
    bucket: true,
};
const shouldRestoreBucket = ({ name }) => name !== 'LogsBucket' && name !== 'SourceDeploymentBucket';
const shouldRestoreTable = (output) => true;
const RESTORED_RESOURCE_NAME_REGEX = /-r(\d+)$/;
exports.deriveRestoredResourceName = ({ stackName, type, name, value }) => {
    let prevCount = 0;
    const restoredMatch = value.match(RESTORED_RESOURCE_NAME_REGEX);
    if (restoredMatch) {
        prevCount = parseInt(restoredMatch[1]);
    }
    let baseName = `${stackName}-${name}`.toLowerCase();
    if (baseName.endsWith(type)) {
        baseName = baseName.slice(0, -type.length);
    }
    if (type === 'bucket') {
        const rand = utils.randomAlphaNumericString(6);
        baseName = `${baseName}-${rand}`;
    }
    return `${baseName}-r${(prevCount + 1)}`;
};
exports.restoreResources = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { client, region, profile, sourceStackArn, date } = opts;
    utils.validateISODate(date);
    const { stackName } = utils.parseStackArn(sourceStackArn);
    const { cloudformation } = client;
    const dynamodb = dynamodb_1.create(client.dynamodb);
    const s3 = s3_1.create(client.s3);
    const getBaseParams = exports.deriveParametersFromStack({ client, stackId: sourceStackArn });
    const getOutputs = utils.listOutputResources({ cloudformation, stackId: sourceStackArn });
    const parameters = yield getBaseParams;
    const outputs = (yield getOutputs).filter(o => IS_RESTORABLE[o.type]);
    outputs.forEach((o, i) => {
        const param = parameters.find(p => p.ParameterValue === o.value);
        if (!param) {
            throw new errors_2.Errors.NotFound(`expected parameter corresponding to output ${o.name}`);
        }
    });
    const buckets = outputs.filter(o => o.type === 'bucket' && shouldRestoreBucket(o));
    const oldBucketIds = buckets.map(b => b.value);
    const tables = outputs.filter(o => o.type === 'table' && shouldRestoreTable(o));
    const oldTableNames = tables.map(t => t.value);
    const getRestoredResourceName = (resource) => exports.deriveRestoredResourceName(Object.assign({ stackName }, resource));
    const restoreBucketsOpts = buckets.map(source => ({
        sourceName: source.value,
        destName: getRestoredResourceName(source),
        date,
        profile,
    }));
    const restoreTablesOpts = tables.map(table => ({
        sourceName: table.value,
        destName: getRestoredResourceName(table),
        date,
    }));
    yield Promise.all([
        Promise.all(restoreTablesOpts.map(opts => dynamodb.assertCanRestoreTable(opts))),
        Promise.all(restoreBucketsOpts.map(opts => s3.assertCanRestoreBucket(opts)))
    ]);
    logger_1.logger.warn(`This will take a while. Do NOT interrupt me!`);
    const streams = {};
    const tableTasks = restoreTablesOpts.map((opts, i) => {
        const { sourceName, destName } = opts;
        return {
            title: `${sourceName} -> ${destName}`,
            task: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
                const { table, stream } = yield dynamodb.restoreTable(opts);
                streams[destName] = stream;
            })
        };
    });
    const bucketTasks = restoreBucketsOpts.map(opts => {
        const { sourceName, destName } = opts;
        return {
            title: `${sourceName} -> ${destName}`,
            task: () => s3.restoreBucket(opts)
        };
    });
    yield new listr_1.default(tableTasks.concat(bucketTasks), { concurrent: true }).run();
    const old = tables.concat(buckets).map(r => r.value);
    const restored = lodash_1.default.map(restoreTablesOpts.concat(restoreBucketsOpts), 'destName');
    let [irreplaceable, replaceable] = lodash_1.default.partition(parameters, p => p.ParameterKey === 'SourceDeploymentBucket');
    old.forEach((oldPhysicalId, i) => {
        const newPhysicalId = restored[i];
        const param = replaceable.find(p => p.ParameterValue === oldPhysicalId);
        param.ParameterValue = newPhysicalId;
        const stream = streams[newPhysicalId];
        if (!stream)
            return;
        const streamParam = replaceable.find(p => p.ParameterValue.includes(`table/${oldPhysicalId}/stream`));
        if (streamParam) {
            streamParam.ParameterValue = stream;
        }
        else {
            logger_1.logger.warn(`relevant parameter not found for stream ${stream} (table ${newPhysicalId})`);
        }
    });
    irreplaceable = irreplaceable.map(r => ({
        ParameterKey: r.ParameterKey,
        UsePreviousValue: true,
    }));
    return irreplaceable.concat(replaceable);
});
const OUTPUT_NAME_TO_PARAM = {
    ObjectsBucket: 'ExistingObjectsBucket',
    SecretsBucket: 'ExistingSecretsBucket',
    PrivateConfBucket: 'ExistingPrivateConfBucket',
    FileUploadBucket: 'ExistingFileUploadBucket',
    LogsBucket: 'ExistingLogsBucket',
    DeploymentBucket: 'ExistingDeploymentBucket',
    EventsTable: 'ExistingEventsTable',
    Bucket0Table: 'ExistingBucket0Table',
    Bucket0TableStream: 'ExistingBucket0TableStreamArn',
    EncryptionKey: 'ExistingEncryptionKey',
    BucketEncryptionKey: 'ExistingBucketEncryptionKey',
    ApiGatewayRestApi: 'ExistingApiGatewayRestApi',
    ApiGatewayRestApiRootResourceId: 'ExistingApiGatewayRestApiRootResourceId',
};
const setImmutableParameters = (parameters) => {
    constants_1.IMMUTABLE_STACK_PARAMETERS.forEach(name => {
        let old = parameters.find(p => p.ParameterKey === name);
        if (!old) {
            old = { ParameterKey: name };
            parameters.push(old);
        }
        delete old.ParameterValue;
        old.UsePreviousValue = true;
    });
};
const isNonEmptyParameter = ({ ParameterValue }) => ParameterValue !== '';
exports.isOverwrite = (oldParameters, newParam) => {
    const { ParameterKey, ParameterValue } = newParam;
    const oldParam = oldParameters.find(p => p.ParameterKey === ParameterKey);
    return oldParam && isNonEmptyParameter(oldParam);
};
exports.deriveParametersFromStack = ({ client, stackId }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { cloudformation, apigateway } = client;
    const oldStack = yield utils.getStackInfo({ cloudformation, stackId });
    const oldParameters = oldStack.Parameters;
    const oldOutputs = oldStack.Outputs;
    const newParameters = oldOutputs
        .filter(r => r.OutputKey in OUTPUT_NAME_TO_PARAM)
        .map(({ OutputKey, OutputValue }) => ({
        ParameterKey: OUTPUT_NAME_TO_PARAM[OutputKey],
        ParameterValue: OutputValue,
    }))
        .filter(newParam => !exports.isOverwrite(oldParameters, newParam));
    const remainingOld = oldParameters.filter(({ ParameterKey }) => !newParameters.find(p => p.ParameterKey === ParameterKey));
    const parameters = newParameters.concat(remainingOld);
    const restApi = newParameters.find(o => o.ParameterKey === 'ExistingApiGatewayRestApi');
    if (restApi) {
        const rootResourceId = oldOutputs.find(o => o.OutputKey === 'ApiGatewayRestApiRootResourceId');
        if (!rootResourceId) {
            const root = yield utils.getRestApiRootResourceId({ apigateway, apiId: restApi.ParameterValue });
            parameters.push({
                ParameterKey: 'ExistingApiGateway',
                ParameterValue: root,
            });
        }
    }
    const sourceParam = parameters.find(p => p.ParameterKey === 'SourceDeploymentBucket');
    if (sourceParam && !sourceParam.ParameterValue) {
        const deploymentBucketParam = parameters.find(p => p.ParameterKey === 'ExistingDeploymentBucket');
        if (deploymentBucketParam && deploymentBucketParam.ParameterValue) {
            sourceParam.ParameterValue = deploymentBucketParam.ParameterValue;
        }
    }
    return utils.sortParameters(parameters);
});
exports.restoreStack = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    utils.requireOption(opts, 'conf', 'object');
    utils.requireOption(opts, 'sourceStackArn', 'string');
    utils.requireOption(opts, 'newStackName', 'string');
    let { conf, sourceStackArn, newStackName, templateUrl, stackParameters, } = opts;
    const { client, profile = 'default' } = conf;
    utils.validateNewMyCloudStackName(newStackName);
    const { region } = utils.parseStackArn(sourceStackArn);
    const cloudformation = utils.createCloudFormationClient({ region, profile });
    const getOldTemplate = yield utils.getStackTemplate({ cloudformation, stackId: sourceStackArn });
    const getNewTemplate = templateUrl ? utils.get(templateUrl) : getOldTemplate;
    const [oldTemplate, template] = yield Promise.all([getOldTemplate, getNewTemplate]);
    if (!stackParameters) {
        stackParameters = yield exports.deriveParametersFromStack({ client, stackId: sourceStackArn });
    }
    const getMissing = utils.isV2Template(oldTemplate)
        ? Promise.resolve([])
        : exports.promptMissingParameters({ conf, template, parameters: stackParameters });
    const missing = yield getMissing;
    stackParameters.push(...missing);
    stackParameters = utils.sortParameters(stackParameters);
    utils.lockImmutableParameters({ template, parameters: stackParameters });
    if (!templateUrl) {
        const deploymentBucket = stackParameters.find(p => p.ParameterKey === 'ExistingDeploymentBucket');
        const s3PathToUploadTemplate = `${deploymentBucket.ParameterValue}/tmp/recovery-template-${Date.now()}.json`;
        logger_1.logger.info(`uploading template to ${s3PathToUploadTemplate}`);
        const s3 = utils.createS3Client({ region, profile });
        const [Bucket, Key] = utils.splitOnCharAtIdx(s3PathToUploadTemplate, s3PathToUploadTemplate.indexOf('/'));
        yield s3.putObject({
            Bucket,
            Key,
            Body: new Buffer(JSON.stringify(template)),
            ACL: 'public-read',
            ContentType: 'application/json',
        }).promise();
        templateUrl = `https://${Bucket}.s3.amazonaws.com/${Key}`;
    }
    return exports.createStack({
        stackName: newStackName,
        parameters: stackParameters,
        region,
        templateUrl,
        profile,
    });
});
exports.validateParameters = ({ dynamodb, parameters }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const ddbHelper = dynamodb_1.create(dynamodb);
    const tables = parameters.filter(p => p.ParameterKey.startsWith('Existing') && p.ParameterKey.endsWith('Table'));
    const streams = parameters.filter(p => p.ParameterKey.startsWith('Existing') && p.ParameterKey.endsWith('StreamArn'));
    yield Promise.all(tables.map((table, i) => tslib_1.__awaiter(this, void 0, void 0, function* () {
        const stream = streams.find(s => s.ParameterKey.startsWith(table.ParameterKey));
        if (!stream)
            return;
        yield ddbHelper.ensureStreamMatchesTable({ tableArn: table.ParameterValue, streamArn: stream.ParameterValue });
    })));
});
exports.createStack = (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const { stackName, region, templateUrl, parameters, profile, notificationTopics } = opts;
    const dynamodb = utils.createDynamoDBClient({ region, profile });
    yield exports.validateParameters({ dynamodb, parameters });
    const cloudformation = utils.createCloudFormationClient({ region, profile });
    logger_1.logger.info('grab some patience, this will take a while (5-20 minutes)');
    return yield utils.createStackAndWait({
        cloudformation,
        params: {
            StackName: stackName,
            Capabilities: ['CAPABILITY_NAMED_IAM'],
            TemplateURL: templateUrl,
            Parameters: parameters,
            DisableRollback: true,
            NotificationARNs: notificationTopics || [],
        },
    });
});
exports.getPromptsForParameters = (parameters) => parameters.map(exports.getPromptForParameter);
const createValidatorForParameter = (parameter) => {
    const { Type, AllowedPattern, MinValue = -Infinity, MaxValue = Infinity, MinLength = -Infinity, MaxLength = Infinity, ConstraintDescription, } = parameter;
    if (AllowedPattern) {
        const regex = new RegExp(AllowedPattern);
        return result => {
            if (!regex.test(result)) {
                return ConstraintDescription || `must match pattern: ${AllowedPattern}`;
            }
        };
    }
    if (Type === 'Number') {
        return result => {
            if (isNaN(result))
                return false;
            if (typeof result === 'string') {
                result = result.includes('.') ? parseFloat(result) : parseInt(result);
            }
            if (result < MinValue || result > MaxValue) {
                return ConstraintDescription || `value must be between ${MinValue} and ${MaxValue}`;
            }
            return true;
        };
    }
    return result => {
        if (result.length < MinLength || result.length > MaxLength) {
            return ConstraintDescription || `value length must be between ${MinValue} and ${MaxValue}`;
        }
        return true;
    };
};
exports.getPromptForParameter = (parameter) => {
    const { Name, Label, Type, Description, AllowedValues, AllowedPattern, ConstraintDescription, MinValue, MaxValue, MinLength, MaxLength, } = parameter;
    const base = {
        name: Name,
        message: utils.getParameterDescription(parameter),
        validate: createValidatorForParameter(parameter),
    };
    if (AllowedValues) {
        return Object.assign({}, base, { type: 'rawlist', pageSize: Infinity, choices: AllowedValues.map(value => ({
                name: value,
                value,
            })) });
    }
    if (AllowedPattern) {
        const regex = new RegExp(AllowedPattern);
        return Object.assign({}, base, { type: 'input' });
    }
    return Object.assign({}, base, { type: 'input' });
};
const groupParameters = (template) => {
    const { Parameters, Metadata } = template;
    if (!Metadata) {
        const parameters = Object.keys(Parameters).map(name => (Object.assign({ Name: name, Label: utils.splitCamelCase(name) }, Parameters[name])));
        return [{ parameters }];
    }
    const { ParameterGroups, ParameterLabels } = Metadata['AWS::CloudFormation::Interface'];
    return ParameterGroups.map(group => ({
        name: group.Label.default,
        parameters: group.Parameters.map(name => (Object.assign({ Name: name, Label: ParameterLabels[name] }, Parameters[name])))
    }));
};
exports.promptParameters = ({ template, parameters }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    const prompts = exports.getPromptsForParameters(parameters);
    const answers = [];
    for (const prompt of prompts) {
        const key = prompt.name;
        const resp = yield inquirer_1.default.prompt([prompt]);
        const value = resp[key];
        answers.push({ ParameterKey: key, ParameterValue: value });
    }
    return answers;
});
exports.getBlanksForMissingParameters = ({ template, parameters }) => {
    const missing = utils.getMissingParameters({ template, parameters });
    return missing.map(m => ({
        ParameterKey: m.Name,
        ParameterValue: m.Default || '',
    }));
};
exports.promptMissingParameters = ({ conf, template, parameters }) => tslib_1.__awaiter(this, void 0, void 0, function* () {
    let org;
    try {
        ({ org } = yield conf.getEndpointInfo());
    }
    catch (err) {
        errors_1.default.rethrow(err, 'developer');
    }
    let missing = utils.getMissingParameters({ template, parameters });
    const added = [];
    if (org) {
        const orgName = missing.find(p => p.Name === 'OrgName');
        const orgDomain = missing.find(p => p.Name === 'OrgDomain');
        const orgLogo = missing.find(p => p.Name === 'OrgLogo');
        const added = [];
        if (orgName) {
            added.push({
                ParameterKey: orgName.Name,
                ParameterValue: org.name,
            });
        }
        if (orgDomain) {
            added.push({
                ParameterKey: orgDomain.Name,
                ParameterValue: org.domain,
            });
        }
        if (orgLogo) {
            added.push({
                ParameterKey: orgLogo.Name,
                ParameterValue: '',
            });
        }
    }
    missing = utils.getMissingParameters({ template, parameters: parameters.concat(added) });
    const answers = yield exports.promptParameters({
        template,
        parameters: missing.map(parameter => (Object.assign({}, parameter, { Description: `please remind me: ${utils.getParameterDescription(parameter)}` })))
    });
    return utils.sortParameters(added.concat(answers));
});
//# sourceMappingURL=restore.js.map