/*
 * 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 {
    MappedParameters,
    Secrets,
} from "@atomist/automation-client/lib/decorators";
import { Success } from "@atomist/automation-client/lib/HandlerResult";
import { metadataFromInstance } from "@atomist/automation-client/lib/internal/metadata/metadataReading";
import {
    populateParameters,
    populateValues,
} from "@atomist/automation-client/lib/internal/parameterPopulation";
import { CommandIncoming } from "@atomist/automation-client/lib/internal/transport/RequestProcessor";
import { guid } from "@atomist/automation-client/lib/internal/util/string";
import { CommandHandlerMetadata } from "@atomist/automation-client/lib/metadata/automationMetadata";
import { toFactory } from "@atomist/automation-client/lib/util/constructionUtils";
import {
    italic,
    url,
} from "@atomist/slack-messages";
import * as _ from "lodash";
import {
    commandHandlerRegistrationToCommand,
    CommandListenerExecutionInterruptError,
} from "../../../api-helper/machine/handlerRegistrations";
import {
    slackErrorMessage,
    slackInfoMessage,
} from "../../../api-helper/misc/slack/messages";
import { CommandListenerInvocation } from "../../../api/listener/CommandListener";
import { SoftwareDeliveryMachine } from "../../../api/machine/SoftwareDeliveryMachine";
import { CommandHandlerRegistration } from "../../../api/registration/CommandHandlerRegistration";
import { ParameterStyle } from "../../../api/registration/CommandRegistration";
import { ParametersObject } from "../../../api/registration/ParametersDefinition";
import {
    GitHubAppResourceProviderQuery,
    GitHubAppResourceProviderQueryVariables,
    OAuthToken,
    RepositoryByOwnerAndNameQuery,
    RepositoryByOwnerAndNameQueryVariables,
    RepositoryMappedChannels,
    RepositoryMappedChannelsQuery,
    RepositoryMappedChannelsQueryVariables,
    ResourceUserQuery,
    ResourceUserQueryVariables,
} from "../../../typings/types";
import {
    CreateGoals,
    DeliveryGoals,
} from "../configure";
import {
    CommandMaker,
    YamlCommandHandlerRegistration,
} from "./configureYaml";
import Repos = RepositoryMappedChannels.Repos;

export function decorateSoftwareDeliveryMachine<G extends DeliveryGoals>(sdm: SoftwareDeliveryMachine & { createGoals: CreateGoals<G> })
    : SoftwareDeliveryMachine & { createGoals: CreateGoals<G> } {
    const proxy = new Proxy<SoftwareDeliveryMachine & { createGoals: CreateGoals<G> }>(sdm, {
        get: (target, propKey) => {
            if (propKey === "addCommand") {
                return (...args) => {
                    const cmd = args[0] as CommandHandlerRegistration;
                    target[propKey]({
                        name: cmd.name,
                        ...mapCommand(cmd)(sdm) as YamlCommandHandlerRegistration,
                    });
                };
            } else {
                return target[propKey];
            }
        },
    });
    return proxy;
}

export function mapCommand(chr: CommandHandlerRegistration): CommandMaker {
    return sdm => {
        const ch = commandHandlerRegistrationToCommand(sdm, chr);
        const metadata = metadataFromInstance(toFactory(ch)()) as CommandHandlerMetadata;
        const parameterNames = metadata.parameters.filter(p => p.displayable === undefined || !!p.displayable).map(p => p.name);
        const mappedParameterNames = metadata.mapped_parameters.map(p => p.name);
        const allParameters = [...parameterNames, ...mappedParameterNames];

        const mapIntent = (intents: string[]) => {
            if (!!intents && intents.length > 0) {
                if (parameterNames.length > 0) {
                    return `^(?:${intents.map(i => i.replace(/ /g, "\\s+")).join("|")})(?:\\s+--(?:${allParameters.join("|")})=(?:'[^']*?'|"[^"]*?"|[\\w-]*?))*$`;
                } else {
                    return `^(?:${intents.map(i => i.replace(/ /g, "\\s+")).join("|")})$`;
                }
            } else {
                return undefined;
            }
        };

        return {

            name: metadata.name,
            description: metadata.description,
            intent: mapIntent(metadata.intent || []),
            tags: (metadata.tags || []).map(t => t.name),

            listener: async ci => {
                const instance = toFactory(ch)();
                const parametersInstance = instance.freshParametersInstance();
                const parameterDefinition: ParametersObject<any> = {};

                const intent = ((ci.context as any).trigger).raw_message;
                if (!!intent) {
                    const args = require("yargs-parser")(intent, { configuration: { "dot-notation": false } });
                    ((ci.context as any).trigger as CommandIncoming).parameters.push(..._.map(args, (v, k) => ({
                        name: k,
                        value: v,
                    })));
                }

                metadata.parameters.forEach(p => {
                    parameterDefinition[p.name] = {
                        ...p,
                        pattern: !!p.pattern ? new RegExp(p.pattern) : undefined,
                    };
                });

                const parameters = await ci.promptFor(parameterDefinition, {
                    autoSubmit: metadata.auto_submit,
                    parameterStyle: ParameterStyle.Dialog[metadata.question],
                });
                populateParameters(parametersInstance, metadata, _.map(parameters, (v, k) => ({
                    name: k,
                    value: v as any,
                })));
                populateValues(parametersInstance, metadata, ci.configuration);
                await populateSecrets(parametersInstance, metadata, ci);

                try {
                    const missing = await populateMappedParameters(parametersInstance, metadata, ci);
                    if (missing.length > 0) {
                        await ci.addressChannels(slackErrorMessage("Missing Mapped Parameters", missing.join("\n"), ci.context));
                        return Success;
                    }
                } catch (e) {
                    if (e instanceof MappedParamterError) {
                        await ci.addressChannels(slackErrorMessage(e.title, e.message, ci.context));
                        return Success;
                    } else {
                        throw e;
                    }
                }
                return instance.handle(ci.context, parametersInstance);
            },
        };
    };
}

async function populateSecrets(parameters: any, metadata: CommandHandlerMetadata, ci: CommandListenerInvocation): Promise<void> {
    for (const secret of (metadata.secrets || [])) {
        if (secret.uri.startsWith(Secrets.UserToken)) {
            const chatId = _.get(ci, "context.trigger.source.slack.user.id");
            if (!!chatId) {
                const resourceUser = await ci.context.graphClient.query<ResourceUserQuery, ResourceUserQueryVariables>({
                    name: "ResourceUser",
                    variables: {
                        id: chatId,
                    },
                });
                const credential: OAuthToken = _.get(resourceUser, "ChatId[0].person.gitHubId.credential");
                if (!!credential) {
                    const s = credential.secret;
                    _.update(parameters, secret.name, () => s);
                } else {
                    // Query GitHubAppResourceProvider to get the resource provider id
                    const provider = await ci.context.graphClient.query<GitHubAppResourceProviderQuery, GitHubAppResourceProviderQueryVariables>({
                        name: "GitHubAppResourceProvider",
                    });
                    if (!!provider?.GitHubAppResourceProvider[0]?.id) {
                        // Send message when there is a GitHubAppResourceProvider
                        const orgUrl = `https://api.atomist.com/v2/auth/teams/${ci.context.workspaceId}/resource-providers/${provider.GitHubAppResourceProvider[0].id}/token?state=${guid()}&redirect-uri=https://www.atomist.com/success.html`;
                        await ci.addressChannels(
                            slackInfoMessage(
                                "Link GitHub Account",
                                `In order to run this command Atomist needs to link your GitHub identity to your Slack user.\n\nPlease ${url(orgUrl, "click here")} to link your account.`));
                        throw new CommandListenerExecutionInterruptError("Sending token collection message");
                    }
                }
            }
        } else if (secret.uri === Secrets.OrgToken) {
            // TODO cd add this
        }
    }
}

async function populateMappedParameters(parameters: any, metadata: CommandHandlerMetadata, ci: CommandListenerInvocation): Promise<string[]> {
    const missing = [];
    for (const mp of (metadata.mapped_parameters || [])) {
        const value = ((ci.context as any).trigger as CommandIncoming).parameters.find(p => p.name === mp.name);
        if (value !== undefined) {
            _.update(parameters, mp.name, () => value.value);
        } else {
            switch (mp.uri) {
                case MappedParameters.GitHubOwner:
                case MappedParameters.GitHubOwnerWithUser:
                    const ownerDetails = await loadRepositoryDetailsFromChannel(ci, metadata);
                    _.update(parameters, mp.name, () => ownerDetails.owner);
                    break;
                case MappedParameters.GitHubRepository:
                case MappedParameters.GitHubAllRepositories:
                    const repoDetails = await loadRepositoryDetailsFromChannel(ci, metadata);
                    _.update(parameters, mp.name, () => repoDetails.name);
                    break;
                case MappedParameters.GitHubApiUrl:
                    const apiUrlDetails = await loadRepositoryDetailsFromChannel(ci, metadata);
                    _.update(parameters, mp.name, () => apiUrlDetails.apiUrl);
                    break;
                case MappedParameters.GitHubRepositoryProvider:
                    const providerIdDetails = await loadRepositoryDetailsFromChannel(ci, metadata);
                    _.update(parameters, mp.name, () => providerIdDetails.providerId);
                    break;
                case MappedParameters.GitHubUrl:
                    const urlDetails = await loadRepositoryDetailsFromChannel(ci, metadata);
                    _.update(parameters, mp.name, () => urlDetails.url);
                    break;

                case MappedParameters.GitHubUserLogin:
                    const chatId = _.get(ci, "context.trigger.source.slack.user.id");
                    const resourceUser = await ci.context.graphClient.query<ResourceUserQuery, ResourceUserQueryVariables>({
                        name: "ResourceUser",
                        variables: {
                            id: chatId,
                        },
                    });
                    _.update(parameters, mp.name, () => _.get(resourceUser, "ChatId[0].person.gitHubId.login"));
                    break;
                case MappedParameters.SlackChannel:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.channel.id"));
                    break;
                case MappedParameters.SlackChannelName:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.channel.name"));
                    break;
                case MappedParameters.SlackChannelType:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.channel.type"));
                    break;
                case MappedParameters.SlackUser:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.user.id"));
                    break;
                case MappedParameters.SlackUserName:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.user.name"));
                    break;
                case MappedParameters.SlackTeam:
                    _.update(parameters, mp.name, () => _.get(ci, "context.trigger.source.slack.team.id"));
                    break;
            }
        }

        if (parameters[mp.name] === undefined && mp.required === true) {
            missing.push(`Required mapped parameter '${mp.name}' missing`);
        }
    }
    return missing;
}

async function loadRepositoryDetailsFromChannel(ci: CommandListenerInvocation,
                                                metadata: CommandHandlerMetadata)
    : Promise<{ name?: string, owner?: string, providerId?: string, providerType?: string, apiUrl?: string, url?: string }> {

    // If owner and repo was provided, find the remaining mapped parameters from that
    const incomingParameters = ((ci.context as any).trigger as CommandIncoming).parameters;
    const ownerMp = metadata.mapped_parameters.find(mp => mp.uri === MappedParameters.GitHubOwner);
    const repoMp = metadata.mapped_parameters.find(mp => mp.uri === MappedParameters.GitHubRepository);
    const ownerParameter = !!ownerMp ? incomingParameters.find(p => p.name === ownerMp.name) : undefined;
    const repoParameter = !!repoMp ? incomingParameters.find(p => p.name === repoMp?.name) : undefined;
    if (!!ownerMp && !!repoMp && !!ownerParameter && !!repoParameter) {
        const repo = await ci.context.graphClient.query<RepositoryByOwnerAndNameQuery, RepositoryByOwnerAndNameQueryVariables>({
            name: "RepositoryByOwnerAndName",
            variables: {
                owner: ownerParameter.value,
                name: repoParameter.value,
            },
        });
        if (repo?.Repo?.length === 1) {
            return {
                name: repo?.Repo[0]?.name,
                owner: repo?.Repo[0]?.owner,
                providerId: repo?.Repo[0]?.org.provider.providerId,
                providerType: repo?.Repo[0]?.org.provider.providerType,
                apiUrl: repo?.Repo[0]?.org.provider.apiUrl,
                url: repo?.Repo[0]?.org.provider.url,
            };
        }
    }

    // Check if we want a list of repositories
    if (metadata.mapped_parameters.some(mp => mp.uri === MappedParameters.GitHubAllRepositories
        || mp.uri === MappedParameters.GitHubOwnerWithUser)) {
        const parameters = await ci.promptFor<{ repo_slug: string }>({
            repo_slug: {
                description: "Slug of repository",
                displayName: "Repository (owner/repository)",
                pattern: /^\S+\/\S+$/,
            },
        }, {});
        const repo = await ci.context.graphClient.query<RepositoryByOwnerAndNameQuery, RepositoryByOwnerAndNameQueryVariables>({
            name: "RepositoryByOwnerAndName",
            variables: {
                owner: parameters.repo_slug.split("/")[0],
                name: parameters.repo_slug.split("/")[1],
            },
        });
        if (!repo?.Repo[0]) {
            throw new MappedParamterError("Repository", `Repository ${italic(parameters.repo_slug)} could not be found.`);
        }
        return {
            name: repo?.Repo[0]?.name,
            owner: repo?.Repo[0]?.owner,
            providerId: repo?.Repo[0]?.org.provider.providerId,
            providerType: repo?.Repo[0]?.org.provider.providerType,
            apiUrl: repo?.Repo[0]?.org.provider.apiUrl,
            url: repo?.Repo[0]?.org.provider.url,
        };
    } else {
        const channelId = _.get(ci, "context.trigger.source.slack.channel.id");
        const channels = await ci.context.graphClient.query<RepositoryMappedChannelsQuery, RepositoryMappedChannelsQueryVariables>({
            name: "RepositoryMappedChannels",
            variables: {
                id: channelId,
            },
        });
        const repos: Repos[] = _.get(channels, "ChatChannel[0].repos") || [];
        if (!!repos) {
            if (repos.length === 1) {
                return {
                    name: repos[0].name,
                    owner: repos[0].owner,
                    providerId: repos[0].org.provider.providerId,
                    providerType: repos[0].org.provider.providerType,
                    apiUrl: repos[0].org.provider.apiUrl,
                    url: repos[0].org.provider.url,
                };
            } else if (repos.length > 0) {
                const parameters = await ci.promptFor<{ repo_id: string }>({
                    repo_id: {
                        displayName: "Repository",
                        type: {
                            kind: "single",
                            options: repos.map(r => ({ description: `${r.owner}/${r.name}`, value: r.id })),
                        },
                    },
                }, {});
                const repo = repos.find(r => r.id === parameters.repo_id);
                return {
                    name: repo.name,
                    owner: repo.owner,
                    providerId: repo.org.provider.providerId,
                    providerType: repo.org.provider.providerType,
                    apiUrl: repo.org.provider.apiUrl,
                    url: repo.org.provider.url,
                };
            } else {
                const parameters = await ci.promptFor<{ repo_slug: string }>({
                    repo_slug: {
                        displayName: "Repository (owner/repository)",
                        description: "Slug of repository",
                        pattern: /^\S+\/\S+$/,
                    },
                }, {});
                const repo = await ci.context.graphClient.query<RepositoryByOwnerAndNameQuery, RepositoryByOwnerAndNameQueryVariables>({
                    name: "RepositoryByOwnerAndName",
                    variables: {
                        owner: parameters.repo_slug.split("/")[0],
                        name: parameters.repo_slug.split("/")[1],
                    },
                });
                if (!repo?.Repo[0]) {
                    throw new MappedParamterError("Repository", `Repository ${italic(parameters.repo_slug)} could not be found.`);
                }
                return {
                    name: repo?.Repo[0]?.name,
                    owner: repo?.Repo[0]?.owner,
                    providerId: repo?.Repo[0]?.org.provider.providerId,
                    providerType: repo?.Repo[0]?.org.provider.providerType,
                    apiUrl: repo?.Repo[0]?.org.provider.apiUrl,
                    url: repo?.Repo[0]?.org.provider.url,
                };
            }
        }
    }
    return {};
}

class MappedParamterError extends Error {
    constructor(public readonly title: string, msg: string) {
        super(msg);
    }
}
