package app

import (
	"fmt"

	"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/log"
	recipePkg "coopcloud.tech/abra/pkg/recipe"
	"coopcloud.tech/abra/pkg/secret"
	"github.com/AlecAivazis/survey/v2"
	"github.com/charmbracelet/lipgloss/table"
	dockerClient "github.com/docker/docker/client"
	"github.com/spf13/cobra"
)

var appNewDescription = `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 <app>" 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.`

var AppNewCommand = &cobra.Command{
	Use:     "new [recipe] [version] [flags]",
	Aliases: []string{"n"},
	Short:   "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 len(args) == 2 && internal.Chaos {
			log.Fatal("cannot use [version] and --chaos together")
		}

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

		chaosVersion := config.CHAOS_DEFAULT
		if internal.Chaos {
			recipeVersion = chaosVersion

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

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

		var recipeVersions recipePkg.RecipeVersions
		if recipeVersion == "" {
			var err error
			recipeVersions, err = recipe.GetRecipeVersions()
			if err != nil {
				log.Fatal(err)
			}
		}

		if len(recipeVersions) > 0 {
			latest := recipeVersions[len(recipeVersions)-1]
			for tag := range latest {
				recipeVersion = tag
			}

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

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

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

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

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

		var appSecrets AppSecrets
		var secretsTable *table.Table
		if generateSecrets {
			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)
			}

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

			secretsTable, err = formatter.CreateTable()
			if err != nil {
				log.Fatal(err)
			}

			headers := []string{"NAME", "VALUE"}
			secretsTable.Headers(headers...)

			for name, val := range appSecrets {
				secretsTable.Row(name, val)
			}
		}

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

		log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)

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

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

			fmt.Println(overview)

			log.Warnf(
				"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.Fatalf("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.Debugf("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: "Specify app domain",
			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
		}
		if err := survey.AskOne(prompt, &appDomain); err != nil {
			return err
		}
	}

	if appDomain == "" {
		return fmt.Errorf("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.Debugf("%s has no secrets to generate, skipping...", recipeName)
		return nil
	}

	if !generateSecrets && !internal.NoInput {
		prompt := &survey.Confirm{
			Message: "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 newAppServer == "" && !internal.NoInput {
		prompt := &survey.Select{
			Message: "Select app server:",
			Options: servers,
		}
		if err := survey.AskOne(prompt, &newAppServer); err != nil {
			return err
		}
	}

	if newAppServer == "" {
		return fmt.Errorf("no server provided")
	}

	return nil
}

var (
	newAppServer    string
	appDomain       string
	saveInPass      bool
	generateSecrets bool
)

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

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

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

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

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

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

}
