package commands

import (
	"context"
	"encoding/json"
	stderrors "errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"text/tabwriter"
	"time"
	"unicode/utf8"

	"github.com/argoproj/gitops-engine/pkg/health"
	"github.com/argoproj/gitops-engine/pkg/sync/common"
	"github.com/argoproj/gitops-engine/pkg/sync/hook"
	"github.com/argoproj/gitops-engine/pkg/sync/ignore"
	"github.com/argoproj/gitops-engine/pkg/utils/kube"
	grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
	"github.com/mattn/go-isatty"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	k8swatch "k8s.io/apimachinery/pkg/watch"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/yaml"

	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
	cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
	argocommon "github.com/argoproj/argo-cd/v3/common"
	"github.com/argoproj/argo-cd/v3/controller"
	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
	"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"

	resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"

	clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
	projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
	"github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
	argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	repoapiclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
	"github.com/argoproj/argo-cd/v3/reposerver/repository"
	"github.com/argoproj/argo-cd/v3/util/argo"
	argodiff "github.com/argoproj/argo-cd/v3/util/argo/diff"
	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
	"github.com/argoproj/argo-cd/v3/util/cli"
	"github.com/argoproj/argo-cd/v3/util/errors"
	"github.com/argoproj/argo-cd/v3/util/git"
	"github.com/argoproj/argo-cd/v3/util/grpc"
	utilio "github.com/argoproj/argo-cd/v3/util/io"
	logutils "github.com/argoproj/argo-cd/v3/util/log"
	"github.com/argoproj/argo-cd/v3/util/manifeststream"
	"github.com/argoproj/argo-cd/v3/util/templates"
	"github.com/argoproj/argo-cd/v3/util/text/label"
)

// NewApplicationCommand returns a new instance of an `argocd app` command
func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	command := &cobra.Command{
		Use:   "app",
		Short: "Manage applications",
		Example: `  # List all the applications.
  argocd app list

  # Get the details of a application
  argocd app get my-app

  # Set an override parameter
  argocd app set my-app -p image.tag=v1.0.1`,
		Run: func(c *cobra.Command, args []string) {
			c.HelpFunc()(c, args)
			os.Exit(1)
		},
	}
	command.AddCommand(NewApplicationCreateCommand(clientOpts))
	command.AddCommand(NewApplicationGetCommand(clientOpts))
	command.AddCommand(NewApplicationDiffCommand(clientOpts))
	command.AddCommand(NewApplicationSetCommand(clientOpts))
	command.AddCommand(NewApplicationUnsetCommand(clientOpts))
	command.AddCommand(NewApplicationSyncCommand(clientOpts))
	command.AddCommand(NewApplicationHistoryCommand(clientOpts))
	command.AddCommand(NewApplicationRollbackCommand(clientOpts))
	command.AddCommand(NewApplicationListCommand(clientOpts))
	command.AddCommand(NewApplicationDeleteCommand(clientOpts))
	command.AddCommand(NewApplicationWaitCommand(clientOpts))
	command.AddCommand(NewApplicationManifestsCommand(clientOpts))
	command.AddCommand(NewApplicationTerminateOpCommand(clientOpts))
	command.AddCommand(NewApplicationEditCommand(clientOpts))
	command.AddCommand(NewApplicationPatchCommand(clientOpts))
	command.AddCommand(NewApplicationGetResourceCommand(clientOpts))
	command.AddCommand(NewApplicationPatchResourceCommand(clientOpts))
	command.AddCommand(NewApplicationDeleteResourceCommand(clientOpts))
	command.AddCommand(NewApplicationResourceActionsCommand(clientOpts))
	command.AddCommand(NewApplicationListResourcesCommand(clientOpts))
	command.AddCommand(NewApplicationLogsCommand(clientOpts))
	command.AddCommand(NewApplicationAddSourceCommand(clientOpts))
	command.AddCommand(NewApplicationRemoveSourceCommand(clientOpts))
	command.AddCommand(NewApplicationConfirmDeletionCommand(clientOpts))
	return command
}

type watchOpts struct {
	sync      bool
	health    bool
	operation bool
	suspended bool
	degraded  bool
	delete    bool
	hydrated  bool
}

