// Copyright 2016-2018, 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 backend

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"strings"

	survey "github.com/AlecAivazis/survey/v2"
	surveycore "github.com/AlecAivazis/survey/v2/core"

	"github.com/pulumi/pulumi/pkg/v3/backend/display"
	sdkDisplay "github.com/pulumi/pulumi/pkg/v3/display"
	"github.com/pulumi/pulumi/pkg/v3/engine"
	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)

// ApplierOptions is a bag of configuration settings for an Applier.
type ApplierOptions struct {
	// DryRun indicates if the update should not change any resource state and instead just preview changes.
	DryRun bool
	// ShowLink indicates if a link to the update persisted result can be displayed.
	ShowLink bool
}

// Applier applies the changes specified by this update operation against the target stack.
type Applier func(ctx context.Context, kind apitype.UpdateKind, stack Stack, op UpdateOperation,
	opts ApplierOptions, events chan<- engine.Event) (*deploy.Plan, sdkDisplay.ResourceChanges, error)

func ActionLabel(kind apitype.UpdateKind, dryRun bool) string {
	v := updateTextMap[kind]
	contract.Assertf(v.previewText != "", "preview text for %q cannot be empty", kind)
	contract.Assertf(v.text != "", "text for %q cannot be empty", kind)

	if dryRun {
		return "Previewing " + v.previewText
	}

	return v.text
}

var updateTextMap = map[apitype.UpdateKind]struct {
	previewText string
	text        string
}{
	apitype.PreviewUpdate:        {"update", "Previewing"},
	apitype.UpdateUpdate:         {"update", "Updating"},
	apitype.RefreshUpdate:        {"refresh", "Refreshing"},
	apitype.DestroyUpdate:        {"destroy", "Destroying"},
	apitype.StackImportUpdate:    {"stack import", "Importing"},
	apitype.ResourceImportUpdate: {"import", "Importing"},
}

type response string

const (
	yes     response = "yes"
	no      response = "no"
	details response = "details"
)

func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack,
	op UpdateOperation, apply Applier,
) (*deploy.Plan, sdkDisplay.ResourceChanges, error) {
	// create a channel to hear about the update events from the engine. this will be used so that
	// we can build up the diff display in case the user asks to see the details of the diff

	// Note that eventsChannel is not closed in a `defer`. It is generally unsafe to do so, since defers run during
	// panics and we can't know whether or not we were in the middle of writing to this channel when the panic occurred.
	//
	// Instead of using a `defer`, we manually close `eventsChannel` on every exit of this function.
	eventsChannel := make(chan engine.Event)

	var events []engine.Event
	go func() {
		// Pull out relevant events we will want to display in the confirmation below.
		for e := range eventsChannel {
			// Don't include internal events in the confirmation stats.
			if e.Internal() {
				continue
			}
			if e.Type == engine.ResourcePreEvent ||
				e.Type == engine.ResourceOutputsEvent ||
				e.Type == engine.PolicyRemediationEvent ||
				e.Type == engine.SummaryEvent {
				events = append(events, e)
			}
		}
	}()

	// Perform the update operations, passing true for dryRun, so that we get a preview.
	// We perform the preview (DryRun), but don't display the cloud link since the
	// thing the user cares about would be the link to the actual update if they
	// confirm the prompt.
	opts := ApplierOptions{
		DryRun:   true,
		ShowLink: true,
	}

	plan, changes, err := apply(ctx, kind, stack, op, opts, eventsChannel)
	if err != nil {
		close(eventsChannel)
		return plan, changes, err
	}

	// If there are no changes, or we're auto-approving or just previewing, we can skip the confirmation prompt.
	if op.Opts.AutoApprove || kind == apitype.PreviewUpdate {
		close(eventsChannel)
		// If we're running in experimental mode then return the plan generated, else discard it. The user may
		// be explicitly setting a plan but that's handled higher up the call stack.
		if !op.Opts.Engine.Experimental {
			plan = nil
		}
		return plan, changes, nil
	}

	stats := computeUpdateStats(events)

	infoPrefix := "\b" + op.Opts.Display.Color.Colorize(colors.SpecWarning+"info: "+colors.Reset)
	if kind != apitype.UpdateUpdate {
		// If not an update, we can skip displaying warnings
	} else if stats.numNonStackResources == 0 {
		// This is an update and there are no resources being CREATED
		fmt.Print(infoPrefix, "There are no resources in your stack (other than the stack resource).\n\n")
	}

	// Warn user if an update is going to leave untracked resources in the environment.
	if (kind == apitype.UpdateUpdate || kind == apitype.PreviewUpdate || kind == apitype.DestroyUpdate) &&
		len(stats.retainedResources) != 0 {
		fmt.Printf(
			"%sThis update will leave %d resource(s) untracked in your environment:\n",
			infoPrefix, len(stats.retainedResources))
		for _, res := range stats.retainedResources {
			urn := res.URN
			fmt.Printf("    - %s %s\n", urn.Type().DisplayName(), urn.Name())
		}
		fmt.Print("\n")
	}

	// Otherwise, ensure the user wants to proceed.
	plan, err = confirmBeforeUpdating(kind, stack, events, plan, op.Opts)
	close(eventsChannel)
	return plan, changes, err
}

