package app

import (
	"context"
	"fmt"
	"os"
	"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/log"
	"coopcloud.tech/abra/pkg/secret"
	"github.com/docker/docker/api/types"
	dockerClient "github.com/docker/docker/client"
	"github.com/spf13/cobra"
)

var AppSecretGenerateCommand = &cobra.Command{
	Use:     "generate <domain> [[secret] [version] | --all] [flags]",
	Aliases: []string{"g"},
	Short:   "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 {
				errMsg := fmt.Sprintf("autocomplete failed: %s", err)
				return []string{errMsg}, 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) == 1 && !generateAllSecrets {
			log.Fatal("missing arguments [secret]/[version] or '--all'")
		}

		if len(args) > 1 && generateAllSecrets {
			log.Fatal("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.Fatalf("%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("no secrets generated")
			os.Exit(1)
		}

		headers := []string{"NAME", "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("unable to render to JSON: %s", err)
			}
			fmt.Println(out)
			return
		}

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

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

var AppSecretInsertCommand = &cobra.Command{
	Use:     "insert <domain> <secret> <version> <data> [flags]",
	Aliases: []string{"i"},
	Short:   "Insert secret",
	Long: `This command inserts a secret into an app environment.

This 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).`,
	Args: cobra.MinimumNArgs(4),
	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 := fmt.Sprintf("autocomplete failed: %s", err)
				return []string{errMsg}, 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 := args[3]

		if insertFromFile {
			raw, err := os.ReadFile(data)
			if err != nil {
				log.Fatalf("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, app.Server); err != nil {
			log.Fatal(err)
		}

		log.Infof("%s successfully stored on server", secretName)

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

// 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.Infof("deleted %s successfully from server", secretName)

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

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

	return nil
}

var AppSecretRmCommand = &cobra.Command{
	Use:     "remove <domain> [[secret] | --all] [flags]",
	Aliases: []string{"rm"},
	Short:   "Remove a secret",
	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 {
					errMsg := fmt.Sprintf("autocomplete failed: %s", err)
					return []string{errMsg}, 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("cannot use [secret] and --all/-a together")
		}

		if len(args) != 2 && !rmAllSecrets {
			log.Fatal("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.Fatalf("%s doesn't exist on server?", secretToRm)
		}

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

var AppSecretLsCommand = &cobra.Command{
	Use:     "list <domain>",
	Aliases: []string{"ls"},
	Short:   "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{"NAME", "VERSION", "GENERATED NAME", "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)
		}

		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("unable to render to JSON: %s", err)
				}
				fmt.Println(out)
				return
			}

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

			return
		}

		log.Warnf("no secrets stored for %s", app.Name)
	},
}

var AppSecretCommand = &cobra.Command{
	Use:     "secret [cmd] [args] [flags]",
	Aliases: []string{"s"},
	Short:   "Manage app secrets",
}

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

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

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

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

	AppSecretGenerateCommand.Flags().BoolVarP(
		&generateAllSecrets,
		"all",
		"a",
		false,
		"generate all secrets",
	)

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

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

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

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

	AppSecretRmCommand.Flags().BoolVarP(
		&rmAllSecrets,
		"all",
		"a",
		false,
		"remove all secrets",
	)

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

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

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

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