// NewApplicationCreateCommand returns a new instance of an `argocd app create` command
func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		appOpts      cmdutil.AppOptions
		fileURL      string
		appName      string
		upsert       bool
		labels       []string
		annotations  []string
		setFinalizer bool
		appNamespace string
	)
	command := &cobra.Command{
		Use:   "create APPNAME",
		Short: "Create an application",
		Example: `  # Create a directory app
  argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --directory-recurse

  # Create a Jsonnet app
  argocd app create jsonnet-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path jsonnet-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --jsonnet-ext-str replicas=2

  # Create a Helm app
  argocd app create helm-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path helm-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --helm-set replicaCount=2

  # Create a Helm app from a Helm repo
  argocd app create nginx-ingress --repo https://charts.helm.sh/stable --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc

  # Create a Kustomize app
  argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image quay.io/argoprojlabs/argocd-e2e-container:0.1

  # Create a MultiSource app while yaml file contains an application with multiple sources
  argocd app create guestbook --file <path-to-yaml-file>

  # Create a app using a custom tool:
  argocd app create kasane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			argocdClient := headless.NewClientOrDie(clientOpts, c)
			apps, err := cmdutil.ConstructApps(fileURL, appName, labels, annotations, args, appOpts, c.Flags())
			errors.CheckError(err)

			for _, app := range apps {
				if app.Name == "" {
					c.HelpFunc()(c, args)
					os.Exit(1)
				}
				if appNamespace != "" {
					app.Namespace = appNamespace
				}
				if setFinalizer {
					app.Finalizers = append(app.Finalizers, argoappv1.ResourcesFinalizerName)
				}
				conn, appIf := argocdClient.NewApplicationClientOrDie()
				defer utilio.Close(conn)
				appCreateRequest := application.ApplicationCreateRequest{
					Application: app,
					Upsert:      &upsert,
					Validate:    &appOpts.Validate,
				}

				// Get app before creating to see if it is being updated or no change
				existing, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
				unwrappedError := grpc.UnwrapGRPCStatus(err).Code()
				// As part of the fix for CVE-2022-41354, the API will return Permission Denied when an app does not exist.
				if unwrappedError != codes.NotFound && unwrappedError != codes.PermissionDenied {
					errors.CheckError(err)
				}

				created, err := appIf.Create(ctx, &appCreateRequest)
				errors.CheckError(err)

				var action string
				switch {
				case existing == nil:
					action = "created"
				case !hasAppChanged(existing, created, upsert):
					action = "unchanged"
				default:
					action = "updated"
				}

				fmt.Printf("application '%s' %s\n", created.Name, action)
			}
		},
	}
	command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)")
	command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override application with the same name even if supplied application spec is different from existing spec")
	command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
	command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app")
	command.Flags().StringArrayVarP(&annotations, "annotations", "", []string{}, "Set metadata annotations (e.g. example=value)")
	command.Flags().BoolVar(&setFinalizer, "set-finalizer", false, "Sets deletion finalizer on the application, application resources will be cascaded on deletion")
	// Only complete files with appropriate extension.
	err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
	if err != nil {
		log.Fatal(err)
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace where the application will be created in")
	cmdutil.AddAppFlags(command, &appOpts)
	return command
}

// getInfos converts a list of string key=value pairs to a list of Info objects.
func getInfos(infos []string) []*argoappv1.Info {
	mapInfos, err := label.Parse(infos)
	errors.CheckError(err)
	sliceInfos := make([]*argoappv1.Info, len(mapInfos))
	i := 0
	for key, element := range mapInfos {
		sliceInfos[i] = &argoappv1.Info{Name: key, Value: element}
		i++
	}
	return sliceInfos
}

func getRefreshType(refresh bool, hardRefresh bool) *string {
	if hardRefresh {
		refreshType := string(argoappv1.RefreshTypeHard)
		return &refreshType
	}

	if refresh {
		refreshType := string(argoappv1.RefreshTypeNormal)
		return &refreshType
	}

	return nil
}

func hasAppChanged(appReq, appRes *argoappv1.Application, upsert bool) bool {
	// upsert==false, no change occurred from create command
	if !upsert {
		return false
	}

	// If no project, assume default project
	if appReq.Spec.Project == "" {
		appReq.Spec.Project = "default"
	}
	// Server will return nils for empty labels, annotations, finalizers
	if len(appReq.Labels) == 0 {
		appReq.Labels = nil
	}
	if len(appReq.Annotations) == 0 {
		appReq.Annotations = nil
	}
	if len(appReq.Finalizers) == 0 {
		appReq.Finalizers = nil
	}

	if reflect.DeepEqual(appRes.Spec, appReq.Spec) &&
		reflect.DeepEqual(appRes.Labels, appReq.Labels) &&
		reflect.DeepEqual(appRes.Annotations, appReq.Annotations) &&
		reflect.DeepEqual(appRes.Finalizers, appReq.Finalizers) {
		return false
	}

	return true
}

func parentChildDetails(ctx context.Context, appIf application.ApplicationServiceClient, appName string, appNs string) (map[string]argoappv1.ResourceNode, map[string][]string, map[string]struct{}) {
	mapUIDToNode := make(map[string]argoappv1.ResourceNode)
	mapParentToChild := make(map[string][]string)
	parentNode := make(map[string]struct{})

	resourceTree, err := appIf.ResourceTree(ctx, &application.ResourcesQuery{Name: &appName, AppNamespace: &appNs, ApplicationName: &appName})
	errors.CheckError(err)

	for _, node := range resourceTree.Nodes {
		mapUIDToNode[node.UID] = node

		if len(node.ParentRefs) > 0 {
			_, ok := mapParentToChild[node.ParentRefs[0].UID]
			if !ok {
				var temp []string
				mapParentToChild[node.ParentRefs[0].UID] = temp
			}
			mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID)
		} else {
			parentNode[node.UID] = struct{}{}
		}
	}
	return mapUIDToNode, mapParentToChild, parentNode
}

func printHeader(ctx context.Context, acdClient argocdclient.Client, app *argoappv1.Application, windows *argoappv1.SyncWindows, showOperation bool, showParams bool, sourcePosition int) {
	appURL := getAppURL(ctx, acdClient, app.Name)
	printAppSummaryTable(app, appURL, windows)

	if len(app.Status.Conditions) > 0 {
		fmt.Println()
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		printAppConditions(w, app)
		_ = w.Flush()
		fmt.Println()
	}
	if showOperation && app.Status.OperationState != nil {
		fmt.Println()
		printOperationResult(app.Status.OperationState)
	}
	if showParams {
		printParams(app, sourcePosition)
	}
}

// getSourceNameToPositionMap returns a map of source name to position
func getSourceNameToPositionMap(app *argoappv1.Application) map[string]int64 {
	sourceNameToPosition := make(map[string]int64)
	for i, s := range app.Spec.Sources {
		if s.Name != "" {
			sourceNameToPosition[s.Name] = int64(i + 1)
		}
	}
	return sourceNameToPosition
}

// NewApplicationGetCommand returns a new instance of an `argocd app get` command
func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		refresh        bool
		hardRefresh    bool
		output         string
		timeout        uint
		showParams     bool
		showOperation  bool
		appNamespace   string
		sourcePosition int
		sourceName     string
	)
	command := &cobra.Command{
		Use:   "get APPNAME",
		Short: "Get application details",
		Example: templates.Examples(`
  # Get basic details about the application "my-app" in wide format
  argocd app get my-app -o wide

  # Get detailed information about the application "my-app" in YAML format
  argocd app get my-app -o yaml

  # Get details of the application "my-app" in JSON format
  argocd get my-app -o json

  # Get application details and include information about the current operation
  argocd app get my-app --show-operation

  # Show application parameters and overrides
  argocd app get my-app --show-params

  # Show application parameters and overrides for a source at position 1 under spec.sources of app my-app
  argocd app get my-app --show-params --source-position 1

  # Show application parameters and overrides for a source named "test"
  argocd app get my-app --show-params --source-name test

  # Refresh application data when retrieving
  argocd app get my-app --refresh

  # Perform a hard refresh, including refreshing application data and target manifests cache
  argocd app get my-app --hard-refresh

  # Get application details and display them in a tree format
  argocd app get my-app --output tree

  # Get application details and display them in a detailed tree format
  argocd app get my-app --output tree=detailed
  		`),

		Run: func(c *cobra.Command, args []string) {
			ctx, cancel := context.WithCancel(c.Context())
			defer cancel()
			if len(args) == 0 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			acdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)

			if timeout != 0 {
				time.AfterFunc(time.Duration(timeout)*time.Second, func() {
					if ctx.Err() != nil {
						fmt.Println("Timeout function: context already cancelled:", ctx.Err())
					} else {
						fmt.Println("Timeout function: cancelling context manually")
						cancel()
					}
				})
			}
			getAppStateWithRetry := func() (*argoappv1.Application, error) {
				type getResponse struct {
					app *argoappv1.Application
					err error
				}

				ch := make(chan getResponse, 1)

				go func() {
					app, err := appIf.Get(ctx, &application.ApplicationQuery{
						Name:         &appName,
						Refresh:      getRefreshType(refresh, hardRefresh),
						AppNamespace: &appNs,
					})
					ch <- getResponse{app: app, err: err}
				}()

				select {
				case result := <-ch:
					return result.app, result.err
				case <-ctx.Done():
					// Timeout occurred, try again without refresh flag
					// Create new context for retry request
					ctx := context.Background()
					app, err := appIf.Get(ctx, &application.ApplicationQuery{
						Name:         &appName,
						AppNamespace: &appNs,
					})
					return app, err
				}
			}

			app, err := getAppStateWithRetry()
			errors.CheckError(err)

			if ctx.Err() != nil {
				ctx = context.Background() // Reset context for subsequent requests
			}
			if sourceName != "" && sourcePosition != -1 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
			}

			if sourceName != "" {
				sourceNameToPosition := getSourceNameToPositionMap(app)
				pos, ok := sourceNameToPosition[sourceName]
				if !ok {
					log.Fatalf("Unknown source name '%s'", sourceName)
				}
				sourcePosition = int(pos)
			}

			// check for source position if --show-params is set
			if app.Spec.HasMultipleSources() && showParams {
				if sourcePosition <= 0 {
					errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
				}
				if len(app.Spec.GetSources()) < sourcePosition {
					errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
				}
			}

			pConn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
			defer utilio.Close(pConn)
			proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: app.Spec.Project})
			errors.CheckError(err)

			windows := proj.Spec.SyncWindows.Matches(app)

			switch output {
			case "yaml", "json":
				err := PrintResource(app, output)
				errors.CheckError(err)
			case "wide", "":
				printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
				if len(app.Status.Resources) > 0 {
					fmt.Println()
					w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
					printAppResources(w, app)
					_ = w.Flush()
				}
			case "tree":
				printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
				mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appName, appNs)
				if len(mapUIDToNode) > 0 {
					fmt.Println()
					printTreeView(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
				}
			case "tree=detailed":
				printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
				mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appName, appNs)
				if len(mapUIDToNode) > 0 {
					fmt.Println()
					printTreeViewDetailed(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
				}
			default:
				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
			}
		},
	}
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree")
	command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
	command.Flags().BoolVar(&showOperation, "show-operation", false, "Show application operation")
	command.Flags().BoolVar(&showParams, "show-params", false, "Show application parameters and overrides")
	command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
	command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only get application from namespace")
	command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
	command.Flags().StringVar(&sourceName, "source-name", "", "Name of the source from the list of sources of the app.")
	return command
}

// NewApplicationLogsCommand returns logs of application pods
func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		group        string
		kind         string
		namespace    string
		resourceName string
		follow       bool
		tail         int64
		sinceSeconds int64
		untilTime    string
		filter       string
		container    string
		previous     bool
		matchCase    bool
	)
	command := &cobra.Command{
		Use:   "logs APPNAME",
		Short: "Get logs of application pods",
		Example: templates.Examples(`
  # Get logs of pods associated with the application "my-app"
  argocd app logs my-app

  # Get logs of pods associated with the application "my-app" in a specific resource group
  argocd app logs my-app --group my-group

  # Get logs of pods associated with the application "my-app" in a specific resource kind
  argocd app logs my-app --kind my-kind

  # Get logs of pods associated with the application "my-app" in a specific namespace
  argocd app logs my-app --namespace my-namespace

  # Get logs of pods associated with the application "my-app" for a specific resource name
  argocd app logs my-app --name my-resource

  # Stream logs in real-time for the application "my-app"
  argocd app logs my-app -f

  # Get the last N lines of logs for the application "my-app"
  argocd app logs my-app --tail 100

  # Get logs since a specified number of seconds ago
  argocd app logs my-app --since-seconds 3600

  # Get logs until a specified time (format: "2023-10-10T15:30:00Z")
  argocd app logs my-app --until-time "2023-10-10T15:30:00Z"

  # Filter logs to show only those containing a specific string
  argocd app logs my-app --filter "error"

  # Filter logs to show only those containing a specific string and match case
  argocd app logs my-app --filter "error" --match-case

  # Get logs for a specific container within the pods
  argocd app logs my-app -c my-container

  # Get previously terminated container logs
  argocd app logs my-app -p
  		`),

		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) == 0 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			acdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)
			appName, appNs := argo.ParseFromQualifiedName(args[0], "")

			retry := true
			for retry {
				retry = false
				stream, err := appIf.PodLogs(ctx, &application.ApplicationPodLogsQuery{
					Name:         &appName,
					Group:        &group,
					Namespace:    ptr.To(namespace),
					Kind:         &kind,
					ResourceName: &resourceName,
					Follow:       ptr.To(follow),
					TailLines:    ptr.To(tail),
					SinceSeconds: ptr.To(sinceSeconds),
					UntilTime:    &untilTime,
					Filter:       &filter,
					MatchCase:    ptr.To(matchCase),
					Container:    ptr.To(container),
					Previous:     ptr.To(previous),
					AppNamespace: &appNs,
				})
				if err != nil {
					log.Fatalf("failed to get pod logs: %v", err)
				}
				for {
					msg, err := stream.Recv()
					if err != nil {
						if stderrors.Is(err, io.EOF) {
							return
						}
						st, ok := status.FromError(err)
						if !ok {
							log.Fatalf("stream read failed: %v", err)
						}
						if st.Code() == codes.Unavailable && follow {
							retry = true
							sinceSeconds = 1
							break
						}
						log.Fatalf("stream read failed: %v", err)
					}
					if msg.GetLast() {
						return
					}
					fmt.Println(msg.GetContent())
				} // Done with receive message
			} // Done with retry
		},
	}

	command.Flags().StringVar(&group, "group", "", "Resource group")
	command.Flags().StringVar(&kind, "kind", "", "Resource kind")
	command.Flags().StringVar(&namespace, "namespace", "", "Resource namespace")
	command.Flags().StringVar(&resourceName, "name", "", "Resource name")
	command.Flags().BoolVarP(&follow, "follow", "f", false, "Specify if the logs should be streamed")
	command.Flags().Int64Var(&tail, "tail", 0, "The number of lines from the end of the logs to show")
	command.Flags().Int64Var(&sinceSeconds, "since-seconds", 0, "A relative time in seconds before the current time from which to show logs")
	command.Flags().StringVar(&untilTime, "until-time", "", "Show logs until this time")
	command.Flags().StringVar(&filter, "filter", "", "Show logs contain this string")
	command.Flags().StringVarP(&container, "container", "c", "", "Optional container name")
	command.Flags().BoolVarP(&previous, "previous", "p", false, "Specify if the previously terminated container logs should be returned")
	command.Flags().BoolVarP(&matchCase, "match-case", "m", false, "Specify if the filter should be case-sensitive")

	return command
}

func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *argoappv1.SyncWindows) {
	fmt.Printf(printOpFmtStr, "Name:", app.QualifiedName())
	fmt.Printf(printOpFmtStr, "Project:", app.Spec.GetProject())
	fmt.Printf(printOpFmtStr, "Server:", getServer(app))
	fmt.Printf(printOpFmtStr, "Namespace:", app.Spec.Destination.Namespace)
	fmt.Printf(printOpFmtStr, "URL:", appURL)
	if !app.Spec.HasMultipleSources() {
		fmt.Println("Source:")
	} else {
		fmt.Println("Sources:")
	}
	for _, source := range app.Spec.GetSources() {
		printAppSourceDetails(&source)
	}
	var wds []string
	var status string
	var allow, deny, inactiveAllows bool
	if windows.HasWindows() {
		active, err := windows.Active()
		if err == nil && active.HasWindows() {
			for _, w := range *active {
				if w.Kind == "deny" {
					deny = true
				} else {
					allow = true
				}
			}
		}
		inactiveAllowWindows, err := windows.InactiveAllows()
		if err == nil && inactiveAllowWindows.HasWindows() {
			inactiveAllows = true
		}

		if deny || !deny && !allow && inactiveAllows {
			s, err := windows.CanSync(true)
			if err == nil && s {
				status = "Manual Allowed"
			} else {
				status = "Sync Denied"
			}
		} else {
			status = "Sync Allowed"
		}
		for _, w := range *windows {
			s := w.Kind + ":" + w.Schedule + ":" + w.Duration
			wds = append(wds, s)
		}
	} else {
		status = "Sync Allowed"
	}
	fmt.Printf(printOpFmtStr, "SyncWindow:", status)
	if len(wds) > 0 {
		fmt.Printf(printOpFmtStr, "Assigned Windows:", strings.Join(wds, ","))
	}

	var syncPolicy string
	if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.IsAutomatedSyncEnabled() {
		syncPolicy = "Automated"
		if app.Spec.SyncPolicy.Automated.Prune {
			syncPolicy += " (Prune)"
		}
	} else {
		syncPolicy = "Manual"
	}
	fmt.Printf(printOpFmtStr, "Sync Policy:", syncPolicy)
	syncStatusStr := string(app.Status.Sync.Status)
	switch app.Status.Sync.Status {
	case argoappv1.SyncStatusCodeSynced:
		syncStatusStr += " to " + app.Spec.GetSource().TargetRevision
	case argoappv1.SyncStatusCodeOutOfSync:
		syncStatusStr += " from " + app.Spec.GetSource().TargetRevision
	}
	if !git.IsCommitSHA(app.Spec.GetSource().TargetRevision) && !git.IsTruncatedCommitSHA(app.Spec.GetSource().TargetRevision) && len(app.Status.Sync.Revision) > 7 {
		syncStatusStr += fmt.Sprintf(" (%s)", app.Status.Sync.Revision[0:7])
	}
	fmt.Printf(printOpFmtStr, "Sync Status:", syncStatusStr)
	healthStr := string(app.Status.Health.Status)
	fmt.Printf(printOpFmtStr, "Health Status:", healthStr)
}

func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
	fmt.Printf(printOpFmtStr, "- Repo:", appSrc.RepoURL)
	fmt.Printf(printOpFmtStr, "  Target:", appSrc.TargetRevision)
	if appSrc.Path != "" {
		fmt.Printf(printOpFmtStr, "  Path:", appSrc.Path)
	}
	if appSrc.IsRef() {
		fmt.Printf(printOpFmtStr, "  Ref:", appSrc.Ref)
	}
	if appSrc.Helm != nil && len(appSrc.Helm.ValueFiles) > 0 {
		fmt.Printf(printOpFmtStr, "  Helm Values:", strings.Join(appSrc.Helm.ValueFiles, ","))
	}
	if appSrc.Kustomize != nil && appSrc.Kustomize.NamePrefix != "" {
		fmt.Printf(printOpFmtStr, "  Name Prefix:", appSrc.Kustomize.NamePrefix)
	}
}

func printAppConditions(w io.Writer, app *argoappv1.Application) {
	_, _ = fmt.Fprintf(w, "CONDITION\tMESSAGE\tLAST TRANSITION\n")
	for _, item := range app.Status.Conditions {
		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", item.Type, item.Message, item.LastTransitionTime)
	}
}

// appURLDefault returns the default URL of an application
func appURLDefault(acdClient argocdclient.Client, appName string) string {
	var scheme string
	opts := acdClient.ClientOptions()
	server := opts.ServerAddr
	if opts.PlainText {
		scheme = "http"
	} else {
		scheme = "https"
		if strings.HasSuffix(opts.ServerAddr, ":443") {
			server = server[0 : len(server)-4]
		}
	}
	return fmt.Sprintf("%s://%s/applications/%s", scheme, server, appName)
}

// getAppURL returns the URL of an application
func getAppURL(ctx context.Context, acdClient argocdclient.Client, appName string) string {
	conn, settingsIf := acdClient.NewSettingsClientOrDie()
	defer utilio.Close(conn)
	argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
	errors.CheckError(err)

	if argoSettings.URL != "" {
		return fmt.Sprintf("%s/applications/%s", argoSettings.URL, appName)
	}
	return appURLDefault(acdClient, appName)
}

func truncateString(str string, num int) string {
	bnoden := str
	if utf8.RuneCountInString(str) > num {
		if num > 3 {
			num -= 3
		}
		bnoden = string([]rune(str)[0:num]) + "..."
	}
	return bnoden
}

// printParams prints parameters and overrides
func printParams(app *argoappv1.Application, sourcePosition int) {
	var source *argoappv1.ApplicationSource

	if app.Spec.HasMultipleSources() {
		// Get the source by the sourcePosition whose params you'd like to print
		source = app.Spec.GetSourcePtrByPosition(sourcePosition)
		if source == nil {
			source = &argoappv1.ApplicationSource{}
		}
	} else {
		src := app.Spec.GetSource()
		source = &src
	}

	if source.Helm != nil {
		printHelmParams(source.Helm)
	}
}

func printHelmParams(helm *argoappv1.ApplicationSourceHelm) {
	paramLenLimit := 80
	fmt.Println()
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	if helm != nil {
		fmt.Println()
		_, _ = fmt.Fprintf(w, "NAME\tVALUE\n")
		for _, p := range helm.Parameters {
			_, _ = fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
		}
	}
	_ = w.Flush()
}

func getServer(app *argoappv1.Application) string {
	if app.Spec.Destination.Server == "" {
		return app.Spec.Destination.Name
	}

	return app.Spec.Destination.Server
}

// NewApplicationSetCommand returns a new instance of an `argocd app set` command
func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		appOpts        cmdutil.AppOptions
		appNamespace   string
		sourcePosition int
		sourceName     string
	)
	command := &cobra.Command{
		Use:   "set APPNAME",
		Short: "Set application parameters",
		Example: templates.Examples(`
  # Set application parameters for the application "my-app"
  argocd app set my-app --parameter key1=value1 --parameter key2=value2

  # Set and validate application parameters for "my-app"
  argocd app set my-app --parameter key1=value1 --parameter key2=value2 --validate

  # Set and override application parameters for a source at position 1 under spec.sources of app my-app. source-position starts at 1.
  argocd app set my-app --source-position 1 --repo https://github.com/argoproj/argocd-example-apps.git

  # Set and override application parameters for a source named "test" under spec.sources of app my-app.
  argocd app set my-app --source-name test --repo https://github.com/argoproj/argocd-example-apps.git

  # Set application parameters and specify the namespace
  argocd app set my-app --parameter key1=value1 --parameter key2=value2 --namespace my-namespace
  		`),

		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			argocdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := argocdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs})
			errors.CheckError(err)

			sourceName = appOpts.SourceName
			if sourceName != "" && sourcePosition != -1 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
			}

			if sourceName != "" {
				sourceNameToPosition := getSourceNameToPositionMap(app)
				pos, ok := sourceNameToPosition[sourceName]
				if !ok {
					log.Fatalf("Unknown source name '%s'", sourceName)
				}
				sourcePosition = int(pos)
			}

			if app.Spec.HasMultipleSources() {
				if sourcePosition <= 0 {
					errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
				}
				if len(app.Spec.GetSources()) < sourcePosition {
					errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
				}
			}

			visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)
			if visited == 0 {
				log.Error("Please set at least one option to update")
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			setParameterOverrides(app, appOpts.Parameters, sourcePosition)
			_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
				Name:         &app.Name,
				Spec:         &app.Spec,
				Validate:     &appOpts.Validate,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)
		},
	}
	cmdutil.AddAppFlags(command, &appOpts)
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Set application parameters in namespace")
	command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
	return command
}

// unsetOpts describe what to unset in an Application.
type unsetOpts struct {
	namePrefix              bool
	nameSuffix              bool
	kustomizeVersion        bool
	kustomizeNamespace      bool
	kustomizeImages         []string
	kustomizeReplicas       []string
	ignoreMissingComponents bool
	parameters              []string
	valuesFiles             []string
	valuesLiteral           bool
	ignoreMissingValueFiles bool
	pluginEnvs              []string
	passCredentials         bool
	ref                     bool
}

// IsZero returns true when the Application options for kustomize are considered empty
func (o *unsetOpts) KustomizeIsZero() bool {
	return o == nil ||
		!o.namePrefix &&
			!o.nameSuffix &&
			!o.kustomizeVersion &&
			!o.kustomizeNamespace &&
			!o.ignoreMissingComponents &&
			len(o.kustomizeImages) == 0 &&
			len(o.kustomizeReplicas) == 0
}

// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var sourcePosition int
	var sourceName string
	appOpts := cmdutil.AppOptions{}
	opts := unsetOpts{}
	var appNamespace string
	command := &cobra.Command{
		Use:   "unset APPNAME parameters",
		Short: "Unset application parameters",
		Example: `  # Unset kustomize override kustomize image
  argocd app unset my-app --kustomize-image=alpine

  # Unset kustomize override suffix
  argocd app unset my-app --namesuffix

  # Unset kustomize override suffix for source at position 1 under spec.sources of app my-app. source-position starts at 1.
  argocd app unset my-app --source-position 1 --namesuffix

  # Unset kustomize override suffix for source named "test" under spec.sources of app my-app.
  argocd app unset my-app --source-name test --namesuffix

  # Unset parameter override
  argocd app unset my-app -p COMPONENT=PARAM`,

		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs})
			errors.CheckError(err)

			sourceName = appOpts.SourceName
			if sourceName != "" && sourcePosition != -1 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
			}

			if sourceName != "" {
				sourceNameToPosition := getSourceNameToPositionMap(app)
				pos, ok := sourceNameToPosition[sourceName]
				if !ok {
					log.Fatalf("Unknown source name '%s'", sourceName)
				}
				sourcePosition = int(pos)
			}

			if app.Spec.HasMultipleSources() {
				if sourcePosition <= 0 {
					errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
				}
				if len(app.Spec.GetSources()) < sourcePosition {
					errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
				}
			}

			source := app.Spec.GetSourcePtrByPosition(sourcePosition)

			updated, nothingToUnset := unset(source, opts)
			if nothingToUnset {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			if !updated {
				return
			}

			cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)

			promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
			canUnset := promptUtil.Confirm("Are you sure you want to unset the parameters? [y/n]")
			if canUnset {
				_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
					Name:         &app.Name,
					Spec:         &app.Spec,
					Validate:     &appOpts.Validate,
					AppNamespace: &appNs,
				})
				errors.CheckError(err)
			} else {
				fmt.Println("The command to unset the parameters has been cancelled.")
			}
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Unset application parameters in namespace")
	command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "Unset a parameter override (e.g. -p guestbook=image)")
	command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Unset one or more Helm values files")
	command.Flags().BoolVar(&opts.valuesLiteral, "values-literal", false, "Unset literal Helm values block")
	command.Flags().BoolVar(&opts.ignoreMissingValueFiles, "ignore-missing-value-files", false, "Unset the helm ignore-missing-value-files option (revert to false)")
	command.Flags().BoolVar(&opts.nameSuffix, "namesuffix", false, "Kustomize namesuffix")
	command.Flags().BoolVar(&opts.namePrefix, "nameprefix", false, "Kustomize nameprefix")
	command.Flags().BoolVar(&opts.kustomizeVersion, "kustomize-version", false, "Kustomize version")
	command.Flags().BoolVar(&opts.kustomizeNamespace, "kustomize-namespace", false, "Kustomize namespace")
	command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images name (e.g. --kustomize-image node --kustomize-image mysql)")
	command.Flags().StringArrayVar(&opts.kustomizeReplicas, "kustomize-replica", []string{}, "Kustomize replicas name (e.g. --kustomize-replica my-deployment --kustomize-replica my-statefulset)")
	command.Flags().BoolVar(&opts.ignoreMissingComponents, "ignore-missing-components", false, "Unset the kustomize ignore-missing-components option (revert to false)")
	command.Flags().StringArrayVar(&opts.pluginEnvs, "plugin-env", []string{}, "Unset plugin env variables (e.g --plugin-env name)")
	command.Flags().BoolVar(&opts.passCredentials, "pass-credentials", false, "Unset passCredentials")
	command.Flags().BoolVar(&opts.ref, "ref", false, "Unset ref on the source")
	command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
	return command
}

func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, nothingToUnset bool) {
	needToUnsetRef := false
	if opts.ref && source.IsRef() {
		source.Ref = ""
		updated = true
		needToUnsetRef = true
	}

	if source.Kustomize != nil {
		if opts.KustomizeIsZero() {
			return updated, !needToUnsetRef
		}

		if opts.namePrefix && source.Kustomize.NamePrefix != "" {
			updated = true
			source.Kustomize.NamePrefix = ""
		}

		if opts.nameSuffix && source.Kustomize.NameSuffix != "" {
			updated = true
			source.Kustomize.NameSuffix = ""
		}

		if opts.kustomizeVersion && source.Kustomize.Version != "" {
			updated = true
			source.Kustomize.Version = ""
		}

		if opts.kustomizeNamespace && source.Kustomize.Namespace != "" {
			updated = true
			source.Kustomize.Namespace = ""
		}

		if opts.ignoreMissingComponents && source.Kustomize.IgnoreMissingComponents {
			source.Kustomize.IgnoreMissingComponents = false
			updated = true
		}

		for _, kustomizeImage := range opts.kustomizeImages {
			for i, item := range source.Kustomize.Images {
				if !argoappv1.KustomizeImage(kustomizeImage).Match(item) {
					continue
				}
				updated = true
				// remove i
				a := source.Kustomize.Images
				copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index.
				a[len(a)-1] = ""     // Erase last element (write zero value).
				a = a[:len(a)-1]     // Truncate slice.
				source.Kustomize.Images = a
			}
		}

		for _, kustomizeReplica := range opts.kustomizeReplicas {
			kustomizeReplicas := source.Kustomize.Replicas
			for i, item := range kustomizeReplicas {
				if kustomizeReplica == item.Name {
					source.Kustomize.Replicas = append(kustomizeReplicas[0:i], kustomizeReplicas[i+1:]...)
					updated = true
					break
				}
			}
		}
	}
	if source.Helm != nil {
		if len(opts.parameters) == 0 && len(opts.valuesFiles) == 0 && !opts.valuesLiteral && !opts.ignoreMissingValueFiles && !opts.passCredentials {
			return updated, !needToUnsetRef
		}
		for _, paramStr := range opts.parameters {
			helmParams := source.Helm.Parameters
			for i, p := range helmParams {
				if p.Name == paramStr {
					source.Helm.Parameters = append(helmParams[0:i], helmParams[i+1:]...)
					updated = true
					break
				}
			}
		}
		if opts.valuesLiteral && !source.Helm.ValuesIsEmpty() {
			err := source.Helm.SetValuesString("")
			if err == nil {
				updated = true
			}
		}
		for _, valuesFile := range opts.valuesFiles {
			specValueFiles := source.Helm.ValueFiles
			for i, vf := range specValueFiles {
				if vf == valuesFile {
					source.Helm.ValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
					updated = true
					break
				}
			}
		}
		if opts.ignoreMissingValueFiles && source.Helm.IgnoreMissingValueFiles {
			source.Helm.IgnoreMissingValueFiles = false
			updated = true
		}
		if opts.passCredentials && source.Helm.PassCredentials {
			source.Helm.PassCredentials = false
			updated = true
		}
	}

	if source.Plugin != nil {
		if len(opts.pluginEnvs) == 0 {
			return false, !needToUnsetRef
		}
		for _, env := range opts.pluginEnvs {
			err := source.Plugin.RemoveEnvEntry(env)
			if err == nil {
				updated = true
			}
		}
	}
	return updated, false
}

// targetObjects deserializes the list of target states into unstructured objects
func targetObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructured, error) {
	objs := make([]*unstructured.Unstructured, len(resources))
	for i, resState := range resources {
		obj, err := resState.TargetObject()
		if err != nil {
			return nil, err
		}
		objs[i] = obj
	}
	return objs, nil
}

func getLocalObjects(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
	trackingMethod string,
) []*unstructured.Unstructured {
	manifestStrings := getLocalObjectsString(ctx, app, proj, local, localRepoRoot, appLabelKey, kubeVersion, apiVersions, kustomizeOptions, trackingMethod)
	objs := make([]*unstructured.Unstructured, len(manifestStrings))
	for i := range manifestStrings {
		obj := unstructured.Unstructured{}
		err := json.Unmarshal([]byte(manifestStrings[i]), &obj)
		errors.CheckError(err)
		objs[i] = &obj
	}
	return objs
}

func getLocalObjectsString(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
	trackingMethod string,
) []string {
	source := app.Spec.GetSource()
	res, err := repository.GenerateManifests(ctx, local, localRepoRoot, source.TargetRevision, &repoapiclient.ManifestRequest{
		Repo:                            &argoappv1.Repository{Repo: source.RepoURL},
		AppLabelKey:                     appLabelKey,
		AppName:                         app.Name,
		Namespace:                       app.Spec.Destination.Namespace,
		ApplicationSource:               &source,
		KustomizeOptions:                kustomizeOptions,
		KubeVersion:                     kubeVersion,
		ApiVersions:                     apiVersions,
		TrackingMethod:                  trackingMethod,
		ProjectName:                     proj.Name,
		ProjectSourceRepos:              proj.Spec.SourceRepos,
		AnnotationManifestGeneratePaths: app.GetAnnotation(argoappv1.AnnotationKeyManifestGeneratePaths),
	}, true, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
	errors.CheckError(err)

	return res.Manifests
}

type resourceInfoProvider struct {
	namespacedByGk map[schema.GroupKind]bool
}

// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced.
// If live object is missing then it does not matter if target is namespaced or not.
func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
	return p.namespacedByGk[gk], nil
}

func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
	namespacedByGk := make(map[schema.GroupKind]bool)
	for i := range liveObjs {
		if liveObjs[i] != nil {
			key := kube.GetResourceKey(liveObjs[i])
			namespacedByGk[schema.GroupKind{Group: key.Group, Kind: key.Kind}] = key.Namespace != ""
		}
	}
	localObs, _, err := controller.DeduplicateTargetObjects(appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
	errors.CheckError(err)
	objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
	for i := range localObs {
		obj := localObs[i]
		if !hook.IsHook(obj) && !ignore.Ignore(obj) {
			objByKey[kube.GetResourceKey(obj)] = obj
		}
	}
	return objByKey
}

type objKeyLiveTarget struct {
	key    kube.ResourceKey
	live   *unstructured.Unstructured
	target *unstructured.Unstructured
}

// NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		refresh              bool
		hardRefresh          bool
		exitCode             bool
		diffExitCode         int
		local                string
		revision             string
		localRepoRoot        string
		serverSideGenerate   bool
		serverSideDiff       bool
		localIncludes        []string
		appNamespace         string
		revisions            []string
		sourcePositions      []int64
		sourceNames          []string
		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
	)
	shortDesc := "Perform a diff against the target and live state."
	command := &cobra.Command{
		Use:   "diff APPNAME",
		Short: shortDesc,
		Long:  shortDesc + "\nUses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool.\nReturns the following exit codes: 2 on general errors, 1 when a diff is found, and 0 when no diff is found\nKubernetes Secrets are ignored from this diff.",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(2)
			}

			if len(sourceNames) > 0 && len(sourcePositions) > 0 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-positions and source-names can be specified.")
			}

			if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
				errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-positions, length of values for both flags should be same.")
			}

			if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
				errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-names, length of values for both flags should be same.")
			}

			clientset := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := clientset.NewApplicationClientOrDie()
			defer utilio.Close(conn)
			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				Refresh:      getRefreshType(refresh, hardRefresh),
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			if len(sourceNames) > 0 {
				sourceNameToPosition := getSourceNameToPositionMap(app)

				for _, name := range sourceNames {
					pos, ok := sourceNameToPosition[name]
					if !ok {
						log.Fatalf("Unknown source name '%s'", name)
					}
					sourcePositions = append(sourcePositions, pos)
				}
			}

			resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName, AppNamespace: &appNs})
			errors.CheckError(err)
			conn, settingsIf := clientset.NewSettingsClientOrDie()
			defer utilio.Close(conn)
			argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
			errors.CheckError(err)
			diffOption := &DifferenceOption{}

			hasServerSideDiffAnnotation := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")

			// Use annotation if flag not explicitly set
			if !c.Flags().Changed("server-side-diff") {
				serverSideDiff = hasServerSideDiffAnnotation
			} else if serverSideDiff && !hasServerSideDiffAnnotation {
				// Flag explicitly set to true, but app annotation is not set
				fmt.Fprintf(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
			}

			// Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
			if serverSideDiff && local != "" && !serverSideGenerate {
				log.Fatal("--server-side-diff with --local requires --server-side-generate.")
			}

			switch {
			case app.Spec.HasMultipleSources() && len(revisions) > 0 && len(sourcePositions) > 0:
				numOfSources := int64(len(app.Spec.GetSources()))
				for _, pos := range sourcePositions {
					if pos <= 0 || pos > numOfSources {
						log.Fatal("source-position cannot be less than 1 or more than number of sources in the app. Counting starts at 1.")
					}
				}

				q := application.ApplicationManifestQuery{
					Name:            &appName,
					AppNamespace:    &appNs,
					Revisions:       revisions,
					SourcePositions: sourcePositions,
					NoCache:         &hardRefresh,
				}
				res, err := appIf.GetManifests(ctx, &q)
				errors.CheckError(err)

				diffOption.res = res
				diffOption.revisions = revisions
			case revision != "":
				q := application.ApplicationManifestQuery{
					Name:         &appName,
					Revision:     &revision,
					AppNamespace: &appNs,
					NoCache:      &hardRefresh,
				}
				res, err := appIf.GetManifests(ctx, &q)
				errors.CheckError(err)
				diffOption.res = res
				diffOption.revision = revision
			case local != "":
				if serverSideGenerate {
					client, err := appIf.GetManifestsWithFiles(ctx, grpc_retry.Disable())
					errors.CheckError(err)

					err = manifeststream.SendApplicationManifestQueryWithFiles(ctx, client, appName, appNs, local, localIncludes)
					errors.CheckError(err)

					res, err := client.CloseAndRecv()
					errors.CheckError(err)

					diffOption.serversideRes = res
				} else {
					fmt.Fprintf(os.Stderr, "Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.")
					conn, clusterIf := clientset.NewClusterClientOrDie()
					defer utilio.Close(conn)
					cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
					errors.CheckError(err)

					diffOption.local = local
					diffOption.localRepoRoot = localRepoRoot
					diffOption.cluster = cluster
				}
			}
			proj := getProject(ctx, c, clientOpts, app.Spec.Project)

			foundDiffs := findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, app.GetName(), app.GetNamespace())
			if foundDiffs && exitCode {
				os.Exit(diffExitCode)
			}
		},
	}
	command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
	command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
	command.Flags().BoolVar(&exitCode, "exit-code", true, "Return non-zero exit code when there is a diff. May also return non-zero exit code if there is an error.")
	command.Flags().IntVar(&diffExitCode, "diff-exit-code", 1, "Return specified exit code when there is a diff. Typical error code is 20 but use another exit code if you want to differentiate from the generic exit code (20) returned by all CLI commands.")
	command.Flags().StringVar(&local, "local", "", "Compare live app to a local manifests")
	command.Flags().StringVar(&revision, "revision", "", "Compare live app to a particular revision")
	command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
	command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
	command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "Use server-side diff to calculate the diff. This will default to true if the ServerSideDiff annotation is set on the application.")
	command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace")
	command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
	command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
	command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
	return command
}

// printResourceDiff prints the diff header and calls cli.PrintDiff for a resource
func printResourceDiff(group, kind, namespace, name string, live, target *unstructured.Unstructured) {
	fmt.Printf("\n===== %s/%s %s/%s ======\n", group, kind, namespace, name)
	_ = cli.PrintDiff(name, live, target)
}

// findAndPrintServerSideDiff performs a server-side diff by making requests to the api server and prints the response
func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application, items []objKeyLiveTarget, resources *application.ManagedResourcesResponse, appIf application.ApplicationServiceClient, appName, appNs string) bool {
	// Process each item for server-side diff
	foundDiffs := false
	for _, item := range items {
		if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
			continue
		}

		// For server-side diff, we need to create aligned arrays for this specific resource
		var liveResource *argoappv1.ResourceDiff
		var targetManifest string

		if item.live != nil {
			for _, res := range resources.Items {
				if res.Group == item.key.Group && res.Kind == item.key.Kind &&
					res.Namespace == item.key.Namespace && res.Name == item.key.Name {
					liveResource = res
					break
				}
			}
		}

		if liveResource == nil {
			// Create empty live resource for creation case
			liveResource = &argoappv1.ResourceDiff{
				Group:       item.key.Group,
				Kind:        item.key.Kind,
				Namespace:   item.key.Namespace,
				Name:        item.key.Name,
				LiveState:   "",
				TargetState: "",
				Modified:    true,
			}
		}

		if item.target != nil {
			jsonBytes, err := json.Marshal(item.target)
			if err != nil {
				errors.CheckError(fmt.Errorf("error marshaling target object: %w", err))
			}
			targetManifest = string(jsonBytes)
		}

		// Call server-side diff for this individual resource
		serverSideDiffQuery := &application.ApplicationServerSideDiffQuery{
			AppName:         &appName,
			AppNamespace:    &appNs,
			Project:         &app.Spec.Project,
			LiveResources:   []*argoappv1.ResourceDiff{liveResource},
			TargetManifests: []string{targetManifest},
		}

		serverSideDiffRes, err := appIf.ServerSideDiff(ctx, serverSideDiffQuery)
		if err != nil {
			errors.CheckError(err)
		}

		// Extract diff for this resource
		for _, resultItem := range serverSideDiffRes.Items {
			if resultItem.Hook || (!resultItem.Modified && resultItem.TargetState != "" && resultItem.LiveState != "") {
				continue
			}

			if resultItem.Modified || resultItem.TargetState == "" || resultItem.LiveState == "" {
				var live, target *unstructured.Unstructured

				if resultItem.TargetState != "" && resultItem.TargetState != "null" {
					target = &unstructured.Unstructured{}
					err = json.Unmarshal([]byte(resultItem.TargetState), target)
					errors.CheckError(err)
				}

				if resultItem.LiveState != "" && resultItem.LiveState != "null" {
					live = &unstructured.Unstructured{}
					err = json.Unmarshal([]byte(resultItem.LiveState), live)
					errors.CheckError(err)
				}

				// Print resulting diff for this resource
				foundDiffs = true
				printResourceDiff(resultItem.Group, resultItem.Kind, resultItem.Namespace, resultItem.Name, live, target)
			}
		}
	}

	return foundDiffs
}

// DifferenceOption struct to store diff options
type DifferenceOption struct {
	local         string
	localRepoRoot string
	revision      string
	cluster       *argoappv1.Cluster
	res           *repoapiclient.ManifestResponse
	serversideRes *repoapiclient.ManifestResponse
	revisions     []string
}

// findAndPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
func findAndPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, useServerSideDiff bool, appIf application.ApplicationServiceClient, appName, appNs string) bool {
	var foundDiffs bool

	items, err := prepareObjectsForDiff(ctx, app, proj, resources, argoSettings, diffOptions)
	errors.CheckError(err)

	if useServerSideDiff {
		return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs)
	}

	for _, item := range items {
		if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
			continue
		}
		overrides := make(map[string]argoappv1.ResourceOverride)
		for k := range argoSettings.ResourceOverrides {
			val := argoSettings.ResourceOverrides[k]
			overrides[k] = *val
		}

		// TODO remove hardcoded IgnoreAggregatedRoles and retrieve the
		// compareOptions in the protobuf
		ignoreAggregatedRoles := false
		diffConfig, err := argodiff.NewDiffConfigBuilder().
			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, ignoreNormalizerOpts).
			WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
			WithNoCache().
			WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
			Build()
		errors.CheckError(err)
		diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig)
		errors.CheckError(err)

		if diffRes.Modified || item.target == nil || item.live == nil {
			var live *unstructured.Unstructured
			var target *unstructured.Unstructured
			if item.target != nil && item.live != nil {
				target = &unstructured.Unstructured{}
				live = item.live
				err = json.Unmarshal(diffRes.PredictedLive, target)
				errors.CheckError(err)
			} else {
				live = item.live
				target = item.target
			}
			foundDiffs = true
			printResourceDiff(item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name, live, target)
		}
	}
	return foundDiffs
}

func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[kube.ResourceKey]*unstructured.Unstructured, items []objKeyLiveTarget, argoSettings *settings.Settings, appName, namespace string) []objKeyLiveTarget {
	resourceTracking := argo.NewResourceTracking()
	for _, res := range resources.Items {
		live := &unstructured.Unstructured{}
		err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
		errors.CheckError(err)

		key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
		if key.Kind == kube.SecretKind && key.Group == "" {
			// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
			delete(objs, key)
			continue
		}
		if local, ok := objs[key]; ok || live != nil {
			if local != nil && !kube.IsCRD(local) {
				err = resourceTracking.SetAppInstance(local, argoSettings.AppLabelKey, appName, namespace, argoappv1.TrackingMethod(argoSettings.GetTrackingMethod()), argoSettings.GetInstallationID())
				errors.CheckError(err)
			}

			items = append(items, objKeyLiveTarget{key, live, local})
			delete(objs, key)
		}
	}
	for key, local := range objs {
		if key.Kind == kube.SecretKind && key.Group == "" {
			// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
			delete(objs, key)
			continue
		}
		items = append(items, objKeyLiveTarget{key, nil, local})
	}
	return items
}

// NewApplicationDeleteCommand returns a new instance of an `argocd app delete` command
func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		cascade           bool
		noPrompt          bool
		propagationPolicy string
		selector          string
		wait              bool
		appNamespace      string
	)
	command := &cobra.Command{
		Use:   "delete APPNAME",
		Short: "Delete an application",
		Example: `  # Delete an app
  argocd app delete my-app

  # Delete multiple apps
  argocd app delete my-app other-app

  # Delete apps by label
  argocd app delete -l app.kubernetes.io/instance=my-app
  argocd app delete -l app.kubernetes.io/instance!=my-app
  argocd app delete -l app.kubernetes.io/instance
  argocd app delete -l '!app.kubernetes.io/instance'
  argocd app delete -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) == 0 && selector == "" {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			acdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)
			isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
			promptFlag := c.Flag("yes")
			if promptFlag.Changed && promptFlag.Value.String() == "true" {
				noPrompt = true
			}

			appNames, err := getAppNamesBySelector(ctx, appIf, selector)
			errors.CheckError(err)

			if len(appNames) == 0 {
				appNames = args
			}

			numOfApps := len(appNames)

			// This is for backward compatibility,
			// before we showed the prompts only when condition cascade && isTerminal && !noPrompt is true
			promptUtil := utils.NewPrompt(cascade && isTerminal && !noPrompt)
			var (
				confirmAll = false
				confirm    = false
			)

			for _, appFullName := range appNames {
				appName, appNs := argo.ParseFromQualifiedName(appFullName, appNamespace)
				appDeleteReq := application.ApplicationDeleteRequest{
					Name:         &appName,
					AppNamespace: &appNs,
				}
				if c.Flag("cascade").Changed {
					appDeleteReq.Cascade = &cascade
				}
				if c.Flag("propagation-policy").Changed {
					appDeleteReq.PropagationPolicy = &propagationPolicy
				}
				messageForSingle := "Are you sure you want to delete '" + appFullName + "' and all its resources? [y/n] "
				messageForAll := "Are you sure you want to delete '" + appFullName + "' and all its resources? [y/n/a] where 'a' is to delete all specified apps and their resources without prompting "

				if !confirmAll {
					confirm, confirmAll = promptUtil.ConfirmBaseOnCount(messageForSingle, messageForAll, numOfApps)
				}
				if confirm || confirmAll {
					_, err := appIf.Delete(ctx, &appDeleteReq)
					errors.CheckError(err)
					if wait {
						checkForDeleteEvent(ctx, acdClient, appFullName)
					}
					fmt.Printf("application '%s' deleted\n", appFullName)
				} else {
					fmt.Println("The command to delete '" + appFullName + "' was cancelled.")
				}
			}
		},
	}
	command.Flags().BoolVar(&cascade, "cascade", true, "Perform a cascaded deletion of all application resources")
	command.Flags().StringVarP(&propagationPolicy, "propagation-policy", "p", "foreground", "Specify propagation policy for deletion of application's resources. One of: foreground|background")
	command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm cascaded deletion of application resources")
	command.Flags().StringVarP(&selector, "selector", "l", "", "Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
	command.Flags().BoolVar(&wait, "wait", false, "Wait until deletion of the application(s) completes")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace where the application will be deleted from")
	return command
}

