/*
 * 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.
 */

import { HandlerContext } from "@atomist/automation-client/lib/HandlerContext";
import { guid } from "@atomist/automation-client/lib/internal/util/string";
import { RepoRef } from "@atomist/automation-client/lib/operations/common/RepoId";
import {
    AnyProjectEditor,
    EditResult,
    ProjectEditor,
    toEditor,
} from "@atomist/automation-client/lib/operations/edit/projectEditor";
import { isLocalProject } from "@atomist/automation-client/lib/project/local/LocalProject";
import { Project } from "@atomist/automation-client/lib/project/Project";
import { buttonForCommand } from "@atomist/automation-client/lib/spi/message/MessageClient";
import { logger } from "@atomist/automation-client/lib/util/logger";
import { bold, codeBlock, italic } from "@atomist/slack-messages";
import { CodeTransformRegistration } from "../../../api/registration/CodeTransformRegistration";
import { DryRunParameter, MsgIdParameter } from "../../machine/handlerRegistrations";
import { execPromise } from "../../misc/child_process";
import { slackErrorMessage, slackInfoMessage, slackSuccessMessage } from "../../misc/slack/messages";
import { confirmEditedness } from "./confirmEditedness";

/**
 * Wrap this editor to make it chatty, so it responds to Slack if there's nothing to do.
 * It also honors the dryRun parameter flag to just capture the git diff and send it back to Slack instead
 * of pushing changes to Git.
 */
export function chattyDryRunAwareEditor(
    ctr: CodeTransformRegistration<any>,
    underlyingEditor: AnyProjectEditor,
): ProjectEditor {
    return async (project: Project, context: HandlerContext, params: any) => {
        const id = project.id;
        const editorName = ctr.name;
        try {
            await sendDryRunUpdateMessage(editorName, id, params, context, ctr);

            const tentativeEditResult = await toEditor(underlyingEditor)(project, context, params);
            const editResult = await confirmEditedness(tentativeEditResult);

            // Figure out if this CodeTransform is running in dryRun mode; if so capture git diff and don't push changes
            if (!editResult.edited) {
                if (!editResult.success) {
                    await sendFailureMessage(editorName, id, params, editResult, context, ctr);
                } else {
                    await sendNoUpdateMessage(editorName, id, params, context, ctr);
                }
                return { target: project, edited: false, success: false };
            } else if (isDryRun(params)) {
                if (!isLocalProject(project)) {
                    const message = `Project is not a local project, cannot diff`;
                    logger.warn(message);
                    return { target: project, edited: false, success: true };
                }
                let diff = "";
                try {
                    const gitDiffResult = await execPromise("git", ["diff"], { cwd: project.baseDir });
                    diff = gitDiffResult.stdout;
                } catch (err) {
                    logger.error(`Error diffing project: %s`, err.message);
                    diff = `Error obtaining \`git diff\`:\n\n${codeBlock(err.message)}`;
                }
                await sendDryRunSummaryMessage(editorName, id, diff, params, context, ctr);
                return { target: project, edited: false, success: true };
            } else {
                await sendSuccessMessage(editorName, id, params, context, ctr);
            }
            return editResult;
        } catch (err) {
            await context.messageClient.respond(
                slackErrorMessage(
                    `Code Transform${isDryRun(params) ? " (dry run)" : ""}`,
                    `Code transform ${italic(editorName)} failed while changing ${bold(slug(id))}:\n\n${codeBlock(
                        err.message,
                    )}`,
                    context,
                ),
                { id: params[MsgIdParameter.name] },
            );
            logger.warn("Code Transform error acting on %j: %s", project.id, err);
            return { target: project, edited: false, success: false };
        }
    };
}

function isDryRun(params: any): boolean {
    return !!params && params[DryRunParameter.name] === true;
}

function slug(id: RepoRef): string {
    return `${id.owner}/${id.repo}/${id.branch}`;
}

function isChatty(ctr: CodeTransformRegistration): boolean {
    if (ctr.chatty !== undefined) {
        return ctr.chatty;
    } else {
        return true;
    }
}

async function sendDryRunUpdateMessage(
    codeTransformName: string,
    id: RepoRef,
    params: any,
    ctx: HandlerContext,
    ctr: CodeTransformRegistration,
): Promise<void> {
    if (isChatty(ctr) && !!params[MsgIdParameter.name]) {
        await ctx.messageClient.respond(
            slackInfoMessage(
                "Code Transform",
                `Applying code transform ${italic(codeTransformName)} to ${bold(slug(id))}`,
            ),
            { id: params[MsgIdParameter.name] },
        );
    }
}

async function sendFailureMessage(
    codeTransformName: string,
    id: RepoRef,
    params: any,
    editResult: EditResult,
    ctx: HandlerContext,
    ctr: CodeTransformRegistration,
): Promise<void> {
    if (isChatty(ctr)) {
        await ctx.messageClient.respond(
            slackErrorMessage(
                `Code Transform${isDryRun(params) ? " (dry run)" : ""}`,
                `Code transform ${italic(codeTransformName)} failed while changing ${bold(slug(id))}:\n\n${
                    editResult.error ? codeBlock(editResult.error.message) : ""
                }`,
                ctx,
            ),
            { id: params[MsgIdParameter.name] },
        );
    }
}

async function sendNoUpdateMessage(
    codeTransformName: string,
    id: RepoRef,
    params: any,
    ctx: HandlerContext,
    ctr: CodeTransformRegistration,
): Promise<void> {
    if (isChatty(ctr)) {
        await ctx.messageClient.respond(
            slackInfoMessage(
                `Code Transform${isDryRun(params) ? " (dry run)" : ""}`,
                `Code transform ${italic(codeTransformName)} made no changes to ${bold(slug(id))}`,
            ),
            { id: params[MsgIdParameter.name] },
        );
    }
}

async function sendSuccessMessage(
    codeTransformName: string,
    id: RepoRef,
    params: any,
    ctx: HandlerContext,
    ctr: CodeTransformRegistration,
): Promise<void> {
    if (isChatty(ctr)) {
        const msgId = params[MsgIdParameter.name];
        await ctx.messageClient.respond(
            slackSuccessMessage(
                "Code Transform",
                `Successfully applied code transform ${italic(codeTransformName)} to ${bold(slug(id))}`,
            ),
            { id: msgId },
        );
    }
}

async function sendDryRunSummaryMessage(
    codeTransformName: string,
    id: RepoRef,
    diff: string,
    params: any,
    ctx: HandlerContext,
    ctr: CodeTransformRegistration,
): Promise<void> {
    const msgId = params[MsgIdParameter.name] || guid();
    const applyAction = {
        actions: [
            buttonForCommand({ text: "Apply Transform" }, codeTransformName, {
                // reuse the other parameters, but set the dryRun flag to false and pin to one repo
                ...params,
                "dry-run": false,
                "msgId": msgId,
                "targets.sha": params.targets.sha,
                "targets.owner": id.owner,
                "targets.repo": id.repo,
            }),
        ],
    };
    await ctx.messageClient.respond(
        slackInfoMessage(
            `Code Transform (dry run)`,
            `Code transform ${italic(codeTransformName)} would make the following changes to ${bold(slug(id))}:
${codeBlock(diff)}
`,
            applyAction,
        ),
        { id: msgId },
    );
}
