package app

import (
	"context"
	"fmt"
	"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/config"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	stack "coopcloud.tech/abra/pkg/upstream/stack"
	"github.com/docker/docker/api/types/filters"
	dockerClient "github.com/docker/docker/client"
	"github.com/spf13/cobra"
)

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

var AppUndeployCommand = &cobra.Command{
	// translators: `app undeploy` command
	Use: i18n.G("undeploy <domain> [flags]"),
	// translators: Short description for `app undeploy` command
	Aliases: strings.Split(appUndeployAliases, ","),
	Short:   i18n.G("Undeploy a deployed app"),
	Long: i18n.G(`This does not destroy any application data.

However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.

Passing "--prune/-p" does not remove those volumes.`),
	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)
		stackName := app.StackName()

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

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

		log.Debug(i18n.G("checking whether %s is already deployed", stackName))

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

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

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

		if err := internal.DeployOverview(
			app,
			version,
			config.MISSING_DEFAULT,
			"",
			nil,
			nil,
			nil,
			nil,
		); err != nil {
			log.Fatal(err)
		}

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

		opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
		compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		rmOpts := stack.Remove{
			Namespaces: []string{stackName},
			Detach:     false,
		}
		if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
			log.Fatal(err)
		}

		if prune {
			if err := pruneApp(cl, app); err != nil {
				log.Fatal(err)
			}
		}

		log.Info(i18n.G("undeploy succeeded 🟢"))

		if err := app.WriteRecipeVersion(version, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}

// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
	stackName := app.StackName()
	ctx := context.Background()

	pruneFilters := filters.NewArgs()
	stackSearch := fmt.Sprintf("%s*", stackName)
	pruneFilters.Add("label", stackSearch)
	cr, err := cl.ContainersPrune(ctx, pruneFilters)
	if err != nil {
		return err
	}

	cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
	log.Info(i18n.G("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed))

	nr, err := cl.NetworksPrune(ctx, pruneFilters)
	if err != nil {
		return err
	}

	log.Info(i18n.G("networks pruned: %d", len(nr.NetworksDeleted)))

	ir, err := cl.ImagesPrune(ctx, pruneFilters)
	if err != nil {
		return err
	}

	imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
	log.Info(i18n.G("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed))

	return nil
}

var (
	prune bool
)

func init() {
	AppUndeployCommand.Flags().BoolVarP(
		&prune,
		i18n.G("prune"),
		i18n.G("p"),
		false,
		i18n.G("prune unused containers, networks, and dangling images"),
	)
}
