package cmd

import (
	"bufio"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/comp500/packwiz/core"
	"github.com/fatih/camelcase"
	"github.com/igorsobreira/titlecase"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// initCmd represents the init command
var initCmd = &cobra.Command{
	Use:   "init",
	Short: "Initialise a packwiz modpack",
	Args:  cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		_, err := os.Stat(viper.GetString("pack-file"))
		if err == nil && !viper.GetBool("init.reinit") {
			fmt.Println("Modpack metadata file already exists, use -r to override!")
			os.Exit(1)
		} else if err != nil && !os.IsNotExist(err) {
			fmt.Printf("Error checking pack file: %s\n", err)
			os.Exit(1)
		}

		name, err := cmd.Flags().GetString("name")
		if err != nil || len(name) == 0 {
			// Get current file directory name
			wd, err := os.Getwd()
			directoryName := "."
			if err == nil {
				directoryName = filepath.Base(wd)
			}
			if directoryName != "." && len(directoryName) > 0 {
				// Turn directory name into a space-seperated proper name
				name = titlecase.Title(strings.ReplaceAll(strings.ReplaceAll(strings.Join(camelcase.Split(directoryName), " "), " - ", " "), " _ ", " "))
				fmt.Print("Modpack name [" + name + "]: ")
			} else {
				fmt.Print("Modpack name: ")
			}
			readName, err := bufio.NewReader(os.Stdin).ReadString('\n')
			if err != nil {
				fmt.Printf("Error reading input: %s\n", err)
				os.Exit(1)
			}
			// Trims both CR and LF
			readName = strings.TrimSpace(strings.TrimRight(readName, "\r\n"))
			if len(readName) > 0 {
				name = readName
			}
		}

		mcVersions, err := getValidMCVersions()
		if err != nil {
			fmt.Printf("Failed to get latest minecraft versions: %s\n", err)
			os.Exit(1)
		}

		mcVersion := viper.GetString("init.mc-version")
		if len(mcVersion) == 0 {
			var latestVersion string
			if viper.GetBool("init.snapshot") {
				latestVersion = mcVersions.Latest.Snapshot
			} else {
				latestVersion = mcVersions.Latest.Release
			}
			if viper.GetBool("init.latest") {
				mcVersion = latestVersion
			} else {
				fmt.Print("Minecraft version [" + latestVersion + "]: ")
				mcVersion, err = bufio.NewReader(os.Stdin).ReadString('\n')
				if err != nil {
					fmt.Printf("Error reading input: %s\n", err)
					os.Exit(1)
				}
				// Trims both CR and LF
				mcVersion = strings.TrimSpace(strings.TrimRight(mcVersion, "\r\n"))
				if len(mcVersion) == 0 {
					mcVersion = latestVersion
				}
			}
		}
		mcVersions.checkValid(mcVersion)

		// TODO: minecraft modloader
		modLoaderName := viper.GetString("init.modloader")
		if len(modLoaderName) == 0 {
			var defaultLoader string
			if viper.GetBool("init.snapshot") {
				defaultLoader = "fabric"
			} else {
				defaultLoader = "forge"
			}
			fmt.Print("Mod loader [" + defaultLoader + "]: ")
			modLoaderName, err = bufio.NewReader(os.Stdin).ReadString('\n')
			if err != nil {
				fmt.Printf("Error reading input: %s\n", err)
				os.Exit(1)
			}
			// Trims both CR and LF
			modLoaderName = strings.ToLower(strings.TrimSpace(strings.TrimRight(modLoaderName, "\r\n")))
			if len(modLoaderName) == 0 {
				modLoaderName = defaultLoader
			}
		}
		_, ok := modLoaders[modLoaderName]
		if modLoaderName != "none" && !ok {
			fmt.Println("Given mod loader is not supported! Use \"none\" to specify no modloader, or to configure one manually.")
			fmt.Print("The following mod loaders are supported: ")
			keys := make([]string, len(modLoaders))
			i := 0
			for k := range modLoaders {
				keys[i] = k
				i++
			}
			fmt.Println(strings.Join(keys, ", "))
			os.Exit(1)
		}

		modLoaderVersions := make(map[string]string)
		if modLoaderName != "none" {
			components := modLoaders[modLoaderName]

			for _, component := range components {
				versions, latestVersion, err := component.VersionListGetter(mcVersion)
				if err != nil {
					fmt.Printf("Error loading versions: %s\n", err)
					os.Exit(1)
				}
				componentVersion := viper.GetString("init." + component.Name + "-version")
				if len(componentVersion) == 0 {
					if viper.GetBool("init." + component.Name + "-latest") {
						componentVersion = latestVersion
					} else {
						fmt.Print(component.FriendlyName + " version [" + latestVersion + "]: ")
						componentVersion, err = bufio.NewReader(os.Stdin).ReadString('\n')
						if err != nil {
							fmt.Printf("Error reading input: %s\n", err)
							os.Exit(1)
						}
						// Trims both CR and LF
						componentVersion = strings.ToLower(strings.TrimSpace(strings.TrimRight(componentVersion, "\r\n")))
						if len(componentVersion) == 0 {
							componentVersion = latestVersion
						}
					}
				}
				found := false
				for _, v := range versions {
					if componentVersion == v {
						found = true
						break
					}
				}
				if !found {
					fmt.Println("Given " + component.FriendlyName + " version cannot be found!")
					os.Exit(1)
				}
				modLoaderVersions[component.Name] = componentVersion
			}
		}

		indexFilePath := viper.GetString("init.index-file")
		_, err = os.Stat(indexFilePath)
		if os.IsNotExist(err) {
			// Create file
			err = ioutil.WriteFile(indexFilePath, []byte{}, 0644)
			if err != nil {
				fmt.Printf("Error creating index file: %s\n", err)
				os.Exit(1)
			}
			fmt.Println(indexFilePath + " created!")
		} else if err != nil {
			fmt.Printf("Error checking index file: %s\n", err)
			os.Exit(1)
		}

		// Create the pack
		pack := core.Pack{
			Name: name,
			Index: struct {
				File       string `toml:"file"`
				HashFormat string `toml:"hash-format"`
				Hash       string `toml:"hash"`
			}{
				File: indexFilePath,
			},
			Versions: map[string]string{
				"minecraft": mcVersion,
			},
		}
		if modLoaderName != "none" {
			for k, v := range modLoaderVersions {
				pack.Versions[k] = v
			}
		}

		// Refresh the index and pack
		index, err := pack.LoadIndex()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		err = index.Refresh()
		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.Println(viper.GetString("pack-file") + " created!")
	},
}

