package recipe

import (
	"fmt"
	"strconv"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/autocomplete"
	gitPkg "coopcloud.tech/abra/pkg/git"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/tagcmp"
	"github.com/AlecAivazis/survey/v2"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/spf13/cobra"
)

var RecipeSyncCommand = &cobra.Command{
	Use:     "sync <recipe> [version] [flags]",
	Aliases: []string{"s"},
	Short:   "Sync recipe version label",
	Long: `Generate labels for the main recipe service.

By convention, the service named "app" using the following format:

    coop-cloud.${STACK_NAME}.version=<version>

Where [version] can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`,
	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.RecipeNameComplete()
		case 1:
			return autocomplete.RecipeVersionComplete(args[0])
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		recipe := internal.ValidateRecipe(args, cmd.Name())

		mainApp, err := internal.GetMainAppImage(recipe)
		if err != nil {
			log.Fatal(err)
		}

		imagesTmp, err := getImageVersions(recipe)
		if err != nil {
			log.Fatal(err)
		}

		mainAppVersion := imagesTmp[mainApp]

		tags, err := recipe.Tags()
		if err != nil {
			log.Fatal(err)
		}

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

		if len(tags) == 0 && nextTag == "" {
			log.Warnf("no git tags found for %s", recipe.Name)
			if internal.NoInput {
				log.Fatalf("unable to continue, input required for initial version")
			}
			fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can
pick for %s that will be published in the recipe catalogue. This follows the
semver convention (more on https://semver.org), here is a short cheatsheet

    0.1.0: development release, still hacking. when you make a major upgrade
           you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
           using the "x" part when things are stable.

    1.0.0: public release, assumed to be working. you already have a stable
           and reliable deployment of this app and feel relatively confident
           about it.

If you want people to be able alpha test your current config for %s but don't
think it is quite reliable, go with 0.1.0 and people will know that things are
likely to change.

`, recipe.Name, recipe.Name))
			var chosenVersion string
			edPrompt := &survey.Select{
				Message: "which version do you want to begin with?",
				Options: []string{"0.1.0", "1.0.0"},
			}

			if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
				log.Fatal(err)
			}

			nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
		}

		if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
			latestRelease := tags[len(tags)-1]
			if err := internal.PromptBumpType("", latestRelease); err != nil {
				log.Fatal(err)
			}
		}

		if nextTag == "" {
			repo, err := git.PlainOpen(recipe.Dir)
			if err != nil {
				log.Fatal(err)
			}

			var lastGitTag tagcmp.Tag
			iter, err := repo.Tags()
			if err != nil {
				log.Fatal(err)
			}

			if err := iter.ForEach(func(ref *plumbing.Reference) error {
				obj, err := repo.TagObject(ref.Hash())
				if err != nil {
					log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
					return err
				}

				tagcmpTag, err := tagcmp.Parse(obj.Name)
				if err != nil {
					return err
				}

				if (lastGitTag == tagcmp.Tag{}) {
					lastGitTag = tagcmpTag
				} else if tagcmpTag.IsGreaterThan(lastGitTag) {
					lastGitTag = tagcmpTag
				}

				return nil
			}); err != nil {
				log.Fatal(err)
			}

			// bumpType is used to decide what part of the tag should be incremented
			bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
			if bumpType != 0 {
				// a bitwise check if the number is a power of 2
				if (bumpType & (bumpType - 1)) != 0 {
					log.Fatal("you can only use one version flag: --major, --minor or --patch")
				}
			}

			newTag := lastGitTag
			if bumpType > 0 {
				if internal.Patch {
					now, err := strconv.Atoi(newTag.Patch)
					if err != nil {
						log.Fatal(err)
					}

					newTag.Patch = strconv.Itoa(now + 1)
				} else if internal.Minor {
					now, err := strconv.Atoi(newTag.Minor)
					if err != nil {
						log.Fatal(err)
					}

					newTag.Patch = "0"
					newTag.Minor = strconv.Itoa(now + 1)
				} else if internal.Major {
					now, err := strconv.Atoi(newTag.Major)
					if err != nil {
						log.Fatal(err)
					}

					newTag.Patch = "0"
					newTag.Minor = "0"
					newTag.Major = strconv.Itoa(now + 1)
				}
			}

			newTag.Metadata = mainAppVersion
			log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
			nextTag = newTag.String()
		}

		if _, err := tagcmp.Parse(nextTag); err != nil {
			log.Fatalf("invalid version %s specified", nextTag)
		}

		mainService := "app"
		label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
		if !internal.Dry {
			if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
				log.Fatal(err)
			}
		} else {
			log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
		}

		isClean, err := gitPkg.IsClean(recipe.Dir)
		if err != nil {
			log.Fatal(err)
		}
		if !isClean {
			log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
			if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
				log.Fatal(err)
			}
		}
	},
}

func init() {
	RecipeSyncCommand.Flags().BoolVarP(
		&internal.Dry,
		"dry-run",
		"r",
		false,
		"report changes that would be made",
	)

	RecipeSyncCommand.Flags().BoolVarP(
		&internal.Major,
		"major",
		"x",
		false,
		"increase the major part of the version",
	)

	RecipeSyncCommand.Flags().BoolVarP(
		&internal.Minor,
		"minor",
		"y",
		false,
		"increase the minor part of the version",
	)

	RecipeSyncCommand.Flags().BoolVarP(
		&internal.Patch,
		"patch",
		"z",
		false,
		"increase the patch part of the version",
	)
}
