package curseforge

import (
	"bufio"
	"errors"
	"fmt"
	"github.com/sahilm/fuzzy"
	"github.com/spf13/viper"
	"os"
	"strings"

	"github.com/packwiz/packwiz/core"
	"github.com/spf13/cobra"
	"gopkg.in/dixonwille/wmenu.v4"
)

const maxCycles = 20

type installableDep struct {
	modInfo
	fileInfo modFileInfo
}

// installCmd represents the install command
var installCmd = &cobra.Command{
	Use:     "install [mod]",
	Short:   "Install a mod from a curseforge URL, slug, ID or search",
	Aliases: []string{"add", "get"},
	Args:    cobra.ArbitraryArgs,
	Run: func(cmd *cobra.Command, args []string) {
		pack, err := core.LoadPack()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		index, err := pack.LoadIndex()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		mcVersion, err := pack.GetMCVersion()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		var done bool
		var modID, fileID int
		// If mod/file IDs are provided in command line, use those
		// TODO: use file id with slug if it exists?
		if addonIDFlag != 0 {
			modID = addonIDFlag
			fileID = fileIDFlag
			done = true
		}
		if (len(args) == 0 || len(args[0]) == 0) && !done {
			fmt.Println("You must specify a mod.")
			os.Exit(1)
		}
		// If there are more than 1 argument, go straight to searching - URLs/Slugs should not have spaces!
		if !done && len(args) == 1 {
			done, modID, fileID, err = getFileIDsFromString(args[0])
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}

			if !done {
				done, modID, err = getModIDFromString(args[0])
				// Ignore error, go to search instead (e.g. lowercase to search instead of as a slug)
				if err != nil {
					done = false
				}
			}
		}

		modInfoObtained := false
		var modInfoData modInfo

		if !done {
			var cancelled bool
			cancelled, modInfoData = searchCurseforgeInternal(args, mcVersion, getLoader(pack))
			if cancelled {
				return
			}
			done = true
			modID = modInfoData.ID
			modInfoObtained = true
		}

		if !done {
			if err == nil {
				fmt.Println("No mods found!")
				os.Exit(1)
			}
			fmt.Println(err)
			os.Exit(1)
		}

		if !modInfoObtained {
			modInfoData, err = getModInfo(modID)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
		}

		var fileInfoData modFileInfo
		fileInfoData, err = getLatestFile(modInfoData, mcVersion, fileID, getLoader(pack))
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		if len(fileInfoData.Dependencies) > 0 {
			var depsInstallable []installableDep
			var depIDPendingQueue []int
			for _, dep := range fileInfoData.Dependencies {
				if dep.Type == dependencyTypeRequired {
					depIDPendingQueue = append(depIDPendingQueue, dep.ModID)
				}
			}

			if len(depIDPendingQueue) > 0 {
				fmt.Println("Finding dependencies...")

				cycles := 0
				var installedIDList []int
				for len(depIDPendingQueue) > 0 && cycles < maxCycles {
					if installedIDList == nil {
						// Get modids of all mods
						for _, modPath := range index.GetAllMods() {
							mod, err := core.LoadMod(modPath)
							if err == nil {
								data, ok := mod.GetParsedUpdateData("curseforge")
								if ok {
									updateData, ok := data.(cfUpdateData)
									if ok {
										if updateData.ProjectID > 0 {
											installedIDList = append(installedIDList, updateData.ProjectID)
										}
									}
								}
							}
						}
					}

					// Remove installed IDs from dep queue
					i := 0
					for _, id := range depIDPendingQueue {
						contains := false
						for _, id2 := range installedIDList {
							if id == id2 {
								contains = true
								break
							}
						}
						for _, data := range depsInstallable {
							if id == data.ID {
								contains = true
								break
							}
						}
						if !contains {
							depIDPendingQueue[i] = id
							i++
						}
					}
					depIDPendingQueue = depIDPendingQueue[:i]

					depInfoData, err := getModInfoMultiple(depIDPendingQueue)
					if err != nil {
						fmt.Printf("Error retrieving dependency data: %s\n", err.Error())
					}
					depIDPendingQueue = depIDPendingQueue[:0]

					for _, currData := range depInfoData {
						depFileInfo, err := getLatestFile(currData, mcVersion, 0, getLoader(pack))
						if err != nil {
							fmt.Printf("Error retrieving dependency data: %s\n", err.Error())
							continue
						}

						for _, dep := range depFileInfo.Dependencies {
							if dep.Type == dependencyTypeRequired {
								depIDPendingQueue = append(depIDPendingQueue, dep.ModID)
							}
						}

						depsInstallable = append(depsInstallable, installableDep{
							currData, depFileInfo,
						})
					}

					cycles++
				}
				if cycles >= maxCycles {
					fmt.Println("Dependencies recurse too deeply! Try increasing maxCycles.")
					os.Exit(1)
				}

				if len(depsInstallable) > 0 {
					fmt.Println("Dependencies found:")
					for _, v := range depsInstallable {
						fmt.Println(v.Name)
					}

					fmt.Print("Would you like to install them? [Y/n]: ")
					answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
					if err != nil {
						fmt.Println(err)
						os.Exit(1)
					}

					ansNormal := strings.ToLower(strings.TrimSpace(answer))
					if !(len(ansNormal) > 0 && ansNormal[0] == 'n') {
						for _, v := range depsInstallable {
							err = createModFile(v.modInfo, v.fileInfo, &index)
							if err != nil {
								fmt.Println(err)
								os.Exit(1)
							}
							fmt.Printf("Dependency \"%s\" successfully installed! (%s)\n", v.modInfo.Name, v.fileInfo.FileName)
						}
					}
				} else {
					fmt.Println("All dependencies are already installed!")
				}
			}
		}

		err = createModFile(modInfoData, fileInfoData, &index)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		err = index.Write()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		err = pack.UpdateIndexHash()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		err = pack.Write()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		fmt.Printf("Mod \"%s\" successfully installed! (%s)\n", modInfoData.Name, fileInfoData.FileName)
	},
}

