package app

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"sort"
	"strconv"
	"strings"

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

// translators: `abra app secret generate` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretGenerateAliases = i18n.G("g")

var AppSecretGenerateCommand = &cobra.Command{
	// translators: `app secret generate` command
	Use:     i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
	Aliases: strings.Split(appSecretGenerateAliases, ","),
	// translators: Short description for `app secret generate` command
	Short: i18n.G("Generate secrets"),
	Args:  cobra.RangeArgs(1, 3),
	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.SecretComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

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

		if len(args) <= 2 && !generateAllSecrets {
			log.Fatal(i18n.G("missing arguments [secret]/[version] or '--all'"))
		}

		if len(args) > 2 && generateAllSecrets {
			log.Fatal(i18n.G("cannot use '[secret] [version]' and '--all' together"))
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !generateAllSecrets {
			secretName := args[1]
			secretVersion := args[2]
			s, ok := secrets[secretName]
			if !ok {
				log.Fatal(i18n.G("%s doesn't exist in the env config?", secretName))
			}
			s.Version = secretVersion
			secrets = map[string]secret.Secret{
				secretName: s,
			}
		}

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

		secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
		if err != nil {
			log.Fatal(err)
		}

		if storeInPass {
			for name, data := range secretVals {
				if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
					log.Fatal(err)
				}
			}
		}

		if len(secretVals) == 0 {
			log.Warn(i18n.G("no secrets generated"))
			os.Exit(1)
		}

		headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.Headers(headers...)

		var rows [][]string
		for name, val := range secretVals {
			row := []string{name, val}
			rows = append(rows, row)
			table.Row(row...)
		}

		if internal.MachineReadable {
			out, err := formatter.ToJSON(headers, rows)
			if err != nil {
				log.Fatal(i18n.G("unable to render to JSON: %s", err))
			}
			fmt.Println(out)
			return
		}

		if err := formatter.PrintTable(table); err != nil {
			log.Fatal(err)
		}

		log.Warn(i18n.G(
			"generated secrets %s shown again, please take note of them %s",
			formatter.BoldStyle.Render(i18n.G("NOT")),
			formatter.BoldStyle.Render(i18n.G("NOW")),
		))
	},
}

// translators: `abra app secret insert` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretInsertAliases = i18n.G("i")

var AppSecretInsertCommand = &cobra.Command{
	// translators: `app secret insert` command
	Use:     i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
	Aliases: strings.Split(appSecretInsertAliases, ","),
	// translators: Short description for `app secret insert` command
	Short: i18n.G("Insert secret"),
	Long: i18n.G(`This command inserts a secret into an app environment.

Arbitrary secret insertion is not supported. Secrets that are inserted must
match those configured in the recipe beforehand.

This command can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets/-S" for more).`),
	Example: i18n.G(`  # insert regular secret
  abra app secret insert 1312.net my_secret v1 mySuperSecret

  # insert secret as file
  abra app secret insert 1312.net my_secret v1 secret.txt -f

  # insert secret from stdin
  echo "mmySuperSecret" | abra app secret insert 1312.net my_secret v1`),
	Args: cobra.MinimumNArgs(3),
	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.SecretComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []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)
		}

		name := args[1]
		version := args[2]
		data, err := readSecretData(args)
		if err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		var isRecipeSecret bool
		for secretName := range secrets {
			if secretName == name {
				isRecipeSecret = true
			}
		}
		if !isRecipeSecret {
			log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
		}

		if insertFromFile {
			raw, err := os.ReadFile(data)
			if err != nil {
				log.Fatal(i18n.G("reading secret from file: %s", err))
			}
			data = string(raw)
		}

		if trimInput {
			data = strings.TrimSpace(data)
		}

		secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
		if err := client.StoreSecret(cl, secretName, data); err != nil {
			log.Fatal(err)
		}

		log.Info(i18n.G("%s successfully stored on server", secretName))

		if storeInPass {
			if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
				log.Fatal(err)
			}
		}
	},
}

func readSecretData(args []string) (string, error) {
	if len(args) == 4 {
		return args[3], nil
	}

	if len(args) != 3 {
		return "", errors.New(i18n.G("need 3 or 4 arguments"))
	}
	// First check if data is provided by stdin
	fi, err := os.Stdin.Stat()
	if err != nil {
		return "", err
	}
	if fi.Mode()&os.ModeNamedPipe != 0 {
		// Can't insert from stdin and read from file
		if insertFromFile {
			return "", errors.New(i18n.G("can not insert from file and read from stdin"))
		}

		log.Debug(i18n.G("reading secret data from stdin"))
		bytes, err := io.ReadAll(os.Stdin)
		if err != nil {
			return "", errors.New(i18n.G("reading data from stdin: %s", err))
		}

		return string(bytes), nil
	}
	if internal.NoInput {
		return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
	}

	log.Debug(i18n.G("secret data not provided on command-line or stdin, prompting"))
	var prompt survey.Prompt
	if !insertFromFile {
		prompt = &survey.Password{
			Message: i18n.G("specify secret value"),
		}
	} else {
		prompt = &survey.Input{
			Message: i18n.G("specify secret file"),
		}
	}
	var data string
	if err := survey.AskOne(prompt, &data); err != nil {
		return "", err
	}
	return data, nil
}

// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
	if err := cl.SecretRemove(context.Background(), secretName); err != nil {
		return err
	}

	log.Info(i18n.G("deleted %s successfully from server", secretName))

	if removeFromPass {
		if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
			return err
		}

		log.Info(i18n.G("deleted %s successfully from local pass store", secretName))
	}

	return nil
}

// translators: `abra app secret remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretRemoveAliases = i18n.G("rm")

