// Copyright 2016-2019, Pulumi Corporation.
//
// 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.

package main

import (
	"fmt"
	"os"
	"sort"
	"strings"

	"github.com/pkg/errors"
	"github.com/pulumi/pulumi/pkg/v2/backend/display"
	"github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors"
	"github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil"
	"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
	"github.com/pulumi/pulumi/sdk/v2/python"
	"github.com/spf13/cobra"
	survey "gopkg.in/AlecAivazis/survey.v1"
	surveycore "gopkg.in/AlecAivazis/survey.v1/core"
)

type newPolicyArgs struct {
	dir               string
	force             bool
	generateOnly      bool
	interactive       bool
	offline           bool
	templateNameOrURL string
	yes               bool
}

func newPolicyNewCmd() *cobra.Command {
	args := newPolicyArgs{
		interactive: cmdutil.Interactive(),
	}

	cmd := &cobra.Command{
		Use:        "new [template|url]",
		SuggestFor: []string{"init", "create"},
		Short:      "Create a new Pulumi Policy Pack",
		Long: "Create a new Pulumi Policy Pack from a template.\n" +
			"\n" +
			"To create a Policy Pack from a specific template, pass the template name (such as `aws-typescript`\n" +
			"or `azure-python`).  If no template name is provided, a list of suggested templates will be presented\n" +
			"which can be selected interactively.\n" +
			"\n" +
			"Once you're done authoring the Policy Pack, you will need to publish the pack to your organization.\n" +
			"Only organization administrators can publish a Policy Pack.",
		Args: cmdutil.MaximumNArgs(1),
		Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
			if len(cliArgs) > 0 {
				args.templateNameOrURL = cliArgs[0]
			}
			return runNewPolicyPack(args)
		}),
	}

	cmd.PersistentFlags().StringVar(
		&args.dir, "dir", "",
		"The location to place the generated Policy Pack; if not specified, the current directory is used")
	cmd.PersistentFlags().BoolVarP(
		&args.force, "force", "f", false,
		"Forces content to be generated even if it would change existing files")
	cmd.PersistentFlags().BoolVarP(
		&args.generateOnly, "generate-only", "g", false,
		"Generate the Policy Pack only; do not install dependencies")
	cmd.PersistentFlags().BoolVarP(
		&args.offline, "offline", "o", false,
		"Use locally cached templates without making any network requests")

	return cmd
}

func runNewPolicyPack(args newPolicyArgs) error {
	if !args.interactive && !args.yes {
		return errors.New("--yes must be passed in to proceed when running in non-interactive mode")
	}

	// Prepare options.
	opts := display.Options{
		Color:         cmdutil.GetGlobalColorization(),
		IsInteractive: args.interactive,
	}

	// Get the current working directory.
	cwd, err := os.Getwd()
	if err != nil {
		return errors.Wrap(err, "getting the working directory")
	}

	// If dir was specified, ensure it exists and use it as the
	// current working directory.
	if args.dir != "" {
		cwd, err = useSpecifiedDir(args.dir)
		if err != nil {
			return err
		}
	}

	// Return an error if the directory isn't empty.
	if !args.force {
		if err = errorIfNotEmptyDirectory(cwd); err != nil {
			return err
		}
	}

	// Retrieve the templates-policy repo.
	repo, err := workspace.RetrieveTemplates(args.templateNameOrURL, args.offline, workspace.TemplateKindPolicyPack)
	if err != nil {
		return err
	}
	defer func() {
		contract.IgnoreError(repo.Delete())
	}()

	// List the templates from the repo.
	templates, err := repo.PolicyTemplates()
	if err != nil {
		return err
	}

	var template workspace.PolicyPackTemplate
	if len(templates) == 0 {
		return errors.New("no templates")
	} else if len(templates) == 1 {
		template = templates[0]
	} else {
		if template, err = choosePolicyPackTemplate(templates, opts); err != nil {
			return err
		}
	}

	// Do a dry run, if we're not forcing files to be overwritten.
	if !args.force {
		if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd, ""); err != nil {
			if os.IsNotExist(err) {
				return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL)
			}
			return err
		}
	}

	// Actually copy the files.
	if err = workspace.CopyTemplateFiles(template.Dir, cwd, args.force, "", ""); err != nil {
		if os.IsNotExist(err) {
			return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL)
		}
		return err
	}

	fmt.Println("Created Policy Pack!")

	proj, projPath, root, err := readPolicyProject()
	if err != nil {
		return err
	}

	// Install dependencies.
	if !args.generateOnly {
		if err := installPolicyPackDependencies(proj, projPath, root); err != nil {
			return err
		}
	}

	fmt.Println(
		opts.Color.Colorize(
			colors.BrightGreen+colors.Bold+"Your new Policy Pack is ready to go!"+colors.Reset) +
			" " + cmdutil.EmojiOr("✨", ""))
	fmt.Println()

	printPolicyPackNextSteps(proj, root, args.generateOnly, opts)

	return nil
}

func installPolicyPackDependencies(proj *workspace.PolicyPackProject, projPath, root string) error {
	// TODO[pulumi/pulumi#1334]: move to the language plugins so we don't have to hard code here.
	if strings.EqualFold(proj.Runtime.Name(), "nodejs") {
		if bin, err := nodeInstallDependencies(); err != nil {
			return errors.Wrapf(err, "`%s install` failed; rerun manually to try again.", bin)
		}
	} else if strings.EqualFold(proj.Runtime.Name(), "python") {
		if err := python.InstallDependencies(root, true /*showOutput*/, func(virtualenv string) error {
			// Save project with venv info.
			proj.Runtime.SetOption("virtualenv", virtualenv)
			if err := proj.Save(projPath); err != nil {
				return errors.Wrapf(err, "saving project at %s", projPath)
			}
			return nil
		}); err != nil {
			return err
		}
	}
	return nil
}

