package app

import (
	"fmt"

	"coopcloud.tech/abra/pkg/app"
	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/envfile"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/lint"
	stack "coopcloud.tech/abra/pkg/upstream/stack"
	"coopcloud.tech/tagcmp"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/log"
	"github.com/AlecAivazis/survey/v2"
	"github.com/spf13/cobra"
)

var AppRollbackCommand = &cobra.Command{
	Use:     "rollback <domain> [version] [flags]",
	Aliases: []string{"rl"},
	Short:   "Roll an app back to a previous version",
	Long: `This command rolls an app back to a previous version.

Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".

It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
version.

Only the deployed version is consulted when trying to determine what downgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.

A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
	Example: ` # standard rollback
  abra app rollback 1312.net

  # rollback to specific version
  abra app rollback 1312.net 2.0.0+1.2.3`,
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				errMsg := fmt.Sprintf("autocomplete failed: %s", err)
				return []string{errMsg}, cobra.ShellCompDirectiveError
			}
			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			downgradeWarnMessages []string
			chosenDowngrade       string
			availableDowngrades   []string
		)

		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := ensureDeployed(cl, app)
		if err != nil {
			log.Fatal(err)
		}

		if err := lint.LintForErrors(app.Recipe); err != nil {
			log.Fatal(err)
		}

		versions, err := app.Recipe.Tags()
		if err != nil {
			log.Fatal(err)
		}

		// NOTE(d1): we've no idea what the live deployment version is, so every
		// possible downgrade can be shown. it's up to the user to make the choice
		if deployMeta.Version == config.UNKNOWN_DEFAULT {
			availableDowngrades = versions
		}

		if len(args) == 2 && args[1] != "" {
			chosenDowngrade = args[1]

			if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
				log.Fatal(err)
			}

			availableDowngrades = append(availableDowngrades, chosenDowngrade)
		}

		if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
			downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
			if err != nil {
				log.Fatal(err)
			}

			if !downgradeAvailable {
				log.Info("no available downgrades")
				return
			}
		}

		if internal.Force || internal.NoInput || chosenDowngrade != "" {
			if len(availableDowngrades) > 0 {
				chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
			}
		} else {
			if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
				log.Fatal(err)
			}
		}

		if internal.Force &&
			chosenDowngrade == "" &&
			deployMeta.Version != config.UNKNOWN_DEFAULT {
			chosenDowngrade = deployMeta.Version
		}

		if chosenDowngrade == "" {
			log.Fatal("unknown deployed version, unable to downgrade")
		}

		log.Debugf("choosing %s as version to rollback", chosenDowngrade)

		if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
			log.Fatal(err)
		}

		abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
		if err != nil {
			log.Fatal(err)
		}
		for k, v := range abraShEnv {
			app.Env[k] = v
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stackName := app.StackName()
		deployOpts := stack.Deploy{
			Composefiles: composeFiles,
			Namespace:    stackName,
			Prune:        false,
			ResolveImage: stack.ResolveImageAlways,
			Detach:       false,
		}

		compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		appPkg.ExposeAllEnv(stackName, compose, app.Env)
		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
		if internal.Chaos {
			appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
		}
		appPkg.SetUpdateLabel(compose, stackName, app.Env)

		chaosVersion := config.CHAOS_DEFAULT
		if deployMeta.IsChaos {
			chaosVersion = deployMeta.ChaosVersion
		}

		// NOTE(d1): no release notes implemeneted for rolling back
		if err := internal.NewVersionOverview(
			app,
			downgradeWarnMessages,
			"rollback",
			deployMeta.Version,
			chaosVersion,
			chosenDowngrade,
			"",
		); err != nil {
			log.Fatal(err)
		}

		if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
			log.Fatal(err)
		}

		if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
			log.Fatalf("writing recipe version failed: %s", err)
		}
	},
}

// chooseDowngrade prompts the user to choose an downgrade interactively.
func chooseDowngrade(
	availableDowngrades []string,
	deployMeta stack.DeployMeta,
	chosenDowngrade *string,
) error {
	msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)

	if deployMeta.IsChaos {
		chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)

		msg = fmt.Sprintf(
			"please select a downgrade (version: %s, chaos: %s):",
			deployMeta.Version,
			chaosVersion,
		)
	}

	prompt := &survey.Select{
		Message: msg,
		Options: internal.SortVersionsDesc(availableDowngrades),
	}

	if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
		return err
	}

	return nil
}

// validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg(
	specificVersion string,
	app app.App,
	deployMeta stack.DeployMeta,
) error {
	parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
	if err != nil {
		return fmt.Errorf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
	}

	parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
	if err != nil {
		return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
	}

	if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
		!parsedSpecificVersion.Equals(parsedDeployedVersion) {
		return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
	}

	if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
		return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
	}

	return nil
}

// ensureDowngradesAvailable ensures that there are available downgrades.
func ensureDowngradesAvailable(
	versions []string,
	availableDowngrades *[]string,
	deployMeta stack.DeployMeta,
) (bool, error) {
	parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
	if err != nil {
		return false, err
	}

	for _, version := range versions {
		parsedVersion, err := tagcmp.Parse(version)
		if err != nil {
			return false, err
		}

		if parsedVersion.IsLessThan(parsedDeployedVersion) &&
			!(parsedVersion.Equals(parsedDeployedVersion)) {
			*availableDowngrades = append(*availableDowngrades, version)
		}
	}

	if len(*availableDowngrades) == 0 && !internal.Force {
		return false, nil
	}

	return true, nil
}

func init() {
	AppRollbackCommand.Flags().BoolVarP(
		&internal.Force,
		"force",
		"f",
		false,
		"perform action without further prompt",
	)

	AppRollbackCommand.Flags().BoolVarP(
		&internal.NoDomainChecks,
		"no-domain-checks",
		"D",
		false,
		"disable public DNS checks",
	)

	AppRollbackCommand.Flags().BoolVarP(
		&internal.DontWaitConverge, "no-converge-checks",
		"c",
		false,
		"disable converge logic checks",
	)
}