func checkForDeleteEvent(ctx context.Context, acdClient argocdclient.Client, appFullName string) {
	appEventCh := acdClient.WatchApplicationWithRetry(ctx, appFullName, "")
	for appEvent := range appEventCh {
		if appEvent.Type == k8swatch.Deleted {
			return
		}
	}
}

// Print simple list of application names
func printApplicationNames(apps []argoappv1.Application) {
	for _, app := range apps {
		fmt.Println(app.QualifiedName())
	}
}

// Print table of application data
func printApplicationTable(apps []argoappv1.Application, output *string) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	var fmtStr string
	headers := []any{"NAME", "CLUSTER", "NAMESPACE", "PROJECT", "STATUS", "HEALTH", "SYNCPOLICY", "CONDITIONS"}
	if *output == "wide" {
		fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
		headers = append(headers, "REPO", "PATH", "TARGET")
	} else {
		fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
	}
	_, _ = fmt.Fprintf(w, fmtStr, headers...)
	for _, app := range apps {
		vals := []any{
			app.QualifiedName(),
			getServer(&app),
			app.Spec.Destination.Namespace,
			app.Spec.GetProject(),
			app.Status.Sync.Status,
			app.Status.Health.Status,
			formatSyncPolicy(app),
			formatConditionsSummary(app),
		}
		if *output == "wide" {
			vals = append(vals, app.Spec.GetSource().RepoURL, app.Spec.GetSource().Path, app.Spec.GetSource().TargetRevision)
		}
		_, _ = fmt.Fprintf(w, fmtStr, vals...)
	}
	_ = w.Flush()
}

