package app

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/secret"
	"coopcloud.tech/tagcmp"

	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/deploy"
	"coopcloud.tech/abra/pkg/dns"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/lint"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/upstream/stack"
	dockerClient "github.com/docker/docker/client"
	"github.com/spf13/cobra"
)

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

var AppDeployCommand = &cobra.Command{
	// translators: `app deploy` command
	Use:     i18n.G("deploy <domain> [version] [flags]"),
	Aliases: strings.Split(appDeployAliases, ","),
	// translators: Short description for `app deploy` command
	Short: i18n.G("Deploy an app"),
	Long: i18n.G(`Deploy an app.

This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported as values for
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`),
	Example: i18n.G(`  # standard deployment
  abra app deploy 1312.net

  # chaos deployment
  abra app deploy 1312.net --chaos
  
  # deploy specific version
  abra app deploy 1312.net 2.0.0+1.2.3

  # deploy a specific git hash
  abra app deploy 1312.net 886db76d`),
	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 := i18n.G("autocomplete failed: %s", err)
				return []string{errMsg}, cobra.ShellCompDirectiveError
			}
			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			deployWarnMessages []string
			toDeployVersion    string
		)

		app := internal.ValidateApp(args)

		if err := validateArgsAndFlags(args); err != nil {
			log.Fatal(err)
		}

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

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

		log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
			log.Fatal(i18n.G("%s is already deployed", app.Name))
		}

		toDeployVersion, err = getDeployVersion(args, deployMeta, app)
		if err != nil {
			log.Fatal(err)
		}

		isChaosCommit, err := app.Recipe.IsChaosCommit(toDeployVersion)
		if err != nil {
			log.Fatal(i18n.G("unable to determine if %s is a chaos commit: %s", toDeployVersion, err))
		}

		if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
			log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
		}

		if !internal.Chaos {
			isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
			if err != nil {
				log.Fatal(i18n.G("ensure recipe: %s", err))
			}
			if isChaosCommit {
				log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
				internal.Chaos = true
			}
		}

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

		if err := validateSecrets(cl, app); 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)
		}

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

		versionLabel := toDeployVersion
		if internal.Chaos {
			for _, service := range compose.Services {
				if service.Name == "app" {
					labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
					// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
					versionLabel = service.Deploy.Labels[labelKey]
				}
			}
		}
		appPkg.SetVersionLabel(compose, stackName, versionLabel)

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

		envVars, err := appPkg.CheckEnv(app)
		if err != nil {
			log.Fatal(err)
		}

		for _, envVar := range envVars {
			if !envVar.Present {
				deployWarnMessages = append(deployWarnMessages,
					i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
				)
			}
		}

		if !internal.NoDomainChecks {
			if domainName, ok := app.Env["DOMAIN"]; ok {
				if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
					log.Fatal(err)
				}
			} else {
				log.Debug(i18n.G("skipping domain checks, no DOMAIN=... configured"))
			}
		} else {
			log.Debug(i18n.G("skipping domain checks"))
		}

		deployedVersion := config.MISSING_DEFAULT
		if deployMeta.IsDeployed {
			deployedVersion = deployMeta.Version
			if deployMeta.IsChaos {
				deployedVersion = deployMeta.ChaosVersion
			}
		}

		// 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)
		}

		// Show deploy overview
		if err := internal.DeployOverview(
			app,
			deployedVersion,
			toDeployVersion,
			"",
			deployWarnMessages,
			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,
			app.Name,
			app.Server,
			internal.DontWaitConverge,
			internal.NoInput,
			f,
		); err != nil {
			log.Fatal(err)
		}

		postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
		if ok && !internal.DontWaitConverge {
			log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
			if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
				log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
			}
		}

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

func getLatestVersionOrCommit(app appPkg.App) (string, error) {
	recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
	if err != nil {
		return "", err
	}

	for _, warning := range warnings {
		log.Warn(warning)
	}

	if len(recipeVersions) > 0 && !internal.Chaos {
		latest := recipeVersions[len(recipeVersions)-1]
		for tag := range latest {
			log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
			return tag, nil
		}
	}

	head, err := app.Recipe.Head()
	if err != nil {
		return "", err
	}

	return formatter.SmallSHA(head.String()), nil
}

// validateArgsAndFlags ensures compatible args/flags.
func validateArgsAndFlags(args []string) error {
	if len(args) == 2 && args[1] != "" && internal.Chaos {
		return errors.New(i18n.G("cannot use [version] and --chaos together"))
	}

	if len(args) == 2 && args[1] != "" && internal.DeployLatest {
		return errors.New(i18n.G("cannot use [version] and --latest together"))
	}

	if internal.DeployLatest && internal.Chaos {
		return errors.New(i18n.G("cannot use --chaos and --latest together"))
	}

	return nil
}

func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
	composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
	if err != nil {
		return err
	}

	secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
	if err != nil {
		return err
	}

	secStats, err := secret.PollSecretsStatus(cl, app)
	if err != nil {
		return err
	}

	for _, secStat := range secStats {
		if !secStat.CreatedOnRemote {
			secretConfig := secretsConfig[secStat.LocalName]
			if secretConfig.SkipGenerate {
				return errors.New(i18n.G("secret not inserted (#generate=false): %s", secStat.LocalName))
			}
			return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
		}
	}

	return nil
}

func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.App) (string, error) {
	// Chaos mode overrides everything
	if internal.Chaos {
		v, err := app.Recipe.ChaosVersion()
		if err != nil {
			return "", err
		}
		log.Debug(i18n.G("version: taking chaos version: %s", v))
		return v, nil
	}

	// Check if the deploy version is set with a cli argument
	if len(cliArgs) == 2 && cliArgs[1] != "" {
		log.Debug(i18n.G("version: taking version from cli arg: %s", cliArgs[1]))
		return cliArgs[1], nil
	}

	// Check if the recipe has a version in the .env file
	if app.Recipe.EnvVersion != "" && !internal.DeployLatest {
		if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
			// NOTE(d1): use double-line 5 spaces ("FATA ") trick to make a more
			// informative error message. it's ugly but that's our logging situation
			// atm
			return "", errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?
     to return to a regular release, specify a release tag, commit SHA or use "--latest"`,
				formatter.BoldDirtyDefault(app.Recipe.EnvVersionRaw)))
		}
		log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
		return app.Recipe.EnvVersion, nil
	}

	// Take deployed version
	if deployMeta.IsDeployed && !internal.DeployLatest {
		log.Debug(i18n.G("version: taking deployed version: %s", deployMeta.Version))
		return deployMeta.Version, nil
	}

	v, err := getLatestVersionOrCommit(app)
	log.Debug(i18n.G("version: taking new recipe version: %s", v))
	if err != nil {
		return "", err
	}
	return v, nil
}

func init() {
	AppDeployCommand.Flags().BoolVarP(
		&internal.Chaos,
		i18n.G("chaos"),
		i18n.G("C"),
		false,
		i18n.G("ignore uncommitted recipes changes"),
	)

	AppDeployCommand.Flags().BoolVarP(
		&internal.Force,
		i18n.G("force"),
		i18n.G("f"),
		false,
		i18n.G("perform action without further prompt"),
	)

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

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

	AppDeployCommand.PersistentFlags().BoolVarP(
		&internal.DeployLatest,
		i18n.G("latest"),
		i18n.G("l"),
		false,
		i18n.G("deploy latest recipe version"),
	)

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