var AppSecretRmCommand = &cobra.Command{
	// translators: `app secret remove` command
	Use:     i18n.G("remove <domain> [[secret] | --all] [flags]"),
	Aliases: strings.Split(appSecretRemoveAliases, ","),
	// translators: Short description for `app secret remove` command
	Short: i18n.G("Remove a secret"),
	Long: i18n.G(`This command removes a secret from an app environment.

Arbitrary secret removal is not supported. Secrets that are removed must
match those configured in the recipe beforehand.`),
	Example: i18n.G("  abra app secret rm 1312.net oauth_key"),
	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:
			if !rmAllSecrets {
				app, err := appPkg.Get(args[0])
				if err != nil {
					return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
				}
				return autocomplete.SecretComplete(app.Recipe.Name)
			}
			return nil, cobra.ShellCompDirectiveDefault
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

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

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if len(args) == 2 && rmAllSecrets {
			log.Fatal(i18n.G("cannot use [secret] and --all/-a together"))
		}

		if len(args) != 2 && !rmAllSecrets {
			log.Fatal(i18n.G("no secret(s) specified?"))
		}

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

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

		secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
		if err != nil {
			log.Fatal(err)
		}

		remoteSecretNames := make(map[string]bool)
		for _, cont := range secretList {
			remoteSecretNames[cont.Spec.Annotations.Name] = true
		}

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

		match := false
		for secretName, val := range secrets {
			secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
			if _, ok := remoteSecretNames[secretRemoteName]; ok {
				if secretToRm != "" {
					if secretName == secretToRm {
						if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
							log.Fatal(err)
						}

						return
					}
				} else {
					match = true

					if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
						log.Fatal(err)
					}
				}
			}
		}

		if !match && secretToRm != "" {
			log.Fatal(i18n.G("%s doesn't exist on server?", secretToRm))
		}

		if !match {
			log.Fatal(i18n.G("no secrets to remove?"))
		}
	},
}

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

var AppSecretLsCommand = &cobra.Command{
	// translators: `app secret list` command
	Use:     i18n.G("list <domain>"),
	Aliases: strings.Split(appSecretLsAliases, ","),
	// translators: Short description for `app secret list` command
	Short: i18n.G("List all secrets"),
	Args:  cobra.MinimumNArgs(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)

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

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

		headers := []string{i18n.G("NAME"), i18n.G("VERSION"), i18n.G("GENERATED NAME"), i18n.G("CREATED ON SERVER")}
		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.Headers(headers...)

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

		// Sort secrets to ensure reproducible output
		sort.Slice(secStats, func(i, j int) bool {
			return secStats[i].LocalName < secStats[j].LocalName
		})
		var rows [][]string
		for _, secStat := range secStats {
			row := []string{
				secStat.LocalName,
				secStat.Version,
				secStat.RemoteName,
				strconv.FormatBool(secStat.CreatedOnRemote),
			}

			rows = append(rows, row)
			table.Row(row...)
		}

		if len(rows) > 0 {
			if internal.MachineReadable {
				out, err := formatter.ToJSON(headers, rows)
				if err != nil {
					log.Fatal(i18n.G("unable to render to JSON: %s", err))
				}
				fmt.Println(out)
				return
			}

			if err := formatter.PrintTable(table); err != nil {
				log.Fatal(err)
			}

			return
		}

		log.Warn(i18n.G("no secrets stored for %s", app.Name))
	},
}

var AppSecretCommand = &cobra.Command{
	// translators: `app secret` command group
	Use:     i18n.G("secret [cmd] [args] [flags]"),
	Aliases: []string{i18n.G("s")},
	// translators: Short description for `app secret` command group
	Short: i18n.G("Manage app secrets"),
}

var (
	storeInPass        bool
	insertFromFile     bool
	trimInput          bool
	rmAllSecrets       bool
	generateAllSecrets bool
	removeFromPass     bool
)

func init() {
	AppSecretGenerateCommand.Flags().BoolVarP(
		&internal.MachineReadable,
		i18n.G("machine"),
		i18n.G("m"),
		false,
		i18n.G("print machine-readable output"),
	)

	AppSecretGenerateCommand.Flags().BoolVarP(
		&storeInPass,
		i18n.G("pass"),
		i18n.G("p"),
		false,
		i18n.G("store generated secrets in a local pass store"),
	)

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

	AppSecretGenerateCommand.Flags().BoolVarP(
		&generateAllSecrets,
		i18n.G("all"),
		i18n.GC("a", "app secret generate"),
		false,
		i18n.G("generate all secrets"),
	)

	AppSecretInsertCommand.Flags().BoolVarP(
		&storeInPass,
		i18n.G("pass"),
		i18n.G("p"),
		false,
		i18n.G("store generated secrets in a local pass store"),
	)

	AppSecretInsertCommand.Flags().BoolVarP(
		&insertFromFile,
		i18n.G("file"),
		i18n.G("f"),
		false,
		i18n.G("treat input as a file"),
	)

	AppSecretInsertCommand.Flags().BoolVarP(
		&trimInput,
		i18n.G("trim"),
		i18n.G("t"),
		false,
		i18n.G("trim input"),
	)

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

	AppSecretRmCommand.Flags().BoolVarP(
		&rmAllSecrets,
		i18n.G("all"),
		i18n.GC("a", "app secret rm"),
		false,
		i18n.G("remove all secrets"),
	)

	AppSecretRmCommand.Flags().BoolVarP(
		&removeFromPass,
		i18n.G("pass"),
		i18n.G("p"),
		false,
		i18n.G("remove generated secrets from a local pass store"),
	)

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

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

	AppSecretLsCommand.Flags().BoolVarP(
		&internal.MachineReadable,
		i18n.G("machine"),
		i18n.G("m"),
		false,
		i18n.G("print machine-readable output"),
	)
}
