package app

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/client"
	stack "coopcloud.tech/abra/pkg/upstream/stack"
	"github.com/AlecAivazis/survey/v2"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/volume"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

var appRemoveCommand = cli.Command{
	Name:      "remove",
	Aliases:   []string{"rm"},
	ArgsUsage: "<domain>",
	Usage:     "Remove all app data, locally and remotely",
	Description: `
This command removes everything related to an app which is already undeployed.

By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.

Only run this command when you are sure you want to completely remove the app
and all associated app data. This is a destructive action, Be Careful!

If you would like to delete specific volumes or secrets, please use removal
sub-commands under "app volume" and "app secret" instead.

Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards.

To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.
`,
	Flags: []cli.Flag{
		internal.ForceFlag,
		internal.DebugFlag,
		internal.NoInputFlag,
		internal.OfflineFlag,
	},
	BashComplete: autocomplete.AppNameComplete,
	Before:       internal.SubCommandBefore,
	Action: func(c *cli.Context) error {
		app := internal.ValidateApp(c)

		if !internal.Force && !internal.NoInput {
			response := false
			msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
			prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
			if err := survey.AskOne(prompt, &response); err != nil {
				logrus.Fatal(err)
			}
			if !response {
				logrus.Fatal("aborting as requested")
			}
		}

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

		isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			logrus.Fatal(err)
		}
		if isDeployed {
			logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
		}

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

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

		secrets := make(map[string]string)
		var secretNames []string

		for _, cont := range secretList {
			secrets[cont.Spec.Annotations.Name] = cont.ID // we have to map the names to ID's
			secretNames = append(secretNames, cont.Spec.Annotations.Name)
		}

		if len(secrets) > 0 {
			for _, name := range secretNames {
				err := cl.SecretRemove(context.Background(), secrets[name])
				if err != nil {
					logrus.Fatal(err)
				}
				logrus.Info(fmt.Sprintf("secret: %s removed", name))
			}
		} else {
			logrus.Info("no secrets to remove")
		}

		fs, err = app.Filters(false, true)
		if err != nil {
			logrus.Fatal(err)
		}

		volumeListOptions := volume.ListOptions{fs}
		volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions)
		volumeList := volumeListOKBody.Volumes
		if err != nil {
			logrus.Fatal(err)
		}

		var vols []string
		for _, vol := range volumeList {
			vols = append(vols, vol.Name)
		}

		if len(vols) > 0 {
			for _, vol := range vols {
				err = retryFunc(5, func() error {
					return cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
				})
				if err != nil {
					log.Fatalf("removing volumes failed: %s", err)
				}
				logrus.Info(fmt.Sprintf("volume %s removed", vol))
			}
		} else {
			logrus.Info("no volumes to remove")
		}

		if err = os.Remove(app.Path); err != nil {
			logrus.Fatal(err)
		}

		logrus.Info(fmt.Sprintf("file: %s removed", app.Path))

		return nil
	},
}

// retryFunc retries the given function for the given retries. After the nth
// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0).
// It returns an error if the function still failed after the last retry.
func retryFunc(retries int, fn func() error) error {
	for i := 0; i < retries; i++ {
		err := fn()
		if err == nil {
			return nil
		}
		if i+1 < retries {
			sleep := time.Duration(i+1) * time.Duration(i+1)
			logrus.Infof("%s: waiting %d seconds before next retry", err, sleep)
			time.Sleep(sleep * time.Second)
		}
	}
	return fmt.Errorf("%d retries failed", retries)
}