package app

import (
	"errors"
	"fmt"
	"strings"

	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/deploy"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"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"
)

// translators: `abra app rollback` aliases. use a comma separated list of
// aliases with no spaces in between
var appRollbackAliases = i18n.G("rl")

var AppRollbackCommand = &cobra.Command{
	// translators: `app rollback` command
	Use:     i18n.G("rollback <domain> [version] [flags]"),
	Aliases: strings.Split(appRollbackAliases, ","),
	// translators: Short description for `app rollback` command
	Short: i18n.G("Roll an app back to a previous version"),
	Long: i18n.G(`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: i18n.G(` # 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 {
				return []string{i18n.G("autocomplete failed: %s", err)}, 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(i18n.G("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(i18n.G("unknown deployed version, unable to downgrade"))
		}

		log.Debug(i18n.G("choosing %s as version to rollback", chosenDowngrade))

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

		if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
			log.Fatal(err)
		}

		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,
			SendRegistryAuth: true,
		}

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

		newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)

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

		// Gather secrets
		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		// Gather configs
		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		// Gather images
		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		deployedVersion := deployMeta.Version
		if deployMeta.IsChaos {
			deployedVersion = deployMeta.ChaosVersion
		}

		// NOTE(d1): no release notes implemeneted for rolling back
		if err := internal.DeployOverview(
			app,
			deployedVersion,
			chosenDowngrade,
			"",
			downgradeWarnMessages,
			secretInfo,
			configInfo,
			imageInfo,
		); err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		f, err := app.Filters(true, false, serviceNames...)
		if err != nil {
			log.Fatal(err)
		}

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

		if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
			log.Fatal(i18n.G("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 := i18n.G("please select a downgrade (version: %s):", deployMeta.Version)

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

		msg = i18n.G(
			"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 appPkg.App,
	deployMeta stack.DeployMeta,
) error {
	parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
	if err != nil {
		return errors.New(i18n.G("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name))
	}

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

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

	if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
		return errors.New(i18n.G("%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,
		i18n.G("force"),
		i18n.G("f"),
		false,
		i18n.G("perform action without further prompt"),
	)

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

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

	AppRollbackCommand.Flags().BoolVarP(
		&internal.ShowUnchanged,
		i18n.G("show-unchanged"),
		i18n.G("U"),
		false,
		i18n.G("show all configs & images, including unchanged ones"),
	)
}