// Used to implement interface for fuzzy matching
type modResultsList []modInfo

func (r modResultsList) String(i int) string {
	return r[i].Name
}

func (r modResultsList) Len() int {
	return len(r)
}

func searchCurseforgeInternal(args []string, mcVersion string, packLoaderType int) (bool, modInfo) {
	fmt.Println("Searching CurseForge...")
	searchTerm := strings.Join(args, " ")

	// If there are more than one acceptable version, we shouldn't filter by game version at all (as we can't filter by multiple)
	filterGameVersion := getCurseforgeVersion(mcVersion)
	if len(viper.GetStringSlice("acceptable-game-versions")) > 0 {
		filterGameVersion = ""
	}
	results, err := getSearch(searchTerm, filterGameVersion, packLoaderType)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	if len(results) == 0 {
		fmt.Println("No mods found!")
		os.Exit(1)
		return false, modInfo{}
	} else if len(results) == 1 {
		return false, results[0]
	} else {
		// Fuzzy search on results list
		fuzzySearchResults := fuzzy.FindFrom(searchTerm, modResultsList(results))

		menu := wmenu.NewMenu("Choose a number:")

		menu.Option("Cancel", nil, false, nil)
		if len(fuzzySearchResults) == 0 {
			for i, v := range results {
				menu.Option(v.Name, v, i == 0, nil)
			}
		} else {
			for i, v := range fuzzySearchResults {
				menu.Option(results[v.Index].Name, results[v.Index], i == 0, nil)
			}
		}

		var modInfoData modInfo
		var cancelled bool
		menu.Action(func(menuRes []wmenu.Opt) error {
			if len(menuRes) != 1 || menuRes[0].Value == nil {
				fmt.Println("Cancelled!")
				cancelled = true
				return nil
			}

			// Why is variable shadowing a thing!!!!
			var ok bool
			modInfoData, ok = menuRes[0].Value.(modInfo)
			if !ok {
				return errors.New("error converting interface from wmenu")
			}
			return nil
		})
		err = menu.Run()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		if cancelled {
			return true, modInfo{}
		}
		return false, modInfoData
	}
}

func getLatestFile(modInfoData modInfo, mcVersion string, fileID int, packLoaderType int) (modFileInfo, error) {
	// For snapshots, curseforge doesn't put them in GameVersionLatestFiles
	if fileID == 0 {
		var fileInfoData modFileInfo
		fileInfoObtained := false

		for _, v := range modInfoData.LatestFiles {
			// Choose "newest" version by largest ID
			if matchGameVersions(mcVersion, v.GameVersions) && v.ID > fileID && matchLoaderTypeFileInfo(packLoaderType, v) {
				fileID = v.ID
				fileInfoData = v
				fileInfoObtained = true
			}
		}
		// TODO: change to timestamp-based comparison??
		for _, v := range modInfoData.GameVersionLatestFiles {
			// Choose "newest" version by largest ID
			if matchGameVersion(mcVersion, v.GameVersion) && v.ID > fileID && matchLoaderType(packLoaderType, v.Modloader) {
				fileID = v.ID
				fileInfoObtained = false // Make sure we get the file info
			}
		}
		if fileInfoObtained {
			return fileInfoData, nil
		}
	}

	if fileID == 0 {
		return modFileInfo{}, errors.New("mod not available for the configured Minecraft version(s) (use the acceptable-game-versions option to accept more) or loader")
	}

	fileInfoData, err := getFileInfo(modInfoData.ID, fileID)
	if err != nil {
		return modFileInfo{}, err
	}
	return fileInfoData, nil
}

var addonIDFlag int
var fileIDFlag int

func init() {
	curseforgeCmd.AddCommand(installCmd)

	installCmd.Flags().IntVar(&addonIDFlag, "addon-id", 0, "The curseforge addon ID to use")
	installCmd.Flags().IntVar(&fileIDFlag, "file-id", 0, "The curseforge file ID to use")
}