package modrinth

import (
	"errors"
	"fmt"
	"github.com/spf13/viper"
	"os"
	"regexp"
	"strings"

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

var modSiteRegex = regexp.MustCompile("modrinth\\.com/mod/([^/]+)/?$")
var versionSiteRegex = regexp.MustCompile("modrinth\\.com/mod/([^/]+)/version/([^/]+)/?$")

// installCmd represents the install command
var installCmd = &cobra.Command{
	Use:     "install [mod]",
	Short:   "Install a mod from a modrinth 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)
		}

		if len(args) == 0 || len(args[0]) == 0 {
			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 len(args) > 1 {
			err = installViaSearch(strings.Join(args, " "), pack)
			if err != nil {
				fmt.Printf("Failed installing mod: %s\n", err)
				os.Exit(1)
			}
			return
		}

		//Try interpreting the arg as a version url
		matches := versionSiteRegex.FindStringSubmatch(args[0])
		if matches != nil && len(matches) == 3 {
			err = installVersionById(matches[2], pack)
			if err != nil {
				fmt.Printf("Failed installing mod: %s\n", err)
				os.Exit(1)
			}
			return
		}

		//Try interpreting the arg as a modId or slug.
		//Modrinth transparently handles slugs/mod ids in their api; we don't have to detect which one it is.
		var modStr string

		//Try to see if it's a site, if extract the id/slug from the url.
		//Otherwise, interpret the arg as a id/slug straight up
		matches = modSiteRegex.FindStringSubmatch(args[0])
		if matches != nil && len(matches) == 2 {
			modStr = matches[1]
		} else {
			modStr = args[0]
		}

		mod, err := fetchMod(modStr)

		if err == nil {
			//We found a mod with that id/slug
			err = installMod(mod, pack)
			if err != nil {
				fmt.Printf("Failed installing mod: %s\n", err)
				os.Exit(1)
			}
			return
		} else {
			//This wasn't a valid modid/slug, try to search for it instead:
			//Don't bother to search if it looks like a url though
			if matches == nil {
				err = installViaSearch(args[0], pack)
				if err != nil {
					fmt.Printf("Failed installing mod: %s\n", err)
					os.Exit(1)
				}
			} else {
				fmt.Printf("Failed installing mod: %s\n", err)
				os.Exit(1)
			}
		}
	},
}

func installViaSearch(query string, pack core.Pack) error {
	mcVersion, err := pack.GetMCVersion()
	if err != nil {
		return err
	}

	results, err := getModIdsViaSearch(query, append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...))
	if err != nil {
		return err
	}

	//Create menu for the user to choose the correct mod
	menu := wmenu.NewMenu("Choose a number:")
	menu.Option("Cancel", nil, false, nil)
	for i, v := range results {
		menu.Option(v.Title, v, i == 0, nil)
	}

	menu.Action(func(menuRes []wmenu.Opt) error {
		if len(menuRes) != 1 || menuRes[0].Value == nil {
			return errors.New("Cancelled!")
		}

		//Get the selected mod
		selectedMod, ok := menuRes[0].Value.(ModResult)
		if !ok {
			return errors.New("error converting interface from wmenu")
		}

		//Install the selected mod
		modId := strings.TrimPrefix(selectedMod.ModID, "local-")

		mod, err := fetchMod(modId)
		if err != nil {
			return err
		}

		return installMod(mod, pack)
	})

	return menu.Run()
}

func installMod(mod Mod, pack core.Pack) error {
	fmt.Printf("Found mod %s: '%s'.\n", mod.Title, mod.Description)

	latestVersion, err := getLatestVersion(mod.ID, pack)
	if err != nil {
		return err
	}
	if latestVersion.ID == "" {
		return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
	}

	return installVersion(mod, latestVersion, pack)
}

func installVersion(mod Mod, version Version, pack core.Pack) error {
	var files = version.Files

	if len(files) == 0 {
		return errors.New("version doesn't have any files attached")
	}

	// TODO: add some way to allow users to pick which file to install?
	var file = files[0]
	// Prefer the primary file
	for _, v := range files {
		if v.Primary {
			file = v
		}
	}

	//Install the file
	fmt.Printf("Installing %s from version %s\n", file.Filename, version.VersionNumber)
	index, err := pack.LoadIndex()
	if err != nil {
		return err
	}

	updateMap := make(map[string]map[string]interface{})

	updateMap["modrinth"], err = mrUpdateData{
		ModID:            mod.ID,
		InstalledVersion: version.ID,
	}.ToMap()
	if err != nil {
		return err
	}

	side := mod.getSide()
	if side == "" {
		return errors.New("version doesn't have a side that's supported. Server: " + mod.ServerSide + " Client: " + mod.ClientSide)
	}

	algorithm, hash := file.getBestHash()
	if algorithm == "" {
		return errors.New("file doesn't have a hash")
	}

	modMeta := core.Mod{
		Name:     mod.Title,
		FileName: file.Filename,
		Side:     side,
		Download: core.ModDownload{
			URL:        file.Url,
			HashFormat: algorithm,
			Hash:       hash,
		},
		Update: updateMap,
	}
	var path string
	if mod.Slug != "" {
		path = modMeta.SetMetaName(mod.Slug, index)
	} else {
		path = modMeta.SetMetaName(mod.Title, index)
	}

	// If the file already exists, this will overwrite it!!!
	// TODO: Should this be improved?
	// Current strategy is to go ahead and do stuff without asking, with the assumption that you are using
	// VCS anyway.

	format, hash, err := modMeta.Write()
	if err != nil {
		return err
	}
	err = index.RefreshFileWithHash(path, format, hash, true)
	if err != nil {
		return err
	}
	err = index.Write()
	if err != nil {
		return err
	}
	err = pack.UpdateIndexHash()
	if err != nil {
		return err
	}
	err = pack.Write()
	if err != nil {
		return err
	}
	return nil
}

func installVersionById(versionId string, pack core.Pack) error {
	version, err := fetchVersion(versionId)
	if err != nil {
		return err
	}

	mod, err := fetchMod(version.ModID)
	if err != nil {
		return err
	}

	return installVersion(mod, version, pack)
}

func init() {
	modrinthCmd.AddCommand(installCmd)
}