// NewApplicationListCommand returns a new instance of an `argocd app list` command
func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		output       string
		selector     string
		projects     []string
		repo         string
		appNamespace string
		cluster      string
		path         string
	)
	command := &cobra.Command{
		Use:   "list",
		Short: "List applications",
		Example: `  # List all apps
  argocd app list

  # List apps by label, in this example we listing apps that are children of another app (aka app-of-apps)
  argocd app list -l app.kubernetes.io/instance=my-app
  argocd app list -l app.kubernetes.io/instance!=my-app
  argocd app list -l app.kubernetes.io/instance
  argocd app list -l '!app.kubernetes.io/instance'
  argocd app list -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
		Run: func(c *cobra.Command, _ []string) {
			ctx := c.Context()

			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)
			apps, err := appIf.List(ctx, &application.ApplicationQuery{
				Selector:     ptr.To(selector),
				AppNamespace: &appNamespace,
			})

			errors.CheckError(err)
			appList := apps.Items

			if len(projects) != 0 {
				appList = argo.FilterByProjects(appList, projects)
			}
			if repo != "" {
				appList = argo.FilterByRepo(appList, repo)
			}
			if cluster != "" {
				appList = argo.FilterByCluster(appList, cluster)
			}
			if path != "" {
				appList = argo.FilterByPath(appList, path)
			}

			switch output {
			case "yaml", "json":
				err := PrintResourceList(appList, output, false)
				errors.CheckError(err)
			case "name":
				printApplicationNames(appList)
			case "wide", "":
				printApplicationTable(appList, &output)
			default:
				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
			}
		},
	}
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name|json|yaml")
	command.Flags().StringVarP(&selector, "selector", "l", "", "List apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
	command.Flags().StringArrayVarP(&projects, "project", "p", []string{}, "Filter by project name")
	command.Flags().StringVarP(&repo, "repo", "r", "", "List apps by source repo URL")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only list applications in namespace")
	command.Flags().StringVarP(&cluster, "cluster", "c", "", "List apps by cluster name or url")
	command.Flags().StringVarP(&path, "path", "P", "", "List apps by path")
	return command
}

func formatSyncPolicy(app argoappv1.Application) string {
	if app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.IsAutomatedSyncEnabled() {
		return "Manual"
	}
	policy := "Auto"
	if app.Spec.SyncPolicy.Automated.Prune {
		policy = policy + "-Prune"
	}
	return policy
}

func formatConditionsSummary(app argoappv1.Application) string {
	typeToCnt := make(map[string]int)
	for i := range app.Status.Conditions {
		condition := app.Status.Conditions[i]
		if cnt, ok := typeToCnt[condition.Type]; ok {
			typeToCnt[condition.Type] = cnt + 1
		} else {
			typeToCnt[condition.Type] = 1
		}
	}
	items := make([]string, 0)
	for cndType, cnt := range typeToCnt {
		if cnt > 1 {
			items = append(items, fmt.Sprintf("%s(%d)", cndType, cnt))
		} else {
			items = append(items, cndType)
		}
	}

	// Sort the keys by name
	sort.Strings(items)

	summary := "<none>"
	if len(items) > 0 {
		slices.Sort(items)
		summary = strings.Join(items, ",")
	}
	return summary
}

const (
	resourceFieldDelimiter              = ":"
	resourceFieldCount                  = 3
	resourceFieldNamespaceDelimiter     = "/"
	resourceFieldNameWithNamespaceCount = 2
	resourceExcludeIndicator            = "!"
)

// resource is GROUP:KIND:NAMESPACE/NAME or GROUP:KIND:NAME
func parseSelectedResources(resources []string) ([]*argoappv1.SyncOperationResource, error) {
	// retrieve name and namespace in case if format is GROUP:KIND:NAMESPACE/NAME, otherwise return name and empty namespace
	nameRetriever := func(resourceName, resource string) (string, string, error) {
		if !strings.Contains(resourceName, resourceFieldNamespaceDelimiter) {
			return resourceName, "", nil
		}
		nameFields := strings.Split(resourceName, resourceFieldNamespaceDelimiter)
		if len(nameFields) != resourceFieldNameWithNamespaceCount {
			return "", "", fmt.Errorf("resource with namespace should have GROUP%sKIND%sNAMESPACE%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resourceFieldNamespaceDelimiter, resource)
		}
		namespace := nameFields[0]
		name := nameFields[1]
		return name, namespace, nil
	}

	var selectedResources []*argoappv1.SyncOperationResource
	if resources == nil {
		return selectedResources, nil
	}

	for _, resource := range resources {
		isExcluded := false
		// check if the resource flag starts with a '!'
		if strings.HasPrefix(resource, resourceExcludeIndicator) {
			resource = strings.TrimPrefix(resource, resourceExcludeIndicator)
			isExcluded = true
		}
		fields := strings.Split(resource, resourceFieldDelimiter)
		if len(fields) != resourceFieldCount {
			return nil, fmt.Errorf("resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resource)
		}
		name, namespace, err := nameRetriever(fields[2], resource)
		if err != nil {
			return nil, err
		}
		selectedResources = append(selectedResources, &argoappv1.SyncOperationResource{
			Group:     fields[0],
			Kind:      fields[1],
			Name:      name,
			Namespace: namespace,
			Exclude:   isExcluded,
		})
	}
	return selectedResources, nil
}

func getWatchOpts(watch watchOpts) watchOpts {
	// if no opts are defined should wait for sync,health,operation
	if (watch == watchOpts{}) {
		return watchOpts{
			sync:      true,
			health:    true,
			operation: true,
		}
	}
	return watch
}

// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		watch        watchOpts
		timeout      uint
		selector     string
		resources    []string
		output       string
		appNamespace string
	)
	command := &cobra.Command{
		Use:   "wait [APPNAME.. | -l selector]",
		Short: "Wait for an application to reach a synced and healthy state",
		Example: `  # Wait for an app
  argocd app wait my-app

  # Wait for multiple apps
  argocd app wait my-app other-app

  # Wait for apps by resource
  # Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME.
  argocd app wait my-app --resource :Service:my-service
  argocd app wait my-app --resource argoproj.io:Rollout:my-rollout
  argocd app wait my-app --resource '!apps:Deployment:my-service'
  argocd app wait my-app --resource apps:Deployment:my-service --resource :Service:my-service
  argocd app wait my-app --resource '!*:Service:*'
  # Specify namespace if the application has resources with the same name in different namespaces
  argocd app wait my-app --resource argoproj.io:Rollout:my-namespace/my-rollout

  # Wait for apps by label, in this example we waiting for apps that are children of another app (aka app-of-apps)
  argocd app wait -l app.kubernetes.io/instance=my-app
  argocd app wait -l app.kubernetes.io/instance!=my-app
  argocd app wait -l app.kubernetes.io/instance
  argocd app wait -l '!app.kubernetes.io/instance'
  argocd app wait -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) == 0 && selector == "" {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			watch = getWatchOpts(watch)
			selectedResources, err := parseSelectedResources(resources)
			errors.CheckError(err)
			appNames := args
			acdClient := headless.NewClientOrDie(clientOpts, c)
			closer, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(closer)
			if selector != "" {
				list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: ptr.To(selector)})
				errors.CheckError(err)
				for _, i := range list.Items {
					appNames = append(appNames, i.QualifiedName())
				}
			}
			for _, appName := range appNames {
				// Construct QualifiedName
				if appNamespace != "" && !strings.Contains(appName, "/") {
					appName = appNamespace + "/" + appName
				}
				_, _, err := waitOnApplicationStatus(ctx, acdClient, appName, timeout, watch, selectedResources, output)
				errors.CheckError(err)
			}
		},
	}
	command.Flags().BoolVar(&watch.sync, "sync", false, "Wait for sync")
	command.Flags().BoolVar(&watch.health, "health", false, "Wait for health")
	command.Flags().BoolVar(&watch.suspended, "suspended", false, "Wait for suspended")
	command.Flags().BoolVar(&watch.degraded, "degraded", false, "Wait for degraded")
	command.Flags().BoolVar(&watch.delete, "delete", false, "Wait for delete")
	command.Flags().BoolVar(&watch.hydrated, "hydrated", false, "Wait for hydration operations")
	command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
	command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
	command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations")
	command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only wait for an application  in namespace")
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
	return command
}

