package cmd

import (
	"errors"
	"fmt"
	"os"
	"strconv"

	"github.com/spf13/cobra"

	"github.com/databus23/helm-diff/v3/diff"
	"github.com/databus23/helm-diff/v3/manifest"
)

type revision struct {
	release            string
	namespaces         // target namespace (-n/--namespace) and helm release storage namespace (--storage-namespace)
	kubeContext        string
	detailedExitCode   bool
	revisions          []string
	includeTests       bool
	normalizeManifests bool
	diff.Options
}

const revisionCmdLongUsage = `
This command compares the manifests details of a named release.

It can be used to compare the manifests of

 - latest REVISION with specified REVISION
	$ helm diff revision [flags] RELEASE REVISION1
   Example:
	$ helm diff revision my-release 2

 - REVISION1 with REVISION2
	$ helm diff revision [flags] RELEASE REVISION1 REVISION2
   Example:
	$ helm diff revision my-release 2 3
`

func revisionCmd() *cobra.Command {
	diff := revision{}
	revisionCmd := &cobra.Command{
		Use:   "revision [flags] RELEASE REVISION1 [REVISION2]",
		Short: "Shows diff between revision's manifests",
		Long:  revisionCmdLongUsage,
		RunE: func(cmd *cobra.Command, args []string) error {
			// Suppress the command usage on error. See #77 for more info
			cmd.SilenceUsage = true

			if v, _ := cmd.Flags().GetBool("version"); v {
				fmt.Println(Version)
				return nil
			}

			switch {
			case len(args) < 2:
				return errors.New("Too few arguments to Command \"revision\".\nMinimum 2 arguments required: release name, revision")
			case len(args) > 3:
				return errors.New("Too many arguments to Command \"revision\".\nMaximum 3 arguments allowed: release name, revision1, revision2")
			}

			ProcessDiffOptions(cmd.Flags(), &diff.Options)

			diff.release = args[0]
			diff.revisions = args[1:]
			return diff.differentiateHelm3()
		},
	}

	addNamespaceFlags(revisionCmd.Flags(), &diff.namespaces)
	revisionCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
	revisionCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
	revisionCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output")
	revisionCmd.Flags().StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use")
	AddDiffOptions(revisionCmd.Flags(), &diff.Options)

	revisionCmd.SuggestionsMinimumDistance = 1

	return revisionCmd
}

func (d *revision) differentiateHelm3() error {
	storageNs := d.storage()
	targetNs := d.namespace
	excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
	if d.includeTests {
		excludes = []string{}
	}
	switch len(d.revisions) {
	case 1:
		releaseResponse, err := getRelease(d.release, 0, storageNs, d.kubeContext)

		if err != nil {
			return err
		}

		revision, _ := strconv.Atoi(d.revisions[0])
		revisionResponse, err := getRelease(d.release, revision, storageNs, d.kubeContext)
		if err != nil {
			return err
		}

		oldSpecs := manifest.Parse(revisionResponse, targetNs, d.normalizeManifests, excludes...)
		newSpecs := manifest.Parse(releaseResponse, targetNs, d.normalizeManifests, excludes...)
		revisionResponse = nil //nolint:ineffassign // nil to allow GC to reclaim raw bytes before diff computation
		releaseResponse = nil  //nolint:ineffassign // nil to allow GC to reclaim raw bytes before diff computation

		diff.Manifests(
			oldSpecs,
			newSpecs,
			&d.Options,
			os.Stdout)

	case 2:
		revision1, _ := strconv.Atoi(d.revisions[0])
		revision2, _ := strconv.Atoi(d.revisions[1])
		if revision1 > revision2 {
			revision1, revision2 = revision2, revision1
		}

		revisionResponse1, err := getRelease(d.release, revision1, storageNs, d.kubeContext)
		if err != nil {
			return err
		}

		revisionResponse2, err := getRelease(d.release, revision2, storageNs, d.kubeContext)
		if err != nil {
			return err
		}

		oldSpecs := manifest.Parse(revisionResponse1, targetNs, d.normalizeManifests, excludes...)
		newSpecs := manifest.Parse(revisionResponse2, targetNs, d.normalizeManifests, excludes...)
		revisionResponse1 = nil //nolint:ineffassign // nil to allow GC to reclaim raw bytes before diff computation
		revisionResponse2 = nil //nolint:ineffassign // nil to allow GC to reclaim raw bytes before diff computation

		seenAnyChanges := diff.Manifests(
			oldSpecs,
			newSpecs,
			&d.Options,
			os.Stdout)

		if d.detailedExitCode && seenAnyChanges {
			return Error{
				error: errors.New("identified at least one change, exiting with non-zero exit code (detailed-exitcode parameter enabled)"),
				Code:  2,
			}
		}

	default:
		return errors.New("Invalid Arguments")
	}

	return nil
}