package app

import (
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"coopcloud.tech/abra/cli/internal"
	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/tagcmp"
	"github.com/spf13/cobra"
)

type appStatus struct {
	Server       string `json:"server"`
	Recipe       string `json:"recipe"`
	AppName      string `json:"appName"`
	Domain       string `json:"domain"`
	Status       string `json:"status"`
	Chaos        string `json:"chaos"`
	ChaosVersion string `json:"chaosVersion"`
	Version      string `json:"version"`
	Upgrade      string `json:"upgrade"`
}

type serverStatus struct {
	Apps             []appStatus `json:"apps"`
	AppCount         int         `json:"appCount"`
	VersionCount     int         `json:"versionCount"`
	UnversionedCount int         `json:"unversionedCount"`
	LatestCount      int         `json:"latestCount"`
	UpgradeCount     int         `json:"upgradeCount"`
}

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

var AppListCommand = &cobra.Command{
	// translators: `app list` command
	Use:     i18n.G("list [flags]"),
	Aliases: strings.Split(appListAliases, ","),
	// translators: Short description for `app list` command
	Short: i18n.G("List all managed apps"),
	Long: i18n.G(`Generate a report of all managed apps.

Use "--status/-S" flag to query all servers for the live deployment status.`),
	Example: i18n.G(`  # list apps of all servers without live status
  abra app ls

  # list apps of a specific server with live status
  abra app ls -s 1312.net -S

  # list apps of all servers which match a specific recipe
  abra app ls -r gitea`),
	Args: cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		appFiles, err := appPkg.LoadAppFiles(listAppServer)
		if err != nil {
			log.Fatal(err)
		}

		apps, err := appPkg.GetApps(appFiles, recipeFilter)
		if err != nil {
			log.Fatal(err)
		}

		sort.Sort(appPkg.ByServerAndRecipe(apps))

		statuses := make(map[string]map[string]string)
		if status {
			alreadySeen := make(map[string]bool)
			for _, app := range apps {
				if _, ok := alreadySeen[app.Server]; !ok {
					alreadySeen[app.Server] = true
				}
			}

			statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
			if err != nil {
				log.Fatal(err)
			}
		}

		var totalServersCount int
		var totalAppsCount int
		allStats := make(map[string]serverStatus)
		for _, app := range apps {
			var stats serverStatus
			var ok bool
			if stats, ok = allStats[app.Server]; !ok {
				stats = serverStatus{}
				if recipeFilter == "" {
					// count server, no filtering
					totalServersCount++
				}
			}

			if app.Recipe.Name == recipeFilter || recipeFilter == "" {
				if recipeFilter != "" {
					// only count server if matches filter
					totalServersCount++
				}

				appStats := appStatus{}
				stats.AppCount++
				totalAppsCount++

				if status {
					if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
						log.Fatal(err)
					}

					status := i18n.G("unknown")
					version := i18n.G("unknown")
					chaos := i18n.G("unknown")
					chaosVersion := i18n.G("unknown")
					if statusMeta, ok := statuses[app.StackName()]; ok {
						if currentVersion, exists := statusMeta["version"]; exists {
							if currentVersion != "" {
								version = currentVersion
							}
						}
						if chaosDeploy, exists := statusMeta["chaos"]; exists {
							chaos = chaosDeploy
						}
						if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
							chaosVersion = chaosDeployVersion
						}
						if statusMeta["status"] != "" {
							status = statusMeta["status"]
						}
						stats.VersionCount++
					} else {
						stats.UnversionedCount++
					}

					appStats.Status = status
					appStats.Chaos = chaos
					appStats.ChaosVersion = chaosVersion
					appStats.Version = version

					var newUpdates []string
					if version != "unknown" && chaos == "false" {
						if err := app.Recipe.EnsureExists(); err != nil {
							log.Fatal(i18n.G("unable to clone %s: %s", app.Name, err))
						}

						updates, err := app.Recipe.Tags()
						if err != nil {
							log.Fatal(i18n.G("unable to retrieve tags for %s: %s", app.Name, err))
						}

						parsedVersion, err := tagcmp.Parse(version)
						if err != nil {
							log.Fatal(err)
						}

						for _, update := range updates {
							if ok := tagcmp.IsParsable(update); !ok {
								log.Debug(i18n.G("unable to parse %s, skipping as upgrade option", update))
								continue
							}

							parsedUpdate, err := tagcmp.Parse(update)
							if err != nil {
								log.Fatal(err)
							}

							if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
								newUpdates = append(newUpdates, update)
							}
						}
					}

					if len(newUpdates) == 0 {
						if version == "unknown" {
							appStats.Upgrade = i18n.G("unknown")
						} else {
							appStats.Upgrade = i18n.G("latest")
							stats.LatestCount++
						}
					} else {
						newUpdates = internal.SortVersionsDesc(newUpdates)
						appStats.Upgrade = strings.Join(newUpdates, "\n")
						stats.UpgradeCount++
					}
				}

				appStats.Server = app.Server
				appStats.Recipe = app.Recipe.Name
				appStats.AppName = app.Name
				appStats.Domain = app.Domain

				stats.Apps = append(stats.Apps, appStats)
			}
			allStats[app.Server] = stats
		}

		if internal.MachineReadable {
			jsonstring, err := json.Marshal(allStats)
			if err != nil {
				log.Fatal(err)
			} else {
				fmt.Println(string(jsonstring))
			}

			return
		}

		alreadySeen := make(map[string]bool)
		for _, app := range apps {
			if _, ok := alreadySeen[app.Server]; ok {
				continue
			}

			serverStat := allStats[app.Server]

			headers := []string{i18n.G("RECIPE"), i18n.G("DOMAIN"), i18n.G("SERVER")}
			if status {
				headers = append(headers, []string{
					i18n.G("STATUS"),
					i18n.G("CHAOS"),
					i18n.G("VERSION"),
					i18n.G("UPGRADE"),
				}...,
				)
			}

			table, err := formatter.CreateTable()
			if err != nil {
				log.Fatal(err)
			}

			table.Headers(headers...)

			var rows [][]string
			for _, appStat := range serverStat.Apps {
				row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
				if status {
					chaosStatus := appStat.Chaos
					if chaosStatus != "unknown" {
						chaosEnabled, err := strconv.ParseBool(chaosStatus)
						if err != nil {
							log.Fatal(err)
						}
						if chaosEnabled && appStat.ChaosVersion != "unknown" {
							chaosStatus = appStat.ChaosVersion
						}
					}

					row = append(row, []string{
						appStat.Status,
						chaosStatus,
						appStat.Version,
						appStat.Upgrade}...,
					)
				}

				rows = append(rows, row)
			}

			table.Rows(rows...)

			if len(rows) > 0 {
				if err := formatter.PrintTable(table); err != nil {
					log.Fatal(err)
				}

				if len(allStats) > 1 && len(rows) > 0 {
					fmt.Println() // newline separator for multiple servers
				}
			}

			alreadySeen[app.Server] = true
		}
	},
}

var (
	status        bool
	recipeFilter  string
	listAppServer string
)

func init() {
	AppListCommand.Flags().BoolVarP(
		&status,
		i18n.G("status"),
		i18n.G("S"),
		false,
		i18n.G("show app deployment status"),
	)

	AppListCommand.Flags().StringVarP(
		&recipeFilter,
		i18n.G("recipe"),
		i18n.G("r"),
		"",
		i18n.G("show apps of a specific recipe"),
	)

	AppListCommand.RegisterFlagCompletionFunc(
		i18n.G("recipe"),
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return autocomplete.RecipeNameComplete()
		},
	)

	AppListCommand.Flags().BoolVarP(
		&internal.MachineReadable,
		i18n.G("machine"),
		i18n.G("m"),
		false,
		i18n.G("print machine-readable output"),
	)

	AppListCommand.Flags().StringVarP(
		&listAppServer,
		i18n.G("server"),
		i18n.G("s"),
		"",
		i18n.G("show apps of a specific server"),
	)

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

	AppListCommand.RegisterFlagCompletionFunc(
		i18n.G("server"),
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return autocomplete.ServerNameComplete()
		},
	)
}