// printAppResources prints the resources of an application in a tabwriter table
func printAppResources(w io.Writer, app *argoappv1.Application) {
	_, _ = fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tSTATUS\tHEALTH\tHOOK\tMESSAGE\n")
	for _, res := range getResourceStates(app, nil) {
		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", res.Group, res.Kind, res.Namespace, res.Name, res.Status, res.Health, res.Hook, res.Message)
	}
}

func printTreeView(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	_, _ = fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tMESSAGE\n")
	for uid := range parentNodes {
		treeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
	}
	_ = w.Flush()
}

func printTreeViewDetailed(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tAGE\tMESSAGE\tREASON\n")
	for uid := range parentNodes {
		detailedTreeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
	}
	_ = w.Flush()
}

// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		revision                string
		revisions               []string
		sourcePositions         []int64
		sourceNames             []string
		resources               []string
		labels                  []string
		selector                string
		prune                   bool
		dryRun                  bool
		timeout                 uint
		strategy                string
		force                   bool
		replace                 bool
		serverSideApply         bool
		applyOutOfSyncOnly      bool
		async                   bool
		retryLimit              int64
		retryRefresh            bool
		retryBackoffDuration    time.Duration
		retryBackoffMaxDuration time.Duration
		retryBackoffFactor      int64
		local                   string
		localRepoRoot           string
		infos                   []string
		diffChanges             bool
		diffChangesConfirm      bool
		projects                []string
		output                  string
		appNamespace            string
		ignoreNormalizerOpts    normalizers.IgnoreNormalizerOpts
	)
	command := &cobra.Command{
		Use:   "sync [APPNAME... | -l selector | --project project-name]",
		Short: "Sync an application to its target state",
		Example: `  # Sync an app
  argocd app sync my-app

  # Sync multiples apps
  argocd app sync my-app other-app

  # Sync apps by label, in this example we sync apps that are children of another app (aka app-of-apps)
  argocd app sync -l app.kubernetes.io/instance=my-app
  argocd app sync -l app.kubernetes.io/instance!=my-app
  argocd app sync -l app.kubernetes.io/instance
  argocd app sync -l '!app.kubernetes.io/instance'
  argocd app sync -l 'app.kubernetes.io/instance notin (my-app,other-app)'

  # Sync a multi-source application for specific revision of specific sources
  argocd app sync my-app --revisions 0.0.1 --source-positions 1 --revisions 0.0.2 --source-positions 2
  argocd app sync my-app --revisions 0.0.1 --source-names my-chart --revisions 0.0.2 --source-names my-values

  # Sync a specific resource
  # Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
  argocd app sync my-app --resource :Service:my-service
  argocd app sync my-app --resource argoproj.io:Rollout:my-rollout
  argocd app sync my-app --resource '!apps:Deployment:my-service'
  argocd app sync my-app --resource apps:Deployment:my-service --resource :Service:my-service
  argocd app sync my-app --resource '!*:Service:*'
  # Specify namespace if the application has resources with the same name in different namespaces
  argocd app sync my-app --resource argoproj.io:Rollout:my-namespace/my-rollout`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()
			if len(args) == 0 && selector == "" && len(projects) == 0 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			if len(args) > 1 && selector != "" {
				log.Fatal("Cannot use selector option when application name(s) passed as argument(s)")
			}

			if len(args) != 1 && (len(revisions) > 0 || len(sourcePositions) > 0) {
				log.Fatal("Cannot use --revisions and --source-positions options when 0 or more than 1 application names are passed as argument(s)")
			}

			if len(args) != 1 && (len(revisions) > 0 || len(sourceNames) > 0) {
				log.Fatal("Cannot use --revisions and --source-names options when 0 or more than 1 application names are passed as argument(s)")
			}

			if len(sourceNames) > 0 && len(sourcePositions) > 0 {
				log.Fatal("Only one of source-positions and source-names can be specified.")
			}

			if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
				log.Fatal("While using --revisions and --source-positions, length of values for both flags should be same.")
			}

			if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
				log.Fatal("While using --revisions and --source-names, length of values for both flags should be same.")
			}

			for _, pos := range sourcePositions {
				if pos <= 0 {
					log.Fatal("source-position cannot be less than or equal to 0, Counting starts at 1")
				}
			}

			acdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			selectedLabels, err := label.Parse(labels)
			errors.CheckError(err)

			if len(args) == 1 && len(sourceNames) > 0 {
				appName, _ := argo.ParseFromQualifiedName(args[0], appNamespace)
				app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
				errors.CheckError(err)

				sourceNameToPosition := getSourceNameToPositionMap(app)

				for _, name := range sourceNames {
					pos, ok := sourceNameToPosition[name]
					if !ok {
						log.Fatalf("Unknown source name '%s'", name)
					}
					sourcePositions = append(sourcePositions, pos)
				}
			}

			appNames := args
			if selector != "" || len(projects) > 0 {
				list, err := appIf.List(ctx, &application.ApplicationQuery{
					Selector:     ptr.To(selector),
					AppNamespace: &appNamespace,
					Projects:     projects,
				})
				errors.CheckError(err)

				// unlike list, we'd want to fail if nothing was found
				if len(list.Items) == 0 {
					errMsg := "No matching apps found for filter:"
					if selector != "" {
						errMsg += " selector " + selector
					}
					if len(projects) != 0 {
						errMsg += fmt.Sprintf(" projects %v", projects)
					}
					log.Fatal(errMsg)
				}

				for _, i := range list.Items {
					appNames = append(appNames, i.QualifiedName())
				}
			}

			for _, appQualifiedName := range appNames {
				// Construct QualifiedName
				if appNamespace != "" && !strings.Contains(appQualifiedName, "/") {
					appQualifiedName = appNamespace + "/" + appQualifiedName
				}
				appName, appNs := argo.ParseFromQualifiedName(appQualifiedName, "")

				if len(selectedLabels) > 0 {
					q := application.ApplicationManifestQuery{
						Name:            &appName,
						AppNamespace:    &appNs,
						Revision:        &revision,
						Revisions:       revisions,
						SourcePositions: sourcePositions,
					}

					res, err := appIf.GetManifests(ctx, &q)
					if err != nil {
						log.Fatal(err)
					}

					fmt.Println("The name of the app is ", appName)

					for _, mfst := range res.Manifests {
						obj, err := argoappv1.UnmarshalToUnstructured(mfst)
						errors.CheckError(err)
						for key, selectedValue := range selectedLabels {
							if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
								gvk := obj.GroupVersionKind()
								resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
							}
						}
					}

					// If labels are provided and none are found return error only if specific resources were also not
					// specified.
					if len(resources) == 0 {
						log.Fatalf("No matching resources found for labels: %v", labels)
						return
					}
				}

				selectedResources, err := parseSelectedResources(resources)
				errors.CheckError(err)

				var localObjsStrings []string
				diffOption := &DifferenceOption{}

				app, err := appIf.Get(ctx, &application.ApplicationQuery{
					Name:         &appName,
					AppNamespace: &appNs,
				})
				errors.CheckError(err)

				if app.Spec.HasMultipleSources() {
					if revision != "" {
						log.Fatal("argocd cli does not work on multi-source app with --revision flag. Use --revisions and --source-position instead.")
						return
					}

					if local != "" {
						log.Fatal("argocd cli does not work on multi-source app with --local flag")
						return
					}
				}

				// filters out only those resources that needs to be synced
				filteredResources := filterAppResources(app, selectedResources)

				// if resources are provided and no app resources match, then return error
				if len(resources) > 0 && len(filteredResources) == 0 {
					log.Fatalf("No matching app resources found for resource filter: %v", strings.Join(resources, ", "))
				}

				if local != "" {
					if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.IsAutomatedSyncEnabled() && !dryRun {
						log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled except with --dry-run")
					}

					errors.CheckError(err)
					conn, settingsIf := acdClient.NewSettingsClientOrDie()
					argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
					errors.CheckError(err)
					utilio.Close(conn)

					conn, clusterIf := acdClient.NewClusterClientOrDie()
					defer utilio.Close(conn)
					cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
					errors.CheckError(err)
					utilio.Close(conn)

					proj := getProject(ctx, c, clientOpts, app.Spec.Project)
					localObjsStrings = getLocalObjectsString(ctx, app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
					errors.CheckError(err)
					diffOption.local = local
					diffOption.localRepoRoot = localRepoRoot
					diffOption.cluster = cluster
				}

				syncOptionsFactory := func() *application.SyncOptions {
					syncOptions := application.SyncOptions{}
					items := make([]string, 0)
					if replace {
						items = append(items, common.SyncOptionReplace)
					}
					if serverSideApply {
						items = append(items, common.SyncOptionServerSideApply)
					}
					if applyOutOfSyncOnly {
						items = append(items, common.SyncOptionApplyOutOfSyncOnly)
					}

					if len(items) == 0 {
						// for prevent send even empty array if not need
						return nil
					}
					syncOptions.Items = items
					return &syncOptions
				}

				syncReq := application.ApplicationSyncRequest{
					Name:            &appName,
					AppNamespace:    &appNs,
					DryRun:          &dryRun,
					Revision:        &revision,
					Resources:       filteredResources,
					Prune:           &prune,
					Manifests:       localObjsStrings,
					Infos:           getInfos(infos),
					SyncOptions:     syncOptionsFactory(),
					Revisions:       revisions,
					SourcePositions: sourcePositions,
				}

				switch strategy {
				case "apply":
					syncReq.Strategy = &argoappv1.SyncStrategy{Apply: &argoappv1.SyncStrategyApply{}}
					syncReq.Strategy.Apply.Force = force
				case "", "hook":
					syncReq.Strategy = &argoappv1.SyncStrategy{Hook: &argoappv1.SyncStrategyHook{}}
					syncReq.Strategy.Hook.Force = force
				default:
					log.Fatalf("Unknown sync strategy: '%s'", strategy)
				}
				if retryLimit != 0 {
					syncReq.RetryStrategy = &argoappv1.RetryStrategy{
						Limit:   retryLimit,
						Refresh: retryRefresh,
						Backoff: &argoappv1.Backoff{
							Duration:    retryBackoffDuration.String(),
							MaxDuration: retryBackoffMaxDuration.String(),
							Factor:      ptr.To(retryBackoffFactor),
						},
					}
				}
				if diffChanges {
					resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{
						ApplicationName: &appName,
						AppNamespace:    &appNs,
					})
					errors.CheckError(err)
					conn, settingsIf := acdClient.NewSettingsClientOrDie()
					defer utilio.Close(conn)
					argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
					errors.CheckError(err)
					foundDiffs := false
					fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)

					proj := getProject(ctx, c, clientOpts, app.Spec.Project)

					// Check if application has ServerSideDiff annotation
					serverSideDiff := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")

					foundDiffs = findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, appName, appNs)
					if !foundDiffs {
						fmt.Printf("====== No Differences found ======\n")
						// if no differences found, then no need to sync
						return
					}
					if !diffChangesConfirm {
						yesno := cli.AskToProceed(fmt.Sprintf("Please review changes to application %s shown above. Do you want to continue the sync process? (y/n): ", appQualifiedName))
						if !yesno {
							os.Exit(0)
						}
					}
				}
				_, err = appIf.Sync(ctx, &syncReq)
				errors.CheckError(err)

				if !async {
					app, opState, err := waitOnApplicationStatus(ctx, acdClient, appQualifiedName, timeout, watchOpts{operation: true}, selectedResources, output)
					errors.CheckError(err)

					if !dryRun {
						if !opState.Phase.Successful() {
							log.Fatalf("Operation has completed with phase: %s", opState.Phase)
						} else if len(selectedResources) == 0 && app.Status.Sync.Status != argoappv1.SyncStatusCodeSynced {
							// Only get resources to be pruned if sync was application-wide and final status is not synced
							pruningRequired := opState.SyncResult.Resources.PruningRequired()
							if pruningRequired > 0 {
								log.Fatalf("%d resources require pruning", pruningRequired)
							}
						}
					}
				}
			}
		},
	}
	command.Flags().BoolVar(&dryRun, "dry-run", false, "Preview apply without affecting cluster")
	command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
	command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
	command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
	command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
	command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
	command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
	command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
	command.Flags().BoolVar(&retryRefresh, "retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one")
	command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
	command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
	command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
	command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
	command.Flags().BoolVar(&force, "force", false, "Use a force apply")
	command.Flags().BoolVar(&replace, "replace", false, "Use a kubectl create/replace instead apply")
	command.Flags().BoolVar(&serverSideApply, "server-side", false, "Use server-side apply while syncing the application")
	command.Flags().BoolVar(&applyOutOfSyncOnly, "apply-out-of-sync-only", false, "Sync only out-of-sync resources")
	command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
	command.Flags().StringVar(&local, "local", "", "Path to a local directory. When this flag is present no git queries will be made")
	command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
	command.Flags().StringArrayVar(&infos, "info", []string{}, "A list of key-value pairs during sync process. These infos will be persisted in app.")
	command.Flags().BoolVar(&diffChangesConfirm, "assumeYes", false, "Assume yes as answer for all user queries or prompts")
	command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation")
	command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.")
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only sync an application in namespace")
	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
	command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
	command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
	command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
	return command
}

