"use strict";
/*
 * Copyright © 2020 Atomist, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.planGoals = exports.determineGoals = exports.chooseAndSetGoals = void 0;
const configuration_1 = require("@atomist/automation-client/lib/configuration");
const string_1 = require("@atomist/automation-client/lib/internal/util/string");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const _ = require("lodash");
const addressChannels_1 = require("../../api/context/addressChannels");
const preferenceStore_1 = require("../../api/context/preferenceStore");
const skillContext_1 = require("../../api/context/skillContext");
const Goal_1 = require("../../api/goal/Goal");
const Goals_1 = require("../../api/goal/Goals");
const GoalWithFulfillment_1 = require("../../api/goal/GoalWithFulfillment");
const SdmGoalMessage_1 = require("../../api/goal/SdmGoalMessage");
const GoalImplementationMapper_1 = require("../../api/goal/support/GoalImplementationMapper");
const types_1 = require("../../typings/types");
const minimalClone_1 = require("./minimalClone");
const storeGoals_1 = require("./storeGoals");
/**
 * Choose and set goals for this push
 * @param {ChooseAndSetGoalsRules} rules: configuration for handling incoming pushes
 * @param parameters details of incoming request
 * @return {Promise<Goals | undefined>}
 */
async function chooseAndSetGoals(rules, parameters) {
    const { projectLoader, goalsListeners, goalSetter, implementationMapping, repoRefResolver, preferencesFactory } = rules;
    const { context, credentials, push } = parameters;
    const enrichGoal = !!rules.enrichGoal ? rules.enrichGoal : async (g) => g;
    const tagGoalSet = !!rules.tagGoalSet ? rules.tagGoalSet : async () => [];
    const id = repoRefResolver.repoRefFromPush(push);
    const addressChannels = addressChannels_1.addressChannelsFor(push.repo, context);
    const preferences = !!preferencesFactory ? preferencesFactory(parameters.context) : preferenceStore_1.NoPreferenceStore;
    const configuration = context.configuration;
    const goalSetId = string_1.guid();
    const { determinedGoals, goalsToSave, tags } = await determineGoals({ projectLoader, repoRefResolver, goalSetter, implementationMapping, enrichGoal, tagGoalSet }, {
        credentials, id, context, push, addressChannels, preferences, goalSetId, configuration,
    });
    if (goalsToSave.length > 0) {
        // First store the goals
        await Promise.all(goalsToSave.map(g => storeGoals_1.storeGoal(context, g)));
        // And then store the goalSet
        await storeGoals_1.storeGoalSet(context, storeGoals_1.constructGoalSet(context, goalSetId, determinedGoals.name, goalsToSave, tags, push));
    }
    // Let GoalSetListeners know even if we determined no goals.
    // This is not an error
    const gsi = {
        id,
        context,
        credentials,
        addressChannels,
        configuration,
        preferences,
        goalSetId,
        goalSetName: determinedGoals ? determinedGoals.name : undefined,
        goalSet: determinedGoals,
        push,
        skill: skillContext_1.createSkillContext(context),
    };
    await Promise.all(goalsListeners.map(l => l(gsi)));
    return determinedGoals;
}
exports.chooseAndSetGoals = chooseAndSetGoals;
async function determineGoals(rules, circumstances) {
    const { enrichGoal, projectLoader, repoRefResolver, goalSetter, implementationMapping, tagGoalSet } = rules;
    const { credentials, id, context, push, addressChannels, goalSetId, preferences, configuration } = circumstances;
    return projectLoader.doWithProject({
        credentials,
        id,
        context,
        readOnly: true,
        cloneOptions: minimalClone_1.minimalClone(push, { detachHead: true }),
    }, async (project) => {
        const pli = {
            project,
            credentials,
            id,
            push,
            context,
            addressChannels,
            configuration,
            preferences: preferences || preferenceStore_1.NoPreferenceStore,
            facts: {},
            skill: skillContext_1.createSkillContext(context),
        };
        const determinedGoals = await chooseGoalsForPushOnProject({ goalSetter }, pli);
        if (!determinedGoals) {
            return { determinedGoals: undefined, goalsToSave: [], tags: [] };
        }
        const goalsToSave = await sdmGoalsFromGoals(implementationMapping, push, repoRefResolver, pli, determinedGoals, goalSetId);
        // Enrich all goals before they get saved
        await Promise.all(goalsToSave.map(async (g1) => enrichGoal(g1, pli)));
        // Optain tags for the goal set
        let tags = [];
        if (!!tagGoalSet) {
            tags = (await tagGoalSet(goalsToSave, pli)) || [];
        }
        return { determinedGoals, goalsToSave, tags };
    });
}
exports.determineGoals = determineGoals;
async function sdmGoalsFromGoals(implementationMapping, push, repoRefResolver, pli, determinedGoals, goalSetId) {
    return Promise.all(determinedGoals.goals.map(async (g) => {
        const ge = storeGoals_1.constructSdmGoal(pli.context, {
            goalSet: determinedGoals.name,
            goalSetId,
            goal: g,
            state: (Goal_1.hasPreconditions(g) ? types_1.SdmGoalState.planned :
                (g.definition.preApprovalRequired ? types_1.SdmGoalState.waiting_for_pre_approval : types_1.SdmGoalState.requested)),
            id: pli.id,
            providerId: repoRefResolver.providerIdFromPush(pli.push),
            fulfillment: await fulfillment({ implementationMapping }, g, pli),
        });
        if (ge.state === types_1.SdmGoalState.requested) {
            const cbs = implementationMapping.findFulfillmentCallbackForGoal(Object.assign(Object.assign({}, ge), { push })) || [];
            let ng = Object.assign(Object.assign({}, ge), { push });
            for (const cb of cbs) {
                ng = await cb.callback(ng, pli);
            }
            return Object.assign(Object.assign({}, ge), { data: ng.data });
        }
        else {
            return ge;
        }
    }));
}
async function fulfillment(rules, g, inv) {
    const { implementationMapping } = rules;
    const plan = await implementationMapping.findFulfillmentByPush(g, inv);
    if (GoalImplementationMapper_1.isGoalImplementation(plan)) {
        return storeGoals_1.constructSdmGoalImplementation(plan, inv.configuration.name);
    }
    else if (GoalImplementationMapper_1.isGoalFulfillment(g.definition)) {
        const ff = g.definition.fulfillment;
        return {
            method: SdmGoalMessage_1.SdmGoalFulfillmentMethod.SideEffect,
            name: ff.name,
            registration: ff.registration,
        };
    }
    else if (GoalImplementationMapper_1.isGoalSideEffect(plan)) {
        return {
            method: SdmGoalMessage_1.SdmGoalFulfillmentMethod.SideEffect,
            name: plan.sideEffectName,
            registration: plan.registration || configuration_1.configurationValue("name"),
        };
    }
    else {
        return { method: SdmGoalMessage_1.SdmGoalFulfillmentMethod.Other, name: "unknown", registration: "unknown" };
    }
}
async function chooseGoalsForPushOnProject(rules, pi) {
    const { goalSetter } = rules;
    const { push, id } = pi;
    try {
        const determinedGoals = await goalSetter.mapping(pi);
        if (!determinedGoals) {
            logger_1.logger.info("No goals set by push '%s' to '%s/%s/%s'", push.after.sha, id.owner, id.repo, push.branch);
            return determinedGoals;
        }
        else {
            const filteredGoals = [];
            const plannedGoals = await planGoals(determinedGoals, pi);
            plannedGoals.goals.forEach(g => {
                if (g.dependsOn) {
                    const preConditions = g.dependsOn;
                    if (preConditions) {
                        const filteredPreConditions = preConditions.filter(pc => plannedGoals.goals.some(ag => ag.uniqueName === pc.uniqueName &&
                            ag.environment === pc.environment));
                        if (filteredPreConditions.length > 0) {
                            filteredGoals.push(new Goal_1.GoalWithPrecondition(g.definition, ...filteredPreConditions));
                        }
                        else {
                            filteredGoals.push(new Goal_1.Goal(g.definition));
                        }
                    }
                    else {
                        filteredGoals.push(g);
                    }
                }
                else {
                    filteredGoals.push(g);
                }
            });
            logger_1.logger.info("Goals for push '%s' on '%s/%s/%s' are '%s'", push.after.sha, id.owner, id.repo, push.branch, plannedGoals.name);
            return new Goals_1.Goals(plannedGoals.name, ...filteredGoals);
        }
    }
    catch (err) {
        logger_1.logger.error("Error determining goals: %s", err);
        logger_1.logger.error(err.stack);
        throw err;
    }
}
async function planGoals(goals, pli) {
    const allGoals = [...goals.goals];
    const names = [];
    for (const dg of goals.goals) {
        if (!!dg.plan) {
            let planResult = await dg.plan(pli, goals);
            if (!!planResult) {
                // Check if planResult is a PlannedGoal or PlannedGoals instance
                if (!_.some(planResult, v => !!v && !!v.goals)) {
                    planResult = { "#": { goals: planResult } };
                }
                const allNewGoals = [];
                const goalMapping = new Map();
                _.forEach(planResult, (planResultGoals, n) => {
                    names.push(n.replace(/_/g, " "));
                    const plannedGoals = [];
                    if (Array.isArray(planResultGoals.goals)) {
                        plannedGoals.push(...planResultGoals.goals);
                    }
                    else {
                        plannedGoals.push(planResultGoals.goals);
                    }
                    let previousGoals = [];
                    const newGoals = [];
                    plannedGoals.forEach(g => {
                        if (Array.isArray(g)) {
                            const gNewGoals = [];
                            for (const gg of g) {
                                const newGoal = createGoal(gg, dg, planResultGoals.dependsOn, allNewGoals.length + gNewGoals.length, previousGoals, goalMapping);
                                gNewGoals.push(newGoal);
                            }
                            allNewGoals.push(...gNewGoals);
                            newGoals.push(...gNewGoals);
                            previousGoals = [...gNewGoals];
                        }
                        else {
                            const newGoal = createGoal(g, dg, planResultGoals.dependsOn, allNewGoals.length, previousGoals, goalMapping);
                            allNewGoals.push(newGoal);
                            newGoals.push(newGoal);
                            previousGoals = [newGoal];
                        }
                    });
                    goalMapping.set(n, newGoals);
                });
                // Replace existing goal with new instances
                const ix = allGoals.findIndex(g => g.uniqueName === dg.uniqueName);
                allGoals.splice(ix, 1, ...allNewGoals);
                // Replace all preConditions that point back to the original goal with references to new goals
                allGoals.filter(Goal_1.hasPreconditions)
                    .filter(g => (g.dependsOn || []).some(gr => gr.uniqueName === dg.uniqueName))
                    .forEach(g => {
                    _.remove(g.dependsOn, gr => gr.uniqueName === dg.uniqueName);
                    g.dependsOn.push(...allNewGoals);
                });
            }
        }
    }
    return new Goals_1.Goals(goals.name, ...allGoals);
}
exports.planGoals = planGoals;
function createGoal(g, dg, preConditions, plannedGoalsCounter, previousGoals, goalMapping) {
    const uniqueName = `${dg.uniqueName}#sdm:${plannedGoalsCounter}`;
    const definition = _.merge({}, dg.definition, GoalWithFulfillment_1.getGoalDefinitionFrom(g.details, uniqueName));
    definition.uniqueName = uniqueName;
    definition.parameters = g.parameters;
    definition.fulfillment = g.fulfillment;
    const dependsOn = [];
    if (Goal_1.hasPreconditions(dg)) {
        dependsOn.push(...dg.dependsOn);
    }
    if (!!previousGoals) {
        dependsOn.push(...previousGoals);
    }
    if (!!preConditions) {
        if (Array.isArray(preConditions)) {
            dependsOn.push(..._.flatten(preConditions.map(d => goalMapping.get(d)).filter(d => !!d)));
        }
        else {
            dependsOn.push(...goalMapping.get(preConditions));
        }
    }
    return new Goal_1.GoalWithPrecondition(definition, ..._.uniqBy(dependsOn.filter(d => !!d), "uniqueName"));
}
//# sourceMappingURL=chooseAndSetGoals.js.map