package app

import (
	"errors"
	"fmt"
	"strings"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/app"
	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	recipePkg "coopcloud.tech/abra/pkg/recipe"
	"coopcloud.tech/abra/pkg/secret"
	"github.com/AlecAivazis/survey/v2"
	dockerClient "github.com/docker/docker/client"
	"github.com/spf13/cobra"
)

var appNewDescription = i18n.G(`Creates a new app from a default recipe.

This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.

This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.

You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls".

Recipe commit hashes are supported values for "[version]".

Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
store them somewhere safe.

You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.`)

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

var AppNewCommand = &cobra.Command{
	// translators: `app new` command
	Use:     i18n.G("new [recipe] [version] [flags]"),
	Aliases: strings.Split(appNewAliases, ","),
	// translators: Short description for `app new` command
	Short: i18n.G("Create a new app"),
	Long:  appNewDescription,
	Args:  cobra.RangeArgs(0, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.RecipeNameComplete()
		case 1:
			recipe := internal.ValidateRecipe(args, cmd.Name())
			return autocomplete.RecipeVersionComplete(recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		recipe := internal.ValidateRecipe(args, cmd.Name())

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

		if len(args) == 2 && internal.Chaos {
			log.Fatal(i18n.G("cannot use [version] and --chaos together"))
		}

		var recipeVersion string
		if len(args) == 2 {
			recipeVersion = args[1]
		}

		chaosVersion := config.CHAOS_DEFAULT
		if internal.Chaos {
			var err error
			chaosVersion, err = recipe.ChaosVersion()
			if err != nil {
				log.Fatal(err)
			}

			recipeVersion = chaosVersion
		} else {
			if err := recipe.EnsureIsClean(); err != nil {
				log.Fatal(err)
			}

			var recipeVersions recipePkg.RecipeVersions
			if recipeVersion == "" {
				var err error
				var warnings []string
				recipeVersions, warnings, err = recipe.GetRecipeVersions()
				if err != nil {
					log.Fatal(err)
				}
				for _, warning := range warnings {
					log.Warn(warning)
				}
			}

			if recipeVersion != "" {
				if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
					log.Fatal(err)
				}
			} else if len(recipeVersions) > 0 {
				latest := recipeVersions[len(recipeVersions)-1]
				for tag := range latest {
					recipeVersion = tag
				}

				log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))

				if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
					log.Fatal(err)
				}
			} else {
				if err := recipe.EnsureLatest(); err != nil {
					log.Fatal(err)
				}

				if recipeVersion == "" {
					head, err := recipe.Head()
					if err != nil {
						log.Fatal(i18n.G("failed to retrieve latest commit for %s: %s", recipe.Name, err))
					}

					recipeVersion = formatter.SmallSHA(head.String())
				}
			}
		}

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

		if err := ensureDomainFlag(recipe, newAppServer); err != nil {
			log.Fatal(err)
		}

		sanitisedAppName := appPkg.SanitiseAppName(appDomain)
		log.Debug(i18n.G("%s sanitised as %s for new app", appDomain, sanitisedAppName))

		if err := appPkg.TemplateAppEnvSample(
			recipe,
			appDomain,
			newAppServer,
			appDomain,
		); err != nil {
			log.Fatal(err)
		}

		sampleEnv, err := recipe.SampleEnv()
		if err != nil {
			log.Fatal(err)
		}

		composeFiles, err := recipe.GetComposeFiles(sampleEnv)
		if err != nil {
			log.Fatal(err)
		}

		secretsConfig, err := secret.ReadSecretsConfig(
			recipe.SampleEnvPath,
			composeFiles,
			appPkg.StackName(appDomain),
		)
		if err != nil {
			log.Fatal(err)
		}

		var appSecrets AppSecrets
		if generateSecrets {
			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
				log.Fatal(err)
			}

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

			appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
			if err != nil {
				log.Fatal(err)
			}
		}

		if newAppServer == "default" {
			newAppServer = "local"
		}

		log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))

		if len(secretsConfig) > 0 {
			var (
				hasSecretToGenerate bool
				hasSecretToSkip     bool
			)

			for _, secretConfig := range secretsConfig {
				if secretConfig.SkipGenerate {
					hasSecretToSkip = true
					continue
				}

				hasSecretToGenerate = true
			}

			if hasSecretToGenerate && !generateSecrets {
				log.Warn(i18n.G("%s requires secret generation before deploy, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
			}

			if hasSecretToSkip {
				log.Warn(i18n.G("%s requires secret insertion before deploy (#generate=false)", recipe.Name))
			}
		}

		if len(appSecrets) > 0 {
			rows := [][]string{}
			for k, v := range appSecrets {
				rows = append(rows, []string{k, v})
			}

			overview := formatter.CreateOverview(i18n.G("SECRETS OVERVIEW"), rows)

			fmt.Println(overview)

			log.Warn(i18n.G(
				"secrets are %s shown again, please save them %s",
				formatter.BoldUnderlineStyle.Render("NOT"),
				formatter.BoldUnderlineStyle.Render("NOW"),
			))
		}

		app, err := app.Get(appDomain)
		if err != nil {
			log.Fatal(err)
		}

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

// AppSecrets represents all app secrest
type AppSecrets map[string]string

// createSecrets creates all secrets for a new app.
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
	// NOTE(d1): trim to match app.StackName() implementation
	if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
		log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
		sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
	}

	secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer)
	if err != nil {
		return nil, err
	}

	if saveInPass {
		for secretName := range secrets {
			secretValue := secrets[secretName]
			if err := secret.PassInsertSecret(
				secretValue,
				secretName,
				appDomain,
				newAppServer,
			); err != nil {
				return nil, err
			}
		}
	}

	return secrets, nil
}

// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
	if appDomain == "" && !internal.NoInput {
		prompt := &survey.Input{
			Message: i18n.G("Specify app domain"),
			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
		}
		if err := survey.AskOne(prompt, &appDomain); err != nil {
			return err
		}
	}

	if appDomain == "" {
		return errors.New(i18n.G("no domain provided"))
	}

	return nil
}

// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
	if len(secretsConfig) == 0 {
		log.Debug(i18n.G("%s has no secrets to generate, skipping...", recipeName))
		return nil
	}

	if !generateSecrets && !internal.NoInput {
		prompt := &survey.Confirm{
			Message: i18n.G("Generate app secrets?"),
		}
		if err := survey.AskOne(prompt, &generateSecrets); err != nil {
			return err
		}
	}

	return nil
}

// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error {
	servers, err := config.GetServers()
	if err != nil {
		return err
	}

	if len(servers) == 1 {
		newAppServer = servers[0]
		log.Info(i18n.G("single server detected, choosing %s automatically", newAppServer))
		return nil
	}

	if newAppServer == "" && !internal.NoInput {
		prompt := &survey.Select{
			Message: i18n.G("Select app server:"),
			Options: servers,
		}
		if err := survey.AskOne(prompt, &newAppServer); err != nil {
			return err
		}
	}

	if newAppServer == "" {
		return errors.New(i18n.G("no server provided"))
	}

	return nil
}

var (
	newAppServer    string
	appDomain       string
	saveInPass      bool
	generateSecrets bool
)

func init() {
	AppNewCommand.Flags().StringVarP(
		&newAppServer,
		i18n.G("server"),
		i18n.G("s"),
		"",
		i18n.G("specify server for new app"),
	)

	AppNewCommand.RegisterFlagCompletionFunc(
		i18n.G("server"),
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return autocomplete.ServerNameComplete()
		},
	)

	AppNewCommand.Flags().StringVarP(
		&appDomain,
		i18n.G("domain"),
		i18n.G("D"),
		"",
		i18n.G("domain name for app"),
	)

	AppNewCommand.Flags().BoolVarP(
		&saveInPass,
		i18n.G("pass"),
		i18n.G("p"),
		false,
		i18n.G("store secrets in a local pass store"),
	)

	AppNewCommand.Flags().BoolVarP(
		&generateSecrets,
		i18n.G("secrets"),
		i18n.G("S"),
		false,
		i18n.G("automatically generate secrets"),
	)

	AppNewCommand.Flags().BoolVarP(
		&internal.Chaos,
		i18n.G("chaos"),
		i18n.G("C"),
		false,
		i18n.G("ignore uncommitted recipes changes"),
	)

}
