// 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 cmd

import (
	"sort"
	"strconv"
	"strings"

	"github.com/dustin/go-humanize"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"

	"github.com/pulumi/pulumi/pkg/backend"
	"github.com/pulumi/pulumi/pkg/backend/display"
	"github.com/pulumi/pulumi/pkg/backend/httpstate"
	"github.com/pulumi/pulumi/pkg/backend/state"
	"github.com/pulumi/pulumi/pkg/util/cmdutil"
	"github.com/pulumi/pulumi/pkg/workspace"
)

func newStackLsCmd() *cobra.Command {
	var jsonOut bool
	var allStacks bool
	var orgFilter string
	var projFilter string
	var tagFilter string

	cmd := &cobra.Command{
		Use:   "ls",
		Short: "List stacks",
		Long: "List stacks\n" +
			"\n" +
			"This command lists stacks. By default only stacks with the same project name as the\n" +
			"current workspace will be returned. By passing --all, all stacks you have access to\n" +
			"will be listed.\n" +
			"\n" +
			"Results may be further filtered by passing additional flags. Tag filters may include\n" +
			"the tag name as well as the tag value, separated by an equals sign. For example\n" +
			"'environment=production' or just 'gcp:project'.",
		Args: cmdutil.NoArgs,
		Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
			// Build up the stack filters. We do not support accepting empty strings as filters
			// from command-line arguments, though the API technically supports it.
			strPtrIfSet := func(s string) *string {
				if s != "" {
					return &s
				}
				return nil
			}
			filter := backend.ListStacksFilter{
				Organization: strPtrIfSet(orgFilter),
				Project:      strPtrIfSet(projFilter),
			}
			if tagFilter != "" {
				tagName, tagValue := parseTagFilter(tagFilter)
				filter.TagName = &tagName
				filter.TagValue = tagValue
			}

			// If --all is not specified, default to filtering to just the current project.
			if !allStacks && projFilter == "" {
				// Ensure we are in a project; if not, we will fail.
				projPath, err := workspace.DetectProjectPath()
				if err != nil {
					return errors.Wrapf(err, "could not detect current project")
				} else if projPath == "" {
					return errors.New("no Pulumi.yaml found; please run this command in a project directory")
				}

				proj, err := workspace.LoadProject(projPath)
				if err != nil {
					return errors.Wrap(err, "could not load current project")
				}
				projName := string(proj.Name)
				filter.Project = &projName
			}

			// Get the current backend.
			b, err := currentBackend(display.Options{Color: cmdutil.GetGlobalColorization()})
			if err != nil {
				return err
			}

			// Get the current stack so we can print a '*' next to it.
			var current string
			if s, _ := state.CurrentStack(commandContext(), b); s != nil {
				// If we couldn't figure out the current stack, just don't print the '*' later on instead of failing.
				current = s.Ref().String()
			}

			// List all of the stacks available.
			stackSummaries, err := b.ListStacks(commandContext(), filter)
			if err != nil {
				return err
			}
			// Sort by stack name.
			sort.Slice(stackSummaries, func(i, j int) bool {
				return stackSummaries[i].Name().String() < stackSummaries[j].Name().String()
			})

			if jsonOut {
				return formatStackSummariesJSON(b, current, stackSummaries)
			}

			return formatStackSummariesConsole(b, current, stackSummaries)
		}),
	}
	cmd.PersistentFlags().BoolVarP(
		&jsonOut, "json", "j", false, "Emit output as JSON")

	cmd.PersistentFlags().BoolVarP(
		&allStacks, "all", "a", false, "List all stacks instead of just stacks for the current project")

	cmd.PersistentFlags().StringVarP(
		&orgFilter, "organization", "o", "", "Filter returned stacks to those in a specific organization")
	cmd.PersistentFlags().StringVarP(
		&projFilter, "project", "p", "", "Filter returned stacks to those with a specific project name")
	cmd.PersistentFlags().StringVarP(
		&tagFilter, "tag", "t", "", "Filter returned stacks to those in a specific tag (tag-name or tag-name=tag-value)")

	return cmd
}