// confirmBeforeUpdating asks the user whether to proceed. A nil error means yes.
func confirmBeforeUpdating(kind apitype.UpdateKind, stack Stack,
	events []engine.Event, plan *deploy.Plan, opts UpdateOptions,
) (*deploy.Plan, error) {
	for {
		var response string

		surveycore.DisableColor = true
		surveyIcons := survey.WithIcons(func(icons *survey.IconSet) {
			icons.Question = survey.Icon{}
			icons.SelectFocus = survey.Icon{Text: opts.Display.Color.Colorize(colors.BrightGreen + ">" + colors.Reset)}
		})

		choices := []string{string(yes), string(no)}

		// For non-previews, we can also offer a detailed summary.
		if !opts.SkipPreview {
			choices = append(choices, string(details))
		}

		var previewWarning string
		if opts.SkipPreview {
			previewWarning = colors.SpecWarning + " without a preview" + colors.Bold
		}

		// Create a prompt. If this is a refresh, we'll add some extra text so it's clear we aren't updating resources.
		prompt := "\b" + opts.Display.Color.Colorize(
			colors.SpecPrompt+fmt.Sprintf("Do you want to perform this %s%s?",
				updateTextMap[kind].previewText, previewWarning)+colors.Reset)
		if kind == apitype.RefreshUpdate {
			prompt += "\n" +
				opts.Display.Color.Colorize(colors.SpecImportant+
					"No resources will be modified as part of this refresh; just your stack's state will be.\n"+
					colors.Reset)
		}

		// Now prompt the user for a yes, no, or details, and then proceed accordingly.
		if err := survey.AskOne(&survey.Select{
			Message: prompt,
			Options: choices,
			Default: string(no),
		}, &response, surveyIcons); err != nil {
			return nil, fmt.Errorf("confirmation cancelled, not proceeding with the %s: %w", kind, err)
		}

		if response == string(no) {
			return nil, result.FprintBailf(os.Stdout, "confirmation declined, not proceeding with the %s", kind)
		}

		if response == string(yes) {
			// If we're in experimental mode always use the plan
			if opts.Engine.Experimental {
				return plan, nil
			}
			return nil, nil
		}

		if response == string(details) {
			diff := createDiff(kind, events, opts.Display)
			_, err := os.Stdout.WriteString(diff + "\n")
			contract.IgnoreError(err)
			continue
		}
	}
}

