package modrinth

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"github.com/spf13/viper"
	"net/url"
	"os"
	"path/filepath"

	"github.com/packwiz/packwiz/core"
	"github.com/spf13/cobra"
	"golang.org/x/exp/slices"
)

// exportCmd represents the export command
var exportCmd = &cobra.Command{
	Use:   "export",
	Short: "Export the current modpack into a .mrpack for Modrinth",
	Args:  cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Loading modpack...")
		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)
		}
		// Do a refresh to ensure files are up to date
		err = index.Refresh()
		if err != nil {
			fmt.Println(err)
			return
		}
		err = index.Write()
		if err != nil {
			fmt.Println(err)
			return
		}
		err = pack.UpdateIndexHash()
		if err != nil {
			fmt.Println(err)
			return
		}
		err = pack.Write()
		if err != nil {
			fmt.Println(err)
			return
		}

		// TODO: should index just expose indexPath itself, through a function?
		indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))

		mods, unwhitelistedMods := loadMods(index)

		fileName := viper.GetString("modrinth.export.output")
		if fileName == "" {
			fileName = pack.GetPackName() + ".mrpack"
		}
		expFile, err := os.Create(fileName)
		if err != nil {
			fmt.Printf("Failed to create zip: %s\n", err.Error())
			os.Exit(1)
		}
		exp := zip.NewWriter(expFile)

		// Add an overrides folder even if there are no files to go in it
		_, err = exp.Create("overrides/")
		if err != nil {
			fmt.Printf("Failed to add overrides folder: %s\n", err.Error())
			os.Exit(1)
		}

		// TODO: cache these (ideally with changes to pack format)
		fmt.Println("Retrieving SHA1 hashes for external mods...")
		sha1Hashes := make([]string, len(mods))
		for i, mod := range mods {
			if mod.Download.HashFormat == "sha1" {
				sha1Hashes[i] = mod.Download.Hash
			} else {
				// Hash format for this mod isn't SHA1 - and the Modrinth pack format requires it; so get it by downloading the file
				h, stringer, err := core.GetHashImpl("sha1")
				if err != nil {
					panic("Failed to get sha1 hash implementation")
				}
				err = mod.DownloadFile(h)
				if err != nil {
					fmt.Printf("Error downloading mod file %s: %s\n", mod.Download.URL, err.Error())
					// TODO: exit(1)?
					continue
				}
				sha1Hashes[i] = stringer.HashToString(h.Sum(nil))
				fmt.Printf("Retrieved SHA1 hash for %s successfully\n", mod.Download.URL)
			}
		}

		manifestFile, err := exp.Create("modrinth.index.json")
		if err != nil {
			_ = exp.Close()
			_ = expFile.Close()
			fmt.Println("Error creating manifest: " + err.Error())
			os.Exit(1)
		}

		manifestFiles := make([]PackFile, len(mods))
		for i, mod := range mods {
			pathForward, err := filepath.Rel(filepath.Dir(indexPath), mod.GetDestFilePath())
			if err != nil {
				fmt.Printf("Error resolving mod file: %s\n", err.Error())
				// TODO: exit(1)?
				continue
			}

			path := filepath.ToSlash(pathForward)

			hashes := make(map[string]string)
			hashes["sha1"] = sha1Hashes[i]

			// Create env options based on configured optional/side
			var envInstalled string
			if mod.Option != nil && mod.Option.Optional {
				envInstalled = "optional"
			} else {
				envInstalled = "required"
			}
			var clientEnv, serverEnv string
			if mod.Side == core.UniversalSide {
				clientEnv = envInstalled
				serverEnv = envInstalled
			} else if mod.Side == core.ClientSide {
				clientEnv = envInstalled
				serverEnv = "unsupported"
			} else if mod.Side == core.ServerSide {
				clientEnv = "unsupported"
				serverEnv = envInstalled
			}

			// Modrinth URLs must be RFC3986
			u, err := core.ReencodeURL(mod.Download.URL)
			if err != nil {
				fmt.Printf("Error re-encoding mod URL: %s\n", err.Error())
				u = mod.Download.URL
			}

			manifestFiles[i] = PackFile{
				Path:   path,
				Hashes: hashes,
				Env: &struct {
					Client string `json:"client"`
					Server string `json:"server"`
				}{Client: clientEnv, Server: serverEnv},
				Downloads: []string{u},
			}
		}

		dependencies := make(map[string]string)
		dependencies["minecraft"], err = pack.GetMCVersion()
		if err != nil {
			_ = exp.Close()
			_ = expFile.Close()
			fmt.Println("Error creating manifest: " + err.Error())
			os.Exit(1)
		}
		if fabricVersion, ok := pack.Versions["fabric"]; ok {
			dependencies["fabric-loader"] = fabricVersion
		} else if forgeVersion, ok := pack.Versions["forge"]; ok {
			dependencies["forge"] = forgeVersion
		}

		manifest := Pack{
			FormatVersion: 1,
			Game:          "minecraft",
			VersionID:     pack.Version,
			Name:          pack.Name,
			Summary:       pack.Description,
			Files:         manifestFiles,
			Dependencies:  dependencies,
		}

		if len(pack.Version) == 0 {
			fmt.Println("Warning: pack.toml version field must not be empty to create a valid Modrinth pack")
		}

		w := json.NewEncoder(manifestFile)
		w.SetIndent("", "    ") // Documentation uses 4 spaces
		err = w.Encode(manifest)
		if err != nil {
			_ = exp.Close()
			_ = expFile.Close()
			fmt.Println("Error writing manifest: " + err.Error())
			os.Exit(1)
		}

		for _, v := range index.Files {
			if !v.MetaFile {
				// Save all non-metadata files into the zip
				path, err := filepath.Rel(filepath.Dir(indexPath), index.GetFilePath(v))
				if err != nil {
					fmt.Printf("Error resolving file: %s\n", err.Error())
					// TODO: exit(1)?
					continue
				}
				file, err := exp.Create(filepath.ToSlash(filepath.Join("overrides", path)))
				if err != nil {
					fmt.Printf("Error creating file: %s\n", err.Error())
					// TODO: exit(1)?
					continue
				}
				err = index.SaveFile(v, file)
				if err != nil {
					fmt.Printf("Error copying file: %s\n", err.Error())
					// TODO: exit(1)?
					continue
				}
			}
		}

		if len(unwhitelistedMods) > 0 {
			fmt.Println("Downloading unwhitelisted mods...")
		}
		for _, v := range unwhitelistedMods {
			pathRel, err := filepath.Rel(filepath.Dir(indexPath), v.GetDestFilePath())
			if err != nil {
				fmt.Printf("Error resolving mod file: %s\n", err.Error())
				// TODO: exit(1)?
				continue
			}
			var path string
			if v.Side == core.ClientSide {
				path = filepath.ToSlash(filepath.Join("client-overrides", pathRel))
			} else if v.Side == core.ServerSide {
				path = filepath.ToSlash(filepath.Join("server-overrides", pathRel))
			} else {
				path = filepath.ToSlash(filepath.Join("overrides", pathRel))
			}

			file, err := exp.Create(path)
			if err != nil {
				fmt.Printf("Error creating file: %s\n", err.Error())
				// TODO: exit(1)?
				continue
			}
			err = v.DownloadFile(file)
			if err != nil {
				fmt.Printf("Error downloading file: %s\n", err.Error())
				// TODO: exit(1)?
				continue
			}
			fmt.Printf("Downloaded %v successfully\n", path)
		}

		err = exp.Close()
		if err != nil {
			fmt.Println("Error writing export file: " + err.Error())
			os.Exit(1)
		}
		err = expFile.Close()
		if err != nil {
			fmt.Println("Error writing export file: " + err.Error())
			os.Exit(1)
		}

		fmt.Println("Modpack exported to " + fileName)
	},
}