func getAppNamesBySelector(ctx context.Context, appIf application.ApplicationServiceClient, selector string) ([]string, error) {
	appNames := []string{}
	if selector != "" {
		list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: ptr.To(selector)})
		if err != nil {
			return []string{}, err
		}
		// unlike list, we'd want to fail if nothing was found
		if len(list.Items) == 0 {
			return []string{}, fmt.Errorf("no apps match selector %v", selector)
		}
		for _, i := range list.Items {
			appNames = append(appNames, i.QualifiedName())
		}
	}
	return appNames, nil
}

// ResourceState tracks the state of a resource when waiting on an application status.
type resourceState struct {
	Group     string
	Kind      string
	Namespace string
	Name      string
	Status    string
	Health    string
	Hook      string
	Message   string
}

// Key returns a unique-ish key for the resource.
func (rs *resourceState) Key() string {
	return fmt.Sprintf("%s/%s/%s/%s", rs.Group, rs.Kind, rs.Namespace, rs.Name)
}

func (rs *resourceState) FormatItems() []any {
	timeStr := time.Now().Format("2006-01-02T15:04:05-07:00")
	return []any{timeStr, rs.Group, rs.Kind, rs.Namespace, rs.Name, rs.Status, rs.Health, rs.Hook, rs.Message}
}

// Merge merges the new state with any different contents from another resourceState.
// Blank fields in the receiver state will be updated to non-blank.
// Non-blank fields in the receiver state will never be updated to blank.
// Returns whether or not any keys were updated.
func (rs *resourceState) Merge(newState *resourceState) bool {
	updated := false
	for _, field := range []string{"Status", "Health", "Hook", "Message"} {
		v := reflect.ValueOf(rs).Elem().FieldByName(field)
		currVal := v.String()
		newVal := reflect.ValueOf(newState).Elem().FieldByName(field).String()
		if newVal != "" && currVal != newVal {
			v.SetString(newVal)
			updated = true
		}
	}
	return updated
}

func getResourceStates(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) []*resourceState {
	var states []*resourceState
	resourceByKey := make(map[kube.ResourceKey]argoappv1.ResourceStatus)
	for i := range app.Status.Resources {
		res := app.Status.Resources[i]
		resourceByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = res
	}

	// print most resources info along with most recent operation results
	if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil {
		for _, res := range app.Status.OperationState.SyncResult.Resources {
			sync := string(res.HookPhase)
			health := string(res.Status)
			key := kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)
			if resource, ok := resourceByKey[key]; ok && res.HookType == "" {
				health = ""
				if resource.Health != nil {
					health = string(resource.Health.Status)
				}
				sync = string(resource.Status)
			}
			states = append(states, &resourceState{
				Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name, Status: sync, Health: health, Hook: string(res.HookType), Message: res.Message,
			})
			delete(resourceByKey, kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name))
		}
	}
	resKeys := make([]kube.ResourceKey, 0)
	for k := range resourceByKey {
		resKeys = append(resKeys, k)
	}
	sort.Slice(resKeys, func(i, j int) bool {
		return resKeys[i].String() < resKeys[j].String()
	})
	// print rest of resources which were not part of most recent operation
	for _, resKey := range resKeys {
		res := resourceByKey[resKey]
		health := ""
		if res.Health != nil {
			health = string(res.Health.Status)
		}
		states = append(states, &resourceState{
			Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name, Status: string(res.Status), Health: health, Hook: "", Message: "",
		})
	}
	// filter out not selected resources
	if len(selectedResources) > 0 {
		for i := len(states) - 1; i >= 0; i-- {
			res := states[i]
			if !argo.IncludeResource(res.Name, res.Namespace, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, selectedResources) {
				states = append(states[:i], states[i+1:]...)
			}
		}
	}
	return states
}

// filterAppResources selects the app resources that match atleast one of the resource filters.
func filterAppResources(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) []*argoappv1.SyncOperationResource {
	var filteredResources []*argoappv1.SyncOperationResource
	if app != nil && len(selectedResources) > 0 {
		for i := range app.Status.Resources {
			appResource := app.Status.Resources[i]
			if (argo.IncludeResource(appResource.Name, appResource.Namespace,
				schema.GroupVersionKind{Group: appResource.Group, Kind: appResource.Kind}, selectedResources)) {
				filteredResources = append(filteredResources, &argoappv1.SyncOperationResource{
					Group:     appResource.Group,
					Kind:      appResource.Kind,
					Name:      appResource.Name,
					Namespace: appResource.Namespace,
				})
			}
		}
	}
	return filteredResources
}

func groupResourceStates(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) map[string]*resourceState {
	resStates := make(map[string]*resourceState)
	for _, result := range getResourceStates(app, selectedResources) {
		key := result.Key()
		if prev, ok := resStates[key]; ok {
			prev.Merge(result)
		} else {
			resStates[key] = result
		}
	}
	return resStates
}

// check if resource health, sync and operation statuses matches watch options
func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation, hydrationFinished bool) bool {
	if watch.delete {
		return false
	}

	healthBeingChecked := watch.suspended || watch.health || watch.degraded
	healthCheckPassed := true

	if healthBeingChecked {
		healthCheckPassed = false
		if watch.health {
			healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusHealthy)
		}
		if watch.suspended {
			healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusSuspended)
		}
		if watch.degraded {
			healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusDegraded)
		}
	}

	synced := !watch.sync || syncStatus == string(argoappv1.SyncStatusCodeSynced)
	operational := !watch.operation || operationStatus == nil
	hydrated := !watch.hydrated || hydrationFinished
	return synced && healthCheckPassed && operational && hydrated
}

// resourceParentChild gets the latest state of the app and the latest state of the app's resource tree and then
// constructs the necessary data structures to print the app as a tree.
func resourceParentChild(ctx context.Context, acdClient argocdclient.Client, appName string, appNs string) (map[string]argoappv1.ResourceNode, map[string][]string, map[string]struct{}, map[string]*resourceState) {
	_, appIf := acdClient.NewApplicationClientOrDie()
	mapUIDToNode, mapParentToChild, parentNode := parentChildDetails(ctx, appIf, appName, appNs)
	app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: ptr.To(appName), AppNamespace: ptr.To(appNs)})
	errors.CheckError(err)
	mapNodeNameToResourceState := make(map[string]*resourceState)
	for _, res := range getResourceStates(app, nil) {
		mapNodeNameToResourceState[res.Kind+"/"+res.Name] = res
	}
	return mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState
}

const waitFormatString = "%s\t%5s\t%10s\t%10s\t%20s\t%8s\t%7s\t%10s\t%s\n"

// AppWithLock encapsulates the application and its lock
type AppWithLock struct {
	mu  sync.Mutex
	app *argoappv1.Application
}

// NewAppWithLock creates a new AppWithLock instance
func NewAppWithLock() *AppWithLock {
	return &AppWithLock{}
}

// SetApp safely updates the application
func (a *AppWithLock) SetApp(app *argoappv1.Application) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.app = app
}

// GetApp safely retrieves the application
func (a *AppWithLock) GetApp() *argoappv1.Application {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.app
}