func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind, stack Stack,
	op UpdateOperation, apply Applier,
) (sdkDisplay.ResourceChanges, error) {
	// Preview the operation to the user and ask them if they want to proceed.

	if !op.Opts.SkipPreview {
		// We want to run the preview with the given plan and then run the full update with the initial plan as well,
		// but because plans are mutated as they're checked we need to clone it here.
		// We want to use the original plan because a program could be non-deterministic and have a plan of
		// operations P0, the update preview could return P1, and then the actual update could run P2, were P1 < P2 < P0.
		var originalPlan *deploy.Plan
		if op.Opts.Engine.Plan != nil {
			originalPlan = op.Opts.Engine.Plan.Clone()
		}

		plan, changes, err := PreviewThenPrompt(ctx, kind, stack, op, apply)
		if err != nil || kind == apitype.PreviewUpdate {
			return changes, err
		}

		// If we had an original plan use it, else if prompt said to use the plan from Preview then use the
		// newly generated plan
		if originalPlan != nil {
			op.Opts.Engine.Plan = originalPlan
		} else if plan != nil {
			op.Opts.Engine.Plan = plan
		} else {
			op.Opts.Engine.Plan = nil
		}
	}

	// Perform the change (!DryRun) and show the cloud link to the result.
	// We don't care about the events it issues, so just pass a nil channel along.
	opts := ApplierOptions{
		DryRun:   false,
		ShowLink: true,
	}
	// No need to generate a plan at this stage, there's no way for the system or user to extract the plan
	// after here.
	op.Opts.Engine.GeneratePlan = false
	_, changes, res := apply(ctx, kind, stack, op, opts, nil /*events*/)
	return changes, res
}

type updateStats struct {
	numNonStackResources int
	retainedResources    []engine.StepEventMetadata
}

func computeUpdateStats(events []engine.Event) updateStats {
	var stats updateStats

	for _, e := range events {
		if e.Type != engine.ResourcePreEvent {
			continue
		}
		p, ok := e.Payload().(engine.ResourcePreEventPayload)

		if !ok {
			continue
		}

		if p.Metadata.Type.String() != "pulumi:pulumi:Stack" {
			stats.numNonStackResources++
		}

		// Track deleted resources that are retained.
		switch p.Metadata.Op {
		case deploy.OpDelete, deploy.OpReplace:
			if old := p.Metadata.Old; old != nil && old.State != nil && old.State.RetainOnDelete {
				stats.retainedResources = append(stats.retainedResources, p.Metadata)
			}
		}
	}
	return stats
}

func createDiff(updateKind apitype.UpdateKind, events []engine.Event, displayOpts display.Options) string {
	buff := &bytes.Buffer{}

	seen := make(map[resource.URN]engine.StepEventMetadata)
	displayOpts.SummaryDiff = true

	outputEventsDiff := make([]string, 0)
	remediationEventsDiff := make([]string, 0)
	for _, e := range events {
		if e.Type == engine.SummaryEvent {
			continue
		}

		msg := display.RenderDiffEvent(e, seen, displayOpts)
		if msg == "" {
			continue
		}

		// Keep track of output and remediation events separately, since we print them after the
		// ordinary resource diff information.
		if e.Type == engine.ResourceOutputsEvent {
			outputEventsDiff = append(outputEventsDiff, msg)
			continue
		} else if e.Type == engine.PolicyRemediationEvent {
			remediationEventsDiff = append(remediationEventsDiff, msg)
			continue
		}

		_, err := buff.WriteString(msg)
		contract.IgnoreError(err)
	}

	// Print resource outputs next.
	if len(outputEventsDiff) > 0 {
		_, err := buff.WriteString("\n")
		contract.IgnoreError(err)
		for _, msg := range outputEventsDiff {
			_, err := buff.WriteString(msg)
			contract.IgnoreError(err)
		}
	}

	// Print policy remediations last.
	if len(remediationEventsDiff) > 0 {
		_, err := buff.WriteString(displayOpts.Color.Colorize(
			fmt.Sprintf("\n%s  Policy Remediations:%s\n", colors.SpecHeadline, colors.Reset)))
		contract.IgnoreError(err)
		for _, msg := range remediationEventsDiff {
			_, err := buff.WriteString(msg)
			contract.IgnoreError(err)
		}
	}

	return strings.TrimSpace(buff.String())
}