// parseTagFilter parses a tag filter into its separate name and value parts, separatedby an equal sign.
// If no "value" is provided, the second return parameter will be `nil`. Either the tag name or value can
// be omitted. e.g. "=x" returns ("", "x") and "=" returns ("", "").
func parseTagFilter(t string) (string, *string) {
	parts := strings.SplitN(t, "=", 2)
	if len(parts) == 1 {
		return parts[0], nil
	}
	return parts[0], &parts[1]
}

// stackSummaryJSON is the shape of the --json output of this command. When --json is passed, we print an array
// of stackSummaryJSON objects.  While we can add fields to this structure in the future, we should not change
// existing fields.
type stackSummaryJSON struct {
	Name             string `json:"name"`
	Current          bool   `json:"current"`
	LastUpdate       string `json:"lastUpdate,omitempty"`
	UpdateInProgress bool   `json:"updateInProgress"`
	ResourceCount    *int   `json:"resourceCount,omitempty"`
	URL              string `json:"url,omitempty"`
}

func formatStackSummariesJSON(b backend.Backend, currentStack string, stackSummaries []backend.StackSummary) error {
	output := make([]stackSummaryJSON, len(stackSummaries))
	for idx, summary := range stackSummaries {
		summaryJSON := stackSummaryJSON{
			Name:          summary.Name().String(),
			ResourceCount: summary.ResourceCount(),
			Current:       summary.Name().String() == currentStack,
		}

		if summary.LastUpdate() != nil {
			if isUpdateInProgress(summary) {
				summaryJSON.UpdateInProgress = true
			} else {
				summaryJSON.LastUpdate = summary.LastUpdate().UTC().Format(timeFormat)
			}
		}

		if httpBackend, ok := b.(httpstate.Backend); ok {
			if consoleURL, err := httpBackend.StackConsoleURL(summary.Name()); err == nil {
				summaryJSON.URL = consoleURL
			}
		}

		output[idx] = summaryJSON
	}

	return printJSON(output)
}

func formatStackSummariesConsole(b backend.Backend, currentStack string, stackSummaries []backend.StackSummary) error {
	_, showURLColumn := b.(httpstate.Backend)

	// Header string and formatting options to align columns.
	headers := []string{"NAME", "LAST UPDATE", "RESOURCE COUNT"}
	if showURLColumn {
		headers = append(headers, "URL")
	}

	rows := []cmdutil.TableRow{}

	for _, summary := range stackSummaries {
		const none = "n/a"

		// Name column
		name := summary.Name().String()
		if name == currentStack {
			name += "*"
		}

		// Last update column
		lastUpdate := none
		if stackLastUpdate := summary.LastUpdate(); stackLastUpdate != nil {
			if isUpdateInProgress(summary) {
				lastUpdate = "in progress"
			} else {
				lastUpdate = humanize.Time(*stackLastUpdate)
			}
		}

		// ResourceCount column
		resourceCount := none
		if stackResourceCount := summary.ResourceCount(); stackResourceCount != nil {
			resourceCount = strconv.Itoa(*stackResourceCount)
		}

		// Render the columns.
		columns := []string{name, lastUpdate, resourceCount}
		if showURLColumn {
			url := none
			if httpBackend, ok := b.(httpstate.Backend); ok {
				if consoleURL, err := httpBackend.StackConsoleURL(summary.Name()); err == nil {
					url = consoleURL
				}
			}

			columns = append(columns, url)
		}

		rows = append(rows, cmdutil.TableRow{Columns: columns})
	}

	cmdutil.PrintTable(cmdutil.Table{
		Headers: headers,
		Rows:    rows,
	})

	return nil
}

func isUpdateInProgress(u backend.StackSummary) bool {
	// When an update is in progress the last update time is set to zero.
	return u.LastUpdate() != nil && u.LastUpdate().Unix() == 0
}