// waitOnApplicationStatus watches an application and blocks until either the desired watch conditions
// are fulfilled or we reach the timeout. Returns the app once desired conditions have been filled.
// Additionally return the operationState at time of fulfilment (which may be different than returned app).
func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client, appName string, timeout uint, watch watchOpts, selectedResources []*argoappv1.SyncOperationResource, output string) (*argoappv1.Application, *argoappv1.OperationState, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	appWithLock := NewAppWithLock()
	// refresh controls whether or not we refresh the app before printing the final status.
	// We only want to do this when an operation is in progress, since operations are the only
	// time when the sync status lags behind when an operation completes
	refresh := false

	// appURL is declared here so that it can be used in the printFinalStatus function when the context is cancelled
	appURL := getAppURL(ctx, acdClient, appName)

	// printSummary controls whether we print the app summary table, OperationState, and ResourceState
	// We don't want to print these when output type is json or yaml, as the output would become unparsable.
	printSummary := output != "json" && output != "yaml"

	appRealName, appNs := argo.ParseFromQualifiedName(appName, "")

	printFinalStatus := func(app *argoappv1.Application) *argoappv1.Application {
		var err error
		if refresh {
			conn, appClient := acdClient.NewApplicationClientOrDie()
			refreshType := string(argoappv1.RefreshTypeNormal)
			app, err = appClient.Get(ctx, &application.ApplicationQuery{
				Name:         &appRealName,
				Refresh:      &refreshType,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)
			_ = conn.Close()
		}

		if printSummary {
			fmt.Println()
			printAppSummaryTable(app, appURL, nil)
			fmt.Println()
			if watch.operation {
				printOperationResult(app.Status.OperationState)
			}
		}

		switch output {
		case "yaml", "json":
			err := PrintResource(app, output)
			errors.CheckError(err)
		case "wide", "":
			if len(app.Status.Resources) > 0 {
				fmt.Println()
				w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
				printAppResources(w, app)
				_ = w.Flush()
			}
		case "tree":
			mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appRealName, appNs)
			if len(mapUIDToNode) > 0 {
				fmt.Println()
				printTreeView(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
			}
		case "tree=detailed":

			mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appRealName, appNs)
			if len(mapUIDToNode) > 0 {
				fmt.Println()
				printTreeViewDetailed(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
			}
		default:
			errors.CheckError(fmt.Errorf("unknown output format: %s", output))
		}
		return app
	}

	if timeout != 0 {
		time.AfterFunc(time.Duration(timeout)*time.Second, func() {
			conn, appClient := acdClient.NewApplicationClientOrDie()
			defer conn.Close()
			// We want to print the final status of the app even if the conditions are not met
			if printSummary {
				fmt.Println()
				fmt.Println("This is the state of the app after wait timed out:")
			}
			// Setting refresh = false because we don't want printFinalStatus to execute a refresh
			refresh = false
			// Updating the app object to the latest state
			app, err := appClient.Get(ctx, &application.ApplicationQuery{
				Name:         &appRealName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)
			// Update the app object
			appWithLock.SetApp(app)
			// Cancel the context to stop the watch
			cancel()

			if printSummary {
				fmt.Println()
				fmt.Println("The command timed out waiting for the conditions to be met.")
			}
		})
	}

	w := tabwriter.NewWriter(os.Stdout, 5, 0, 2, ' ', 0)
	if printSummary {
		_, _ = fmt.Fprintf(w, waitFormatString, "TIMESTAMP", "GROUP", "KIND", "NAMESPACE", "NAME", "STATUS", "HEALTH", "HOOK", "MESSAGE")
	}

	prevStates := make(map[string]*resourceState)
	conn, appClient := acdClient.NewApplicationClientOrDie()
	defer utilio.Close(conn)
	app, err := appClient.Get(ctx, &application.ApplicationQuery{
		Name:         &appRealName,
		AppNamespace: &appNs,
	})
	errors.CheckError(err)
	appWithLock.SetApp(app) // Update the app object

	// printFinalStatus() will refresh and update the app object, potentially causing the app's
	// status.operationState to be different than the version when we break out of the event loop.
	// This means the app.status is unreliable for determining the final state of the operation.
	// finalOperationState captures the operationState as it was seen when we met the conditions of
	// the wait, so the caller can rely on it to determine the outcome of the operation.
	// See: https://github.com/argoproj/argo-cd/issues/5592
	finalOperationState := appWithLock.GetApp().Status.OperationState

	appEventCh := acdClient.WatchApplicationWithRetry(ctx, appName, appWithLock.GetApp().ResourceVersion)
	for appEvent := range appEventCh {
		appWithLock.SetApp(&appEvent.Application)
		app = appWithLock.GetApp()

		finalOperationState = app.Status.OperationState
		operationInProgress := false

		if watch.delete && appEvent.Type == k8swatch.Deleted {
			fmt.Printf("Application '%s' deleted\n", app.QualifiedName())
			return nil, nil, nil
		}

		// consider the operation is in progress
		if app.Operation != nil {
			// if it just got requested
			operationInProgress = true
			if !app.Operation.DryRun() {
				refresh = true
			}
		} else if app.Status.OperationState != nil {
			if app.Status.OperationState.FinishedAt == nil {
				// if it is not finished yet
				operationInProgress = true
			} else if !app.Status.OperationState.Operation.DryRun() && (app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Before(app.Status.OperationState.FinishedAt)) {
				// if it is just finished and we need to wait for controller to reconcile app once after syncing
				operationInProgress = true
			}
		}

		hydrationFinished := app.Status.SourceHydrator.CurrentOperation != nil && app.Status.SourceHydrator.CurrentOperation.Phase == argoappv1.HydrateOperationPhaseHydrated && app.Status.SourceHydrator.CurrentOperation.SourceHydrator.DeepEquals(app.Status.SourceHydrator.LastSuccessfulOperation.SourceHydrator) && app.Status.SourceHydrator.CurrentOperation.DrySHA == app.Status.SourceHydrator.LastSuccessfulOperation.DrySHA

		var selectedResourcesAreReady bool

		// If selected resources are included, wait only on those resources, otherwise wait on the application as a whole.
		if len(selectedResources) > 0 {
			selectedResourcesAreReady = true
			for _, state := range getResourceStates(app, selectedResources) {
				resourceIsReady := checkResourceStatus(watch, state.Health, state.Status, appEvent.Application.Operation, hydrationFinished)
				if !resourceIsReady {
					selectedResourcesAreReady = false
					break
				}
			}
		} else {
			// Wait on the application as a whole
			selectedResourcesAreReady = checkResourceStatus(watch, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation, hydrationFinished)
		}

		if selectedResourcesAreReady && (!operationInProgress || !watch.operation) {
			app = printFinalStatus(app)
			return app, finalOperationState, nil
		}

		newStates := groupResourceStates(app, selectedResources)
		for _, newState := range newStates {
			var doPrint bool
			stateKey := newState.Key()
			if prevState, found := prevStates[stateKey]; found {
				if watch.health && prevState.Health != string(health.HealthStatusUnknown) && prevState.Health != string(health.HealthStatusDegraded) && newState.Health == string(health.HealthStatusDegraded) {
					_ = printFinalStatus(app)
					return nil, finalOperationState, fmt.Errorf("application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
				}
				doPrint = prevState.Merge(newState)
			} else {
				prevStates[stateKey] = newState
				doPrint = true
			}
			if doPrint && printSummary {
				_, _ = fmt.Fprintf(w, waitFormatString, prevStates[stateKey].FormatItems()...)
			}
		}
		_ = w.Flush()
	}
	_ = printFinalStatus(appWithLock.GetApp())
	return nil, finalOperationState, fmt.Errorf("timed out (%ds) waiting for app %q match desired state", timeout, appName)
}

// setParameterOverrides updates an existing or appends a new parameter override in the application
// the app is assumed to be a helm app and is expected to be in the form:
// param=value
func setParameterOverrides(app *argoappv1.Application, parameters []string, sourcePosition int) {
	if len(parameters) == 0 {
		return
	}
	source := app.Spec.GetSourcePtrByPosition(sourcePosition)
	var sourceType argoappv1.ApplicationSourceType
	if st, _ := source.ExplicitType(); st != nil {
		sourceType = *st
	} else if app.Status.SourceType != "" {
		sourceType = app.Status.SourceType
	} else if len(strings.SplitN(parameters[0], "=", 2)) == 2 {
		sourceType = argoappv1.ApplicationSourceTypeHelm
	}

	switch sourceType {
	case argoappv1.ApplicationSourceTypeHelm:
		if source.Helm == nil {
			source.Helm = &argoappv1.ApplicationSourceHelm{}
		}
		for _, p := range parameters {
			newParam, err := argoappv1.NewHelmParameter(p, false)
			if err != nil {
				log.Error(err)
				continue
			}
			source.Helm.AddParameter(*newParam)
		}
	default:
		log.Fatalf("Parameters can only be set against Helm applications")
	}
}

// Print list of history ID's for an application.
func printApplicationHistoryIDs(revHistory []argoappv1.RevisionHistory) {
	for _, depInfo := range revHistory {
		fmt.Println(depInfo.ID)
	}
}

// Print a history table for an application.
func printApplicationHistoryTable(revHistory []argoappv1.RevisionHistory) {
	maxAllowedRevisions := 7
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	type history struct {
		id       int64
		date     string
		revision string
	}
	varHistory := map[string][]history{}
	varHistoryKeys := []string{}
	for _, depInfo := range revHistory {
		if depInfo.Sources != nil {
			for i, sourceInfo := range depInfo.Sources {
				rev := sourceInfo.TargetRevision
				if len(depInfo.Revisions) == len(depInfo.Sources) && len(depInfo.Revisions[i]) >= maxAllowedRevisions {
					rev = fmt.Sprintf("%s (%s)", rev, depInfo.Revisions[i][0:maxAllowedRevisions])
				}
				if _, ok := varHistory[sourceInfo.RepoURL]; !ok {
					varHistoryKeys = append(varHistoryKeys, sourceInfo.RepoURL)
				}
				varHistory[sourceInfo.RepoURL] = append(varHistory[sourceInfo.RepoURL], history{
					id:       depInfo.ID,
					date:     depInfo.DeployedAt.String(),
					revision: rev,
				})
			}
		} else {
			rev := depInfo.Source.TargetRevision
			if len(depInfo.Revision) >= maxAllowedRevisions {
				rev = fmt.Sprintf("%s (%s)", rev, depInfo.Revision[0:maxAllowedRevisions])
			}
			if _, ok := varHistory[depInfo.Source.RepoURL]; !ok {
				varHistoryKeys = append(varHistoryKeys, depInfo.Source.RepoURL)
			}
			varHistory[depInfo.Source.RepoURL] = append(varHistory[depInfo.Source.RepoURL], history{
				id:       depInfo.ID,
				date:     depInfo.DeployedAt.String(),
				revision: rev,
			})
		}
	}
	for i, key := range varHistoryKeys {
		_, _ = fmt.Fprintf(w, "SOURCE\t%s\n", key)
		_, _ = fmt.Fprintf(w, "ID\tDATE\tREVISION\n")
		for _, history := range varHistory[key] {
			_, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", history.id, history.date, history.revision)
		}
		// Add a newline if it's not the last iteration
		if i < len(varHistoryKeys)-1 {
			_, _ = fmt.Fprintf(w, "\n")
		}
	}
	_ = w.Flush()
}

// NewApplicationHistoryCommand returns a new instance of an `argocd app history` command
func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		output       string
		appNamespace string
	)
	command := &cobra.Command{
		Use:   "history APPNAME",
		Short: "Show application deployment history",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)
			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			if output == "id" {
				printApplicationHistoryIDs(app.Status.History)
			} else {
				printApplicationHistoryTable(app.Status.History)
			}
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only show application deployment history in namespace")
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|id")
	return command
}

func findRevisionHistory(application *argoappv1.Application, historyId int64) (*argoappv1.RevisionHistory, error) {
	// in case if history id not passed and need fetch previous history revision
	if historyId == -1 {
		l := len(application.Status.History)
		if l < 2 {
			return nil, fmt.Errorf("application '%s' should have at least two successful deployments", application.Name)
		}
		return &application.Status.History[l-2], nil
	}
	for _, di := range application.Status.History {
		if di.ID == historyId {
			return &di, nil
		}
	}
	return nil, fmt.Errorf("application '%s' does not have deployment id '%d' in history", application.Name, historyId)
}

// NewApplicationRollbackCommand returns a new instance of an `argocd app rollback` command
func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		prune        bool
		timeout      uint
		output       string
		appNamespace string
	)
	command := &cobra.Command{
		Use:   "rollback APPNAME [ID]",
		Short: "Rollback application to a previous deployed version by History ID, omitted will Rollback to the previous version",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()
			if len(args) == 0 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			var err error
			depID := -1
			if len(args) > 1 {
				depID, err = strconv.Atoi(args[1])
				errors.CheckError(err)
			}
			acdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := acdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			depInfo, err := findRevisionHistory(app, int64(depID))
			errors.CheckError(err)

			_, err = appIf.Rollback(ctx, &application.ApplicationRollbackRequest{
				Name:         &appName,
				AppNamespace: &appNs,
				Id:           ptr.To(depInfo.ID),
				Prune:        ptr.To(prune),
			})
			errors.CheckError(err)

			_, _, err = waitOnApplicationStatus(ctx, acdClient, app.QualifiedName(), timeout, watchOpts{
				operation: true,
			}, nil, output)
			errors.CheckError(err)
		},
	}
	command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
	command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Rollback application in namespace")
	return command
}

const (
	printOpFmtStr              = "%-20s%s\n"
	defaultCheckTimeoutSeconds = 0
)

func printOperationResult(opState *argoappv1.OperationState) {
	if opState == nil {
		return
	}
	if opState.SyncResult != nil {
		fmt.Printf(printOpFmtStr, "Operation:", "Sync")
		if opState.SyncResult.Sources != nil && opState.SyncResult.Revisions != nil {
			fmt.Printf(printOpFmtStr, "Sync Revision:", strings.Join(opState.SyncResult.Revisions, ", "))
		} else {
			fmt.Printf(printOpFmtStr, "Sync Revision:", opState.SyncResult.Revision)
		}
	}
	fmt.Printf(printOpFmtStr, "Phase:", opState.Phase)
	fmt.Printf(printOpFmtStr, "Start:", opState.StartedAt)
	fmt.Printf(printOpFmtStr, "Finished:", opState.FinishedAt)
	var duration time.Duration
	if !opState.FinishedAt.IsZero() {
		duration = time.Second * time.Duration(opState.FinishedAt.Unix()-opState.StartedAt.Unix())
	} else {
		duration = time.Second * time.Duration(time.Now().UTC().Unix()-opState.StartedAt.Unix())
	}
	fmt.Printf(printOpFmtStr, "Duration:", duration)
	if opState.Message != "" {
		fmt.Printf(printOpFmtStr, "Message:", opState.Message)
	}
}

