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/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"`
	AutoUpdate   string `json:"autoUpdate"`
	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"`
}

var AppListCommand = &cobra.Command{
	Use:     "list [flags]",
	Aliases: []string{"ls"},
	Short:   "List all managed apps",
	Long: `Generate a report of all managed apps.

Use "--status/-S" flag to query all servers for the live deployment status.`,
	Example: `  # 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 {
					status := "unknown"
					version := "unknown"
					chaos := "unknown"
					chaosVersion := "unknown"
					autoUpdate := "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 autoUpdateState, exists := statusMeta["autoUpdate"]; exists {
							autoUpdate = autoUpdateState
						}
						if statusMeta["status"] != "" {
							status = statusMeta["status"]
						}
						stats.VersionCount++
					} else {
						stats.UnversionedCount++
					}

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

					var newUpdates []string
					if version != "unknown" {
						updates, err := app.Recipe.Tags()
						if err != nil {
							log.Fatal(err)
						}

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

						for _, update := range updates {
							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 = "unknown"
						} else {
							appStats.Upgrade = "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{"RECIPE", "DOMAIN", "SERVER"}
			if status {
				headers = append(headers, []string{
					"STATUS",
					"CHAOS",
					"VERSION",
					"UPGRADE",
					"AUTOUPDATE"}...,
				)
			}

			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,
						appStat.AutoUpdate}...,
					)
				}

				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,
		"status",
		"S",
		false,
		"show app deployment status",
	)

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

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

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

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

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