package app

import (
	"context"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"sort"
	"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"
	containerPkg "coopcloud.tech/abra/pkg/container"
	contextPkg "coopcloud.tech/abra/pkg/context"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/upstream/stack"
	"github.com/docker/docker/api/types/filters"
	"github.com/spf13/cobra"
)

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

// translators: `abra app env list` aliases. use a comma separated list of
// aliases with no spaces in between
var appEnvListAliases = i18n.G("l,ls")

// translators: `abra app env pull` aliases. use a comma separated list of
// aliases with no spaces in between
var appEnvPullAliases = i18n.G("pl,p")

var AppEnvListCommand = &cobra.Command{
	// translators: `app env list` command
	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appEnvListAliases, ","),
	// translators: Short description for `app env list` command
	Short:   i18n.G("List all app environment values"),
	Example: i18n.G("  abra app env list 1312.net"),
	Args:    cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		var envKeys []string
		for k := range app.Env {
			envKeys = append(envKeys, k)
		}

		sort.Strings(envKeys)

		var rows [][]string
		for _, k := range envKeys {
			rows = append(rows, []string{k, app.Env[k]})
		}

		overview := formatter.CreateOverview(i18n.G("ENV OVERVIEW"), rows)
		fmt.Println(overview)
	},
}

var AppEnvPullCommand = &cobra.Command{
	// translators: `app pull` command
	Use:     i18n.G("pull <domain> [flags]"),
	Aliases: strings.Split(appEnvPullAliases, ","),
	// translators: Short description for `app env pull` command
	Short: i18n.G("Pull app environment values from a deployed app"),
	Long: i18n.G(`Pull app environment values from a deploymed app.

A convenient command for when you've lost your app environment file or want to
synchronize your local app environment values with what is deployed live.`),
	Example: i18n.G(`  # pull existing .env file and overwrite local values
  abra app env pull 1312.net --force

  # pull lost app .env file
  abra app env pull my.gitea.net --server 1312.net`),
	Args: cobra.MaximumNArgs(2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		appName := args[0]

		appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
		if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
			log.Fatal(i18n.G("%s already exists?", appEnvPath))
		}

		if server == "" {
			log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
		}

		serverDir := filepath.Join(config.SERVERS_DIR, server)
		if _, err := os.Stat(serverDir); os.IsNotExist(err) {
			log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
		}

		store := contextPkg.NewDefaultDockerContextStore()
		contexts, err := store.Store.List()
		if err != nil {
			log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
		}

		var contextCreated bool
		if server == "default" {
			contextCreated = true
		}

		for _, context := range contexts {
			if context.Name == server {
				contextCreated = true
			}
		}

		if !contextCreated {
			log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
		}

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

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

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", appName))
		}

		filters := filters.NewArgs()
		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
		targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
		if err != nil {
			log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
		}

		inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
		if err != nil {
			log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
		}

		deploymentEnv := make(map[string]string)
		for _, envVar := range inspectResult.Config.Env {
			split := strings.SplitN(envVar, "=", 2)
			if len(split) != 2 {
				log.Debug(i18n.G("no value attached to %s", envVar))
				continue
			}

			key, val := split[0], split[1]
			deploymentEnv[key] = val
		}

		log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))

		var (
			recipeEnvVar string
			recipeKey    string
		)

		if r, ok := deploymentEnv["TYPE"]; ok {
			recipeKey = "TYPE"
			recipeEnvVar = r
		}

		if r, ok := deploymentEnv["RECIPE"]; ok {
			recipeKey = "RECIPE"
			recipeEnvVar = r
		}

		if recipeEnvVar == "" {
			log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
		}

		var recipeName = recipeEnvVar
		if strings.Contains(recipeEnvVar, ":") {
			split := strings.Split(recipeEnvVar, ":")
			recipeName = split[0]
		}

		recipe := internal.ValidateRecipe(
			[]string{recipeName},
			cmd.Name(),
		)

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

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

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

		log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))

		for k, v := range deploymentEnv {
			mergedEnv[k] = v
		}

		if !strings.Contains(recipeEnvVar, ":") {
			mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
		}

		log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))

		envSample, err := os.ReadFile(recipe.SampleEnvPath)
		if err != nil {
			log.Fatal(err)
		}

		err = os.WriteFile(appEnvPath, envSample, 0o664)
		if err != nil {
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
		}

		read, err := os.ReadFile(appEnvPath)
		if err != nil {
			log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
		}

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

		var composeFileUpdated bool
		newContents := string(read)
		for key, val := range mergedEnv {
			if sampleEnv[key] == val {
				continue
			}

			if key == "COMPOSE_FILE" {
				composeFileUpdated = true
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
				log.Debug(i18n.G("uncommenting %s", key))
				re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
				log.Debug(i18n.G("uncommenting %s", key))
				re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
				log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
				re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
				log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
				re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
				log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
				re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
			}
		}

		err = os.WriteFile(appEnvPath, []byte(newContents), 0)
		if err != nil {
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
		}

		log.Info(i18n.G("%s successfully created", appEnvPath))

		if composeFileUpdated {
			log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
		}
	},
}

var AppEnvCommand = &cobra.Command{
	// translators: `app env` command group
	Use:     i18n.G("env [cmd] [args] [flags]"),
	Aliases: strings.Split(appEnvAliases, ","),
	// translators: Short description for `app env` command group
	Short: i18n.G("Manage app environment values"),
}

var (
	server string
)

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

	AppEnvPullCommand.Flags().StringVarP(
		&server,
		i18n.G("server"),
		i18n.G("s"),
		"",
		i18n.G("server associated with deployed app"),
	)

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