var whitelistedHosts = []string{
	"cdn.modrinth.com",
	"edge.forgecdn.net",
	"github.com",
	"raw.githubusercontent.com",
}

func loadMods(index core.Index) ([]core.Mod, []core.Mod) {
	modPaths := index.GetAllMods()
	mods := make([]core.Mod, 0, len(modPaths))
	unwhitelistedMods := make([]core.Mod, 0)
	fmt.Println("Reading mod files...")
	for _, v := range modPaths {
		modData, err := core.LoadMod(v)
		if err != nil {
			fmt.Printf("Error reading mod file %s: %s\n", v, err.Error())
			// TODO: exit(1)?
			continue
		}

		modUrl, err := url.Parse(modData.Download.URL)
		if err == nil {
			if slices.Contains(whitelistedHosts, modUrl.Host) {
				mods = append(mods, modData)
			} else {
				unwhitelistedMods = append(unwhitelistedMods, modData)
			}
		} else {
			fmt.Printf("Failed to parse mod URL: %v\n", modUrl)
			mods = append(mods, modData)
		}
	}
	return mods, unwhitelistedMods
}

func init() {
	modrinthCmd.AddCommand(exportCmd)
	exportCmd.Flags().StringP("output", "o", "", "The file to export the modpack to")
	_ = viper.BindPFlag("modrinth.export.output", exportCmd.Flags().Lookup("output"))
}