// NewApplicationManifestsCommand returns a new instance of an `argocd app manifests` command
func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		source          string
		revision        string
		revisions       []string
		sourcePositions []int64
		sourceNames     []string
		local           string
		localRepoRoot   string
	)
	command := &cobra.Command{
		Use:   "manifests APPNAME",
		Short: "Print manifests of an application",
		Example: templates.Examples(`
  # Get manifests for an application
  argocd app manifests my-app

  # Get manifests for an application at a specific revision
  argocd app manifests my-app --revision 0.0.1

  # Get manifests for a multi-source application at specific revisions for specific sources
  argocd app manifests my-app --revisions 0.0.1 --source-names src-base --revisions 0.0.2 --source-names src-values

  # Get manifests for a multi-source application at specific revisions for specific sources
  argocd app manifests my-app --revisions 0.0.1 --source-positions 1 --revisions 0.0.2 --source-positions 2
  		`),
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			if len(sourceNames) > 0 && len(sourcePositions) > 0 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-positions and source-names can be specified.")
			}

			if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
				errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-positions, length of values for both flags should be same.")
			}

			if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
				errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-names, length of values for both flags should be same.")
			}

			for _, pos := range sourcePositions {
				if pos <= 0 {
					log.Fatal("source-position cannot be less than or equal to 0, Counting starts at 1")
				}
			}

			appName, appNs := argo.ParseFromQualifiedName(args[0], "")
			clientset := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := clientset.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			app, err := appIf.Get(context.Background(), &application.ApplicationQuery{
				Name:         &appName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			if len(sourceNames) > 0 {
				sourceNameToPosition := getSourceNameToPositionMap(app)

				for _, name := range sourceNames {
					pos, ok := sourceNameToPosition[name]
					if !ok {
						log.Fatalf("Unknown source name '%s'", name)
					}
					sourcePositions = append(sourcePositions, pos)
				}
			}

			resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{
				ApplicationName: &appName,
				AppNamespace:    &appNs,
			})
			errors.CheckError(err)

			var unstructureds []*unstructured.Unstructured
			switch source {
			case "git":
				switch {
				case local != "":
					settingsConn, settingsIf := clientset.NewSettingsClientOrDie()
					defer utilio.Close(settingsConn)
					argoSettings, err := settingsIf.Get(context.Background(), &settings.SettingsQuery{})
					errors.CheckError(err)

					clusterConn, clusterIf := clientset.NewClusterClientOrDie()
					defer utilio.Close(clusterConn)
					cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
					errors.CheckError(err)

					proj := getProject(ctx, c, clientOpts, app.Spec.Project)
					//nolint:staticcheck
					unstructureds = getLocalObjects(context.Background(), app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
				case len(revisions) > 0 && len(sourcePositions) > 0:
					q := application.ApplicationManifestQuery{
						Name:            &appName,
						AppNamespace:    &appNs,
						Revision:        ptr.To(revision),
						Revisions:       revisions,
						SourcePositions: sourcePositions,
					}
					res, err := appIf.GetManifests(ctx, &q)
					errors.CheckError(err)

					for _, mfst := range res.Manifests {
						obj, err := argoappv1.UnmarshalToUnstructured(mfst)
						errors.CheckError(err)
						unstructureds = append(unstructureds, obj)
					}
				case revision != "":
					q := application.ApplicationManifestQuery{
						Name:         &appName,
						AppNamespace: &appNs,
						Revision:     ptr.To(revision),
					}
					res, err := appIf.GetManifests(ctx, &q)
					errors.CheckError(err)

					for _, mfst := range res.Manifests {
						obj, err := argoappv1.UnmarshalToUnstructured(mfst)
						errors.CheckError(err)
						unstructureds = append(unstructureds, obj)
					}
				default:
					targetObjs, err := targetObjects(resources.Items)
					errors.CheckError(err)
					unstructureds = targetObjs
				}
			case "live":
				liveObjs, err := cmdutil.LiveObjects(resources.Items)
				errors.CheckError(err)
				unstructureds = liveObjs
			default:
				log.Fatalf("Unknown source type '%s'", source)
			}

			for _, obj := range unstructureds {
				fmt.Println("---")
				yamlBytes, err := yaml.Marshal(obj)
				errors.CheckError(err)
				fmt.Printf("%s\n", yamlBytes)
			}
		},
	}
	command.Flags().StringVar(&source, "source", "git", "Source of manifests. One of: live|git")
	command.Flags().StringVar(&revision, "revision", "", "Show manifests at a specific revision")
	command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for the source at position in source-positions")
	command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
	command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
	command.Flags().StringVar(&local, "local", "", "If set, show locally-generated manifests. Value is the absolute path to app manifests within the manifest repo. Example: '/home/username/apps/env/app-1'.")
	command.Flags().StringVar(&localRepoRoot, "local-repo-root", ".", "Path to the local repository root. Used together with --local allows setting the repository root. Example: '/home/username/apps'.")
	return command
}

// NewApplicationTerminateOpCommand returns a new instance of an `argocd app terminate-op` command
func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	command := &cobra.Command{
		Use:   "terminate-op APPNAME",
		Short: "Terminate running operation of an application",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			appName, appNs := argo.ParseFromQualifiedName(args[0], "")
			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)
			_, err := appIf.TerminateOperation(ctx, &application.OperationTerminateRequest{
				Name:         &appName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)
			fmt.Printf("Application '%s' operation terminating\n", appName)
		},
	}
	return command
}

func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var appNamespace string
	command := &cobra.Command{
		Use:   "edit APPNAME",
		Short: "Edit application",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)
			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			appData, err := json.Marshal(app.Spec)
			errors.CheckError(err)
			appData, err = yaml.JSONToYAML(appData)
			errors.CheckError(err)

			cli.InteractiveEdit(appName+"-*-edit.yaml", appData, func(input []byte) error {
				input, err = yaml.YAMLToJSON(input)
				if err != nil {
					return fmt.Errorf("error converting YAML to JSON: %w", err)
				}
				updatedSpec := argoappv1.ApplicationSpec{}
				err = json.Unmarshal(input, &updatedSpec)
				if err != nil {
					return fmt.Errorf("error unmarshaling input into application spec: %w", err)
				}

				var appOpts cmdutil.AppOptions

				// do not allow overrides for applications with multiple sources
				if !app.Spec.HasMultipleSources() {
					cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, 0)
				}
				_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
					Name:         &appName,
					Spec:         &updatedSpec,
					Validate:     &appOpts.Validate,
					AppNamespace: &appNs,
				})
				if err != nil {
					return fmt.Errorf("failed to update application spec: %w", err)
				}
				return nil
			})
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only edit application in namespace")
	return command
}

func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		patch        string
		patchType    string
		appNamespace string
	)

	command := cobra.Command{
		Use:   "patch APPNAME",
		Short: "Patch application",
		Example: `  # Update an application's source path using json patch
  argocd app patch myapplication --patch='[{"op": "replace", "path": "/spec/source/path", "value": "newPath"}]' --type json

  # Update an application's repository target revision using merge patch
  argocd app patch myapplication --patch '{"spec": { "source": { "targetRevision": "master" } }}' --type merge`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}
			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
			defer utilio.Close(conn)

			patchedApp, err := appIf.Patch(ctx, &application.ApplicationPatchRequest{
				Name:         &appName,
				Patch:        &patch,
				PatchType:    &patchType,
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			yamlBytes, err := yaml.Marshal(patchedApp)
			errors.CheckError(err)

			fmt.Println(string(yamlBytes))
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only patch application in namespace")
	command.Flags().StringVar(&patch, "patch", "", "Patch body")
	command.Flags().StringVar(&patchType, "type", "json", "The type of patch being provided; one of [json merge]")
	return &command
}

// NewApplicationAddSourceCommand returns a new instance of an `argocd app add-source` command
func NewApplicationAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		appOpts      cmdutil.AppOptions
		appNamespace string
	)
	command := &cobra.Command{
		Use:   "add-source APPNAME",
		Short: "Adds a source to the list of sources in the application",
		Example: `  # Append a source to the list of sources in the application
  argocd app add-source guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --source-name guestbook`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()
			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			argocdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := argocdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)

			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				Refresh:      getRefreshType(false, false),
				AppNamespace: &appNs,
			})

			errors.CheckError(err)

			if c.Flags() == nil {
				errors.Fatal(errors.ErrorGeneric, "ApplicationSource needs atleast repoUrl, path or chart or ref field. No source to add.")
			}

			if len(app.Spec.Sources) > 0 {
				appSource, _ := cmdutil.ConstructSource(&argoappv1.ApplicationSource{}, appOpts, c.Flags())

				// sourcePosition is the index at which new source will be appended to spec.Sources
				sourcePosition := len(app.Spec.GetSources())
				app.Spec.Sources = append(app.Spec.Sources, *appSource)

				setParameterOverrides(app, appOpts.Parameters, sourcePosition)

				_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
					Name:         &app.Name,
					Spec:         &app.Spec,
					Validate:     &appOpts.Validate,
					AppNamespace: &appNs,
				})
				errors.CheckError(err)

				fmt.Printf("Application '%s' updated successfully\n", app.Name)
			} else {
				errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Cannot add source: application %s does not have spec.sources defined", appName))
			}
		},
	}
	cmdutil.AddAppFlags(command, &appOpts)
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
	return command
}

// NewApplicationRemoveSourceCommand returns a new instance of an `argocd app remove-source` command
func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var (
		sourcePosition int
		sourceName     string
		appNamespace   string
	)
	command := &cobra.Command{
		Use:   "remove-source APPNAME",
		Short: "Remove a source from multiple sources application.",
		Example: `  # Remove the source at position 1 from application's sources. Counting starts at 1.
  argocd app remove-source myapplication --source-position 1

  # Remove the source named "test" from application's sources.
  argocd app remove-source myapplication --source-name test`,
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			if sourceName == "" && sourcePosition <= 0 {
				errors.Fatal(errors.ErrorGeneric, "Value of source-position must be greater than 0")
			}

			argocdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := argocdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)

			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				Refresh:      getRefreshType(false, false),
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			if sourceName != "" && sourcePosition != -1 {
				errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
			}

			if sourceName != "" {
				sourceNameToPosition := getSourceNameToPositionMap(app)
				pos, ok := sourceNameToPosition[sourceName]
				if !ok {
					log.Fatalf("Unknown source name '%s'", sourceName)
				}
				sourcePosition = int(pos)
			}

			if !app.Spec.HasMultipleSources() {
				errors.Fatal(errors.ErrorGeneric, "Application does not have multiple sources configured")
			}

			if len(app.Spec.GetSources()) == 1 {
				errors.Fatal(errors.ErrorGeneric, "Cannot remove the only source remaining in the app")
			}

			if len(app.Spec.GetSources()) < sourcePosition {
				errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Application does not have source at %d\n", sourcePosition))
			}

			app.Spec.Sources = append(app.Spec.Sources[:sourcePosition-1], app.Spec.Sources[sourcePosition:]...)

			promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
			canDelete := promptUtil.Confirm("Are you sure you want to delete the source? [y/n]")
			if canDelete {
				_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
					Name:         &app.Name,
					Spec:         &app.Spec,
					AppNamespace: &appNs,
				})
				errors.CheckError(err)

				fmt.Printf("Application '%s' updated successfully\n", app.Name)
			} else {
				fmt.Println("The command to delete the source was cancelled")
			}
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
	command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
	command.Flags().StringVar(&sourceName, "source-name", "", "Name of the source from the list of sources of the app.")
	return command
}

func NewApplicationConfirmDeletionCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
	var appNamespace string
	command := &cobra.Command{
		Use:   "confirm-deletion APPNAME",
		Short: "Confirms deletion/pruning of an application resources",
		Run: func(c *cobra.Command, args []string) {
			ctx := c.Context()

			if len(args) != 1 {
				c.HelpFunc()(c, args)
				os.Exit(1)
			}

			argocdClient := headless.NewClientOrDie(clientOpts, c)
			conn, appIf := argocdClient.NewApplicationClientOrDie()
			defer utilio.Close(conn)

			appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)

			app, err := appIf.Get(ctx, &application.ApplicationQuery{
				Name:         &appName,
				Refresh:      getRefreshType(false, false),
				AppNamespace: &appNs,
			})
			errors.CheckError(err)

			annotations := app.Annotations
			if annotations == nil {
				annotations = map[string]string{}
				app.Annotations = annotations
			}
			annotations[common.AnnotationDeletionApproved] = metav1.Now().Format(time.RFC3339)

			_, err = appIf.Update(ctx, &application.ApplicationUpdateRequest{
				Application: app,
				Validate:    ptr.To(false),
				Project:     &app.Spec.Project,
			})
			errors.CheckError(err)

			fmt.Printf("Application '%s' updated successfully\n", app.Name)
		},
	}
	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
	return command
}

// prepareObjectsForDiff prepares objects for diffing using the switch statement
// to handle different diff options and building the objKeyLiveTarget items
func prepareObjectsForDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) ([]objKeyLiveTarget, error) {
	liveObjs, err := cmdutil.LiveObjects(resources.Items)
	if err != nil {
		return nil, err
	}
	items := make([]objKeyLiveTarget, 0)

	switch {
	case diffOptions.local != "":
		localObjs := groupObjsByKey(getLocalObjects(ctx, app, proj, diffOptions.local, diffOptions.localRepoRoot, argoSettings.AppLabelKey, diffOptions.cluster.Info.ServerVersion, diffOptions.cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
		items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
	case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
		var unstructureds []*unstructured.Unstructured
		for _, mfst := range diffOptions.res.Manifests {
			obj, err := argoappv1.UnmarshalToUnstructured(mfst)
			if err != nil {
				return nil, err
			}
			unstructureds = append(unstructureds, obj)
		}
		groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
		items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
	case diffOptions.serversideRes != nil:
		var unstructureds []*unstructured.Unstructured
		for _, mfst := range diffOptions.serversideRes.Manifests {
			obj, err := argoappv1.UnmarshalToUnstructured(mfst)
			if err != nil {
				return nil, err
			}
			unstructureds = append(unstructureds, obj)
		}
		groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
		items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
	default:
		for i := range resources.Items {
			res := resources.Items[i]
			live := &unstructured.Unstructured{}
			err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
			if err != nil {
				return nil, err
			}

			target := &unstructured.Unstructured{}
			err = json.Unmarshal([]byte(res.TargetState), &target)
			if err != nil {
				return nil, err
			}

			items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
		}
	}

	return items, nil
}