func init() {
	rootCmd.AddCommand(initCmd)

	initCmd.Flags().String("name", "", "The name of the modpack (omit to define interactively)")
	initCmd.Flags().String("index-file", "index.toml", "The index file to use")
	_ = viper.BindPFlag("init.index-file", initCmd.Flags().Lookup("index-file"))
	initCmd.Flags().String("mc-version", "", "The Minecraft version to use (omit to define interactively)")
	_ = viper.BindPFlag("init.mc-version", initCmd.Flags().Lookup("mc-version"))
	initCmd.Flags().BoolP("latest", "l", false, "Automatically select the latest version of Minecraft")
	_ = viper.BindPFlag("init.latest", initCmd.Flags().Lookup("latest"))
	initCmd.Flags().BoolP("snapshot", "s", false, "Use the latest snapshot version with --latest")
	_ = viper.BindPFlag("init.snapshot", initCmd.Flags().Lookup("snapshot"))
	initCmd.Flags().BoolP("reinit", "r", false, "Recreate the pack file if it already exists, rather than exiting")
	_ = viper.BindPFlag("init.reinit", initCmd.Flags().Lookup("reinit"))
	initCmd.Flags().String("modloader", "", "The mod loader to use (omit to define interactively)")
	_ = viper.BindPFlag("init.modloader", initCmd.Flags().Lookup("modloader"))

	// ok this is epic
	for _, loader := range modLoaders {
		for _, component := range loader {
			initCmd.Flags().String(component.Name+"-version", "", "The "+component.FriendlyName+" version to use (omit to define interactively)")
			_ = viper.BindPFlag("init."+component.Name+"-version", initCmd.Flags().Lookup(component.Name+"-version"))
			initCmd.Flags().Bool(component.Name+"-latest", false, "Automatically select the latest version of "+component.FriendlyName)
			_ = viper.BindPFlag("init."+component.Name+"-latest", initCmd.Flags().Lookup(component.Name+"-latest"))
		}
	}
}

type mcVersionManifest struct {
	Latest struct {
		Release  string `json:"release"`
		Snapshot string `json:"snapshot"`
	} `json:"latest"`
	Versions []struct {
		ID          string    `json:"id"`
		Type        string    `json:"type"`
		URL         string    `json:"url"`
		Time        time.Time `json:"time"`
		ReleaseTime time.Time `json:"releaseTime"`
	} `json:"versions"`
}

