package catalogue

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

	"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/i18n"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/recipe"
	"github.com/go-git/go-git/v5"
	"github.com/spf13/cobra"
)

// translators: `abra catalogue sync` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueSyncAliases = i18n.G("s")

var CatalogueSyncCommand = &cobra.Command{
	// translators: `catalogue sync` command
	Use:     i18n.G("sync [flags]"),
	Aliases: strings.Split(appCatalogueSyncAliases, ","),
	// translators: Short description for `catalogue sync` command
	Short: i18n.G("Sync recipe catalogue for latest changes"),
	Args:  cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		if err := catalogue.EnsureCatalogue(); err != nil {
			log.Fatal(err)
		}

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

		log.Info(i18n.G("catalogue successfully synced"))
	},
}

// translators: `abra catalogue` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueAliases = i18n.G("g")

var CatalogueGenerateCommand = &cobra.Command{
	// translators: `catalogue generate` command
	Use:     i18n.G("generate [recipe] [flags]"),
	Aliases: strings.Split(appCatalogueAliases, ","),
	// translators: Short description for `catalogue generate` command
	Short: i18n.G("Generate the recipe catalogue"),
	Long: i18n.G(`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.

Publish 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. Enable ssh-agent and make sure to add
your private key and enter your passphrase beforehand.

    eval ` + "`ssh-agent`" + `
    ssh-add ~/.ssh/<my-ssh-private-key-for-git-coopcloud-tech>`),
	Example: `  # publish catalogue
  eval ` + "`ssh-agent`" + `
  ssh-add ~/.ssh/id_ed25519
  abra catalogue generate -p`,
	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 os.Getenv("SSH_AUTH_SOCK") == "" {
			log.Warn(i18n.G("ssh: SSH_AUTH_SOCK missing, --publish/-p will fail. see \"abra catalogue generate --help\""))
		}

		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, i18n.G("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.Info(i18n.G("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.Fatal(i18n.G("no changes discovered in %s, nothing to publish?", cataloguePath))
				}
			}

			msg := i18n.G("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.Info(i18n.G("new changes published: %s", url))
		}

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

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

var (
	publishChanges bool
	skipUpdates    bool
)

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

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

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

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