func printPolicyPackNextSteps(proj *workspace.PolicyPackProject, root string, generateOnly bool, opts display.Options) {
	var commands []string
	if generateOnly {
		// We didn't install dependencies, so instruct the user to do so.
		if strings.EqualFold(proj.Runtime.Name(), "nodejs") {
			commands = append(commands, "npm install")
		} else if strings.EqualFold(proj.Runtime.Name(), "python") {
			commands = append(commands, pythonCommands()...)
		}
	}

	if len(commands) == 1 {
		installMsg := fmt.Sprintf("To install dependencies for the Policy Pack, run `%s`", commands[0])
		installMsg = colors.Highlight(installMsg, commands[0], colors.BrightBlue+colors.Bold)
		fmt.Println(opts.Color.Colorize(installMsg))
		fmt.Println()
	}

	if len(commands) > 1 {
		fmt.Println("To install dependencies for the Policy Pack, run the following commands:")
		fmt.Println()
		for i, cmd := range commands {
			cmdColors := colors.BrightBlue + colors.Bold + cmd + colors.Reset
			fmt.Printf("   %d. %s\n", i+1, opts.Color.Colorize(cmdColors))
		}
		fmt.Println()
	}

	usageCommandPreambles :=
		[]string{"run the Policy Pack against a Pulumi program, in the directory of the Pulumi program run"}
	usageCommands := []string{fmt.Sprintf("pulumi up --policy-pack %s", root)}

	if strings.EqualFold(proj.Runtime.Name(), "nodejs") || strings.EqualFold(proj.Runtime.Name(), "python") {
		usageCommandPreambles = append(usageCommandPreambles, "publish the Policy Pack, run")
		usageCommands = append(usageCommands, "pulumi policy publish [org-name]")
	}

	contract.Assert(len(usageCommandPreambles) == len(usageCommands))

	if len(usageCommands) == 1 {
		usageMsg := fmt.Sprintf("Once you're done editing your Policy Pack, to %s `%s`", usageCommandPreambles[0],
			usageCommands[0])
		usageMsg = colors.Highlight(usageMsg, usageCommands[0], colors.BrightBlue+colors.Bold)
		fmt.Println(opts.Color.Colorize(usageMsg))
		fmt.Println()
	} else {
		fmt.Println("Once you're done editing your Policy Pack:")
		fmt.Println()
		for i, cmd := range usageCommands {
			cmdColors := colors.BrightBlue + colors.Bold + cmd + colors.Reset
			fmt.Printf("   * To %s `%s`\n", usageCommandPreambles[i], opts.Color.Colorize(cmdColors))
		}
		fmt.Println()
	}
}

// choosePolicyPackTemplate will prompt the user to choose amongst the available templates.
func choosePolicyPackTemplate(templates []workspace.PolicyPackTemplate,
	opts display.Options) (workspace.PolicyPackTemplate, error) {

	const chooseTemplateErr = "no template selected; please use `pulumi policy new` to choose one"
	if !opts.IsInteractive {
		return workspace.PolicyPackTemplate{}, errors.New(chooseTemplateErr)
	}

	// Customize the prompt a little bit (and disable color since it doesn't match our scheme).
	surveycore.DisableColor = true
	surveycore.QuestionIcon = ""
	surveycore.SelectFocusIcon = opts.Color.Colorize(colors.BrightGreen + ">" + colors.Reset)
	message := "\rPlease choose a template:"
	message = opts.Color.Colorize(colors.SpecPrompt + message + colors.Reset)

	options, optionToTemplateMap := policyTemplatesToOptionArrayAndMap(templates)

	cmdutil.EndKeypadTransmitMode()

	var option string
	if err := survey.AskOne(&survey.Select{
		Message:  message,
		Options:  options,
		PageSize: len(options),
	}, &option, nil); err != nil {
		return workspace.PolicyPackTemplate{}, errors.New(chooseTemplateErr)
	}
	return optionToTemplateMap[option], nil
}

// policyTemplatesToOptionArrayAndMap returns an array of option strings and a map of option strings to policy
// templates. Each option string is made up of the template name and description with some padding in between.
func policyTemplatesToOptionArrayAndMap(
	templates []workspace.PolicyPackTemplate) ([]string, map[string]workspace.PolicyPackTemplate) {

	// Find the longest name length. Used to add padding between the name and description.
	maxNameLength := 0
	for _, template := range templates {
		if len(template.Name) > maxNameLength {
			maxNameLength = len(template.Name)
		}
	}

	// Build the array and map.
	var options []string
	nameToTemplateMap := make(map[string]workspace.PolicyPackTemplate)
	for _, template := range templates {
		// Create the option string that combines the name, padding, and description.
		option := fmt.Sprintf(fmt.Sprintf("%%%ds    %%s", -maxNameLength), template.Name, template.Description)

		// Add it to the array and map.
		options = append(options, option)
		nameToTemplateMap[option] = template
	}
	sort.Strings(options)
	return options, nameToTemplateMap
}