func (m mcVersionManifest) checkValid(version string) {
	for _, v := range m.Versions {
		if v.ID == version {
			return
		}
	}
	fmt.Println("Given version is not a valid Minecraft version!")
	os.Exit(1)
}

func getValidMCVersions() (mcVersionManifest, error) {
	res, err := http.Get("https://launchermeta.mojang.com/mc/game/version_manifest.json")
	if err != nil {
		return mcVersionManifest{}, err
	}
	dec := json.NewDecoder(res.Body)
	out := mcVersionManifest{}
	err = dec.Decode(&out)
	if err != nil {
		return mcVersionManifest{}, err
	}
	// Sort by newest to oldest
	sort.Slice(out.Versions, func(i, j int) bool {
		return out.Versions[i].ReleaseTime.Before(out.Versions[j].ReleaseTime)
	})
	return out, nil
}

type mavenMetadata struct {
	XMLName    xml.Name `xml:"metadata"`
	GroupID    string   `xml:"groupId"`
	ArtifactID string   `xml:"artifactId"`
	Versioning struct {
		Release  string `xml:"release"`
		Versions struct {
			Version []string `xml:"version"`
		} `xml:"versions"`
		LastUpdated string `xml:"lastUpdated"`
	} `xml:"versioning"`
}

type modLoaderComponent struct {
	Name              string
	FriendlyName      string
	VersionListGetter func(mcVersion string) ([]string, string, error)
}

var modLoaders = map[string][]modLoaderComponent{
	"fabric": {
		{
			Name:              "fabric",
			FriendlyName:      "Fabric loader",
			VersionListGetter: fetchMavenVersionList("https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml"),
		},
		{
			Name:              "yarn",
			FriendlyName:      "Yarn (mappings)",
			VersionListGetter: fetchMavenVersionPrefixedList("https://maven.fabricmc.net/net/fabricmc/yarn/maven-metadata.xml", "Yarn"),
		},
	},
	"forge": {
		{
			Name:              "forge",
			FriendlyName:      "Forge",
			VersionListGetter: fetchMavenVersionPrefixedListStrip("https://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml", "Forge"),
		},
	},
	"liteloader": {
		{
			Name:              "liteloader",
			FriendlyName:      "LiteLoader",
			VersionListGetter: fetchMavenVersionPrefixedList("http://repo.mumfrey.com/content/repositories/snapshots/com/mumfrey/liteloader/maven-metadata.xml", "LiteLoader"),
		},
	},
}

func fetchMavenVersionList(url string) func(mcVersion string) ([]string, string, error) {
	return func(mcVersion string) ([]string, string, error) {
		res, err := http.Get(url)
		if err != nil {
			return []string{}, "", err
		}
		dec := xml.NewDecoder(res.Body)
		out := mavenMetadata{}
		err = dec.Decode(&out)
		if err != nil {
			return []string{}, "", err
		}
		return out.Versioning.Versions.Version, out.Versioning.Release, nil
	}
}

func fetchMavenVersionPrefixedListStrip(url string, friendlyName string) func(mcVersion string) ([]string, string, error) {
	noStrip := fetchMavenVersionPrefixedList(url, friendlyName)
	return func(mcVersion string) ([]string, string, error) {
		versions, latestVersion, err := noStrip(mcVersion)
		if err != nil {
			return nil, "", err
		}
		for k, v := range versions {
			versions[k] = strings.TrimPrefix(v, mcVersion+"-")
		}
		latestVersion = strings.TrimPrefix(latestVersion, mcVersion+"-")
		return versions, latestVersion, nil
	}
}

func fetchMavenVersionPrefixedList(url string, friendlyName string) func(mcVersion string) ([]string, string, error) {
	return func(mcVersion string) ([]string, string, error) {
		res, err := http.Get(url)
		if err != nil {
			return []string{}, "", err
		}
		dec := xml.NewDecoder(res.Body)
		out := mavenMetadata{}
		err = dec.Decode(&out)
		if err != nil {
			return []string{}, "", err
		}
		allowedVersions := make([]string, 0, len(out.Versioning.Versions.Version))
		for _, v := range out.Versioning.Versions.Version {
			if strings.HasPrefix(v, mcVersion) {
				allowedVersions = append(allowedVersions, v)
			}
		}
		if len(allowedVersions) == 0 {
			return []string{}, "", errors.New("no " + friendlyName + " versions available for this Minecraft version")
		}
		if strings.HasPrefix(out.Versioning.Release, mcVersion) {
			return allowedVersions, out.Versioning.Release, nil
		}
		return allowedVersions, allowedVersions[len(allowedVersions)-1], nil
	}
}