package catalogue

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path"
	"slices"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/catalogue"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/formatter"
	gitPkg "coopcloud.tech/abra/pkg/git"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/recipe"
	"github.com/go-git/go-git/v5"
	"github.com/spf13/cobra"
)

var CatalogueGenerateCommand = &cobra.Command{
	Use:     "generate [recipe] [flags]",
	Aliases: []string{"g"},
	Short:   "Generate the recipe catalogue",
	Long: `Generate a new copy of the recipe catalogue.

N.B. this command **will** wipe local unstaged changes from your local recipes
if present. "--chaos/-C" on this command refers to the catalogue repository
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
changes.

It is possible to generate new metadata for a single recipe by passing
[recipe]. The existing local catalogue will be updated, not overwritten.

It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can "docker login" and Abra will automatically
use those details.

Push your new release to git.coopcloud.tech with "--publish/-p". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.`,
	Args: cobra.RangeArgs(0, 1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.RecipeNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		var recipeName string
		if len(args) > 0 {
			recipeName = args[0]
		}

		if recipeName != "" {
			internal.ValidateRecipe(args, cmd.Name())
		}

		if err := catalogue.EnsureCatalogue(); err != nil {
			log.Fatal(err)
		}

		if !internal.Chaos {
			if err := catalogue.EnsureIsClean(); err != nil {
				log.Fatal(err)
			}
		}

		repos, err := recipe.ReadReposMetadata(internal.Debug)
		if err != nil {
			log.Fatal(err)
		}

		barLength := len(repos)
		if recipeName != "" {
			barLength = 1
		}

		if !skipUpdates {
			if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
				log.Fatal(err)
			}
		}

		var warnings []string
		catl := make(recipe.RecipeCatalogue)
		catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata")
		for _, recipeMeta := range repos {
			if recipeName != "" && recipeName != recipeMeta.Name {
				if !internal.Debug {
					catlBar.Add(1)
				}
				continue
			}

			r := recipe.Get(recipeMeta.Name)
			versions, warnMsgs, err := r.GetRecipeVersions()
			if err != nil {
				warnings = append(warnings, err.Error())
			}
			if len(warnMsgs) > 0 {
				warnings = append(warnings, warnMsgs...)
			}

			features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
			if err != nil {
				warnings = append(warnings, err.Error())
			}
			if len(warnMsgs) > 0 {
				warnings = append(warnings, warnMsgs...)
			}

			catl[recipeMeta.Name] = recipe.RecipeMeta{
				Name:          recipeMeta.Name,
				Repository:    recipeMeta.CloneURL,
				SSHURL:        recipeMeta.SSHURL,
				Icon:          recipeMeta.AvatarURL,
				DefaultBranch: recipeMeta.DefaultBranch,
				Description:   recipeMeta.Description,
				Website:       recipeMeta.Website,
				Versions:      versions,
				Category:      category,
				Features:      features,
			}

			if !internal.Debug {
				catlBar.Add(1)
			}
		}

		if err := catlBar.Close(); err != nil {
			log.Fatal(err)
		}

		var uniqueWarnings []string
		for _, w := range warnings {
			if !slices.Contains(uniqueWarnings, w) {
				uniqueWarnings = append(uniqueWarnings, w)
			}
		}

		for _, warnMsg := range uniqueWarnings {
			log.Warn(warnMsg)
		}

		recipesJSON, err := json.MarshalIndent(catl, "", "    ")
		if err != nil {
			log.Fatal(err)
		}

		if recipeName == "" {
			if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
				log.Fatal(err)
			}
		} else {
			catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
			if err != nil {
				log.Fatal(err)
			}

			catlFS[recipeName] = catl[recipeName]

			updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", "    ")
			if err != nil {
				log.Fatal(err)
			}

			if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
				log.Fatal(err)
			}
		}

		log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON)

		cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
		if publishChanges {

			isClean, err := gitPkg.IsClean(cataloguePath)
			if err != nil {
				log.Fatal(err)
			}

			if isClean {
				if !internal.Dry {
					log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
				}
			}

			msg := "chore: publish new catalogue release changes"
			if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
				log.Fatal(err)
			}

			repo, err := git.PlainOpen(cataloguePath)
			if err != nil {
				log.Fatal(err)
			}

			sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
			if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
				log.Fatal(err)
			}

			if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
				log.Fatal(err)
			}
		}

		repo, err := git.PlainOpen(cataloguePath)
		if err != nil {
			log.Fatal(err)
		}

		head, err := repo.Head()
		if err != nil {
			log.Fatal(err)
		}

		if !internal.Dry && publishChanges {
			url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
			log.Infof("new changes published: %s", url)
		}

		if internal.Dry {
			log.Info("dry run: no changes published")
		}
	},
}

// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cobra.Command{
	Use:     "catalogue [cmd] [args] [flags]",
	Short:   "Manage the recipe catalogue",
	Aliases: []string{"c"},
}

var (
	publishChanges bool
	skipUpdates    bool
)

func init() {
	CatalogueGenerateCommand.Flags().BoolVarP(
		&publishChanges,
		"publish",
		"p",
		false,
		"publish changes to git.coopcloud.tech",
	)

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

	CatalogueGenerateCommand.Flags().BoolVarP(
		&skipUpdates,
		"skip-updates",
		"s",
		false,
		"skip updating recipe repositories",
	)

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