package modrinth

import (
	"encoding/json"
	"errors"
	"github.com/spf13/viper"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"

	"github.com/Masterminds/semver/v3"
	"github.com/packwiz/packwiz/cmd"
	"github.com/packwiz/packwiz/core"
	"github.com/spf13/cobra"
)

const modrinthApiUrl = "https://api.modrinth.com/api/v1/"

var modrinthApiUrlParsed, _ = url.Parse(modrinthApiUrl)

var modrinthCmd = &cobra.Command{
	Use:     "modrinth",
	Aliases: []string{"mr"},
	Short:   "Manage modrinth-based mods",
}

func init() {
	cmd.Add(modrinthCmd)
	core.Updaters["modrinth"] = mrUpdater{}
}

type License struct {
	Id   string `json:"id"`   //The license id of a mod, retrieved from the licenses get route
	Name string `json:"name"` //The long for name of a license
	Url  string `json:"url"`  //The URL to this license
}

type Mod struct {
	ID          string   `json:"id"`          //The ID of the mod, encoded as a base62 string
	Slug        string   `json:"slug"`        //The slug of a mod, used for vanity URLs
	Team        string   `json:"team"`        //The id of the team that has ownership of this mod
	Title       string   `json:"title"`       //The title or name of the mod
	Description string   `json:"description"` //A short description of the mod
	Body        string   `json:"body"`        //A long form description of the mod.
	BodyUrl     string   `json:"body_url"`    //DEPRECATED The link to the long description of the mod (Optional)
	Published   string   `json:"published"`   //The date at which the mod was first published
	Updated     string   `json:"updated"`     //The date at which the mod was updated
	Status      string   `json:"status"`      //The status of the mod - approved, rejected, draft, unlisted, processing, or unknown
	License     struct { //The license of the mod
		ID   string `json:"id"`
		Name string `json:"name"`
		URL  string `json:"url"`
	} `json:"license"`
	ClientSide string   `json:"client_side"` //The support range for the client mod - required, optional, unsupported, or unknown
	ServerSide string   `json:"server_side"` //The support range for the server mod - required, optional, unsupported, or unknown
	Downloads  int      `json:"downloads"`   //The total number of downloads the mod has
	Categories []string `json:"categories"`  //A list of the categories that the mod is in
	Versions   []string `json:"versions"`    //A list of ids for versions of the mod
	IconUrl    string   `json:"icon_url"`    //The URL of the icon of the mod (Optional)
	IssuesUrl  string   `json:"issues_url"`  //An optional link to where to submit bugs or issues with the mod (Optional)
	SourceUrl  string   `json:"source_url"`  //An optional link to the source code for the mod (Optional)
	WikiUrl    string   `json:"wiki_url"`    //An optional link to the mod's wiki page or other relevant information (Optional)
	DiscordUrl string   `json:"discord_url"` //An optional link to the mod's discord (Optional)
}

type ModResult struct {
	ModID         string   `json:"mod_id"`         //The id of the mod; prefixed with local-
	ProjectType   string   `json:"project_id"`     //The project type of the mod
	Author        string   `json:"author"`         //The username of the author of the mod
	Title         string   `json:"title"`          //The name of the mod
	Description   string   `json:"description"`    //A short description of the mod
	Categories    []string `json:"categories"`     //A list of the categories the mod is in
	Versions      []string `json:"versions"`       //A list of the minecraft versions supported by the mod
	Downloads     int      `json:"downloads"`      //The total number of downloads for the mod
	PageUrl       string   `json:"page_url"`       //A link to the mod's main page;
	IconUrl       string   `json:"icon_url"`       //The url of the mod's icon
	AuthorUrl     string   `json:"author_url"`     //The url of the mod's author
	DateCreated   string   `json:"date_created"`   //The date that the mod was originally created
	DateModified  string   `json:"date_modified"`  //The date that the mod was last modified
	LatestVersion string   `json:"latest_version"` //The latest version of minecraft that this mod supports
	License       string   `json:"license"`        //The id of the license this mod follows
	ClientSide    string   `json:"client_side"`    //The side type id that this mod is on the client
	ServerSide    string   `json:"server_side"`    //The side type id that this mod is on the server
	Host          string   `json:"host"`           //The host that this mod is from, always modrinth
}

type ModSearchResult struct {
	Hits      []ModResult `json:"hits"`       //The list of results
	Offset    int         `json:"offset"`     //The number of results that were skipped by the query
	Limit     int         `json:"limit"`      //The number of mods returned by the query
	TotalHits int         `json:"total_hits"` //The total number of mods that the query found
}

type Version struct {
	ID            string        `json:"id"`             //The ID of the version, encoded as a base62 string
	ModID         string        `json:"mod_id"`         //The ID of the mod this version is for
	AuthorId      string        `json:"author_id"`      //The ID of the author who published this version
	Featured      bool          `json:"featured"`       //Whether the version is featured or not
	Name          string        `json:"name"`           //The name of this version
	VersionNumber string        `json:"version_number"` //The version number. Ideally will follow semantic versioning
	Changelog     string        `json:"changelog"`      //The changelog for this version of the mod. (Optional)
	DatePublished string        `json:"date_published"` //The date that this version was published
	Downloads     int           `json:"downloads"`      //The number of downloads this specific version has
	VersionType   string        `json:"version_type"`   //The type of the release - alpha, beta, or release
	Files         []VersionFile `json:"files"`          //A list of files available for download for this version
	//Dependencies  []string      `json:"dependencies"`   //A list of specific versions of mods that this version depends on
	GameVersions []string `json:"game_versions"` //A list of versions of Minecraft that this version of the mod supports
	Loaders      []string `json:"loaders"`       //The mod loaders that this version supports
}

type VersionFile struct {
	Hashes   map[string]string //A map of hashes of the file. The key is the hashing algorithm and the value is the string version of the hash.
	Url      string            //A direct link to the file
	Filename string            //The name of the file
	Primary  bool              // Is the file the primary file?
}

func getModIdsViaSearch(query string, versions []string) ([]ModResult, error) {
	baseUrl := *modrinthApiUrlParsed
	baseUrl.Path += "mod"

	params := url.Values{}
	params.Add("limit", "5")
	params.Add("index", "relevance")
	facets := make([]string, 0)
	for _, v := range versions {
		facets = append(facets, "versions:"+v)
	}
	facetsEncoded, err := json.Marshal(facets)
	if err != nil {
		return []ModResult{}, err
	}
	params.Add("facets", "["+string(facetsEncoded)+"]")
	params.Add("query", query)

	baseUrl.RawQuery = params.Encode()

	resp, err := http.Get(baseUrl.String())
	if err != nil {
		return []ModResult{}, err
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return []ModResult{}, err
	}

	var result ModSearchResult
	err = json.Unmarshal(body, &result)
	if err != nil {
		return []ModResult{}, err
	}

	if result.TotalHits <= 0 {
		return []ModResult{}, errors.New("Couldn't find that mod. Is it available for this version?")
	}

	return result.Hits, nil
}

func getLatestVersion(modID string, pack core.Pack) (Version, error) {
	mcVersion, err := pack.GetMCVersion()
	if err != nil {
		return Version{}, err
	}
	gameVersions := append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...)
	gameVersionsEncoded, err := json.Marshal(gameVersions)
	if err != nil {
		return Version{}, err
	}

	loader := getLoader(pack)

	baseUrl := *modrinthApiUrlParsed
	baseUrl.Path += "mod/"
	baseUrl.Path += modID
	baseUrl.Path += "/version"

	params := url.Values{}
	params.Add("game_versions", string(gameVersionsEncoded))
	if loader != "any" {
		params.Add("loaders", "[\""+loader+"\"]")
	}

	baseUrl.RawQuery = params.Encode()

	resp, err := http.Get(baseUrl.String())
	if err != nil {
		return Version{}, err
	}

	if resp.StatusCode == 404 {
		return Version{}, errors.New("couldn't find mod: " + modID)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return Version{}, err
	}

	var result []Version
	err = json.Unmarshal(body, &result)
	if err != nil {
		return Version{}, err
	}

	if len(result) == 0 {
		return Version{}, errors.New("no valid versions found")
	}

	latestValidVersion := result[0]
	for _, v := range result[1:] {
		currVersion, err1 := semver.NewVersion(v.VersionNumber)
		latestVersion, err2 := semver.NewVersion(latestValidVersion.VersionNumber)
		var semverCompare = 0
		// Only compare with semver if both are valid semver - otherwise compare by release date
		if err1 == nil && err2 == nil {
			semverCompare = currVersion.Compare(latestVersion)
		}

		if semverCompare == 0 {
			//Semver is equal, compare date instead
			vDate, _ := time.Parse(time.RFC3339Nano, v.DatePublished)
			latestDate, _ := time.Parse(time.RFC3339Nano, latestValidVersion.DatePublished)
			if vDate.After(latestDate) {
				latestValidVersion = v
			}
		} else if semverCompare == 1 {
			latestValidVersion = v
		}
	}

	return latestValidVersion, nil
}

func fetchMod(modID string) (Mod, error) {
	var mod Mod

	resp, err := http.Get(modrinthApiUrl + "mod/" + modID)
	if err != nil {
		return mod, err
	}

	if resp.StatusCode == 404 {
		return mod, errors.New("couldn't find mod: " + modID)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return mod, err
	}

	err = json.Unmarshal(body, &mod)
	if err != nil {
		return mod, err
	}

	if mod.ID == "" {
		return mod, errors.New("invalid json whilst fetching mod: " + modID)
	}

	return mod, nil
}

func fetchVersion(versionId string) (Version, error) {
	var version Version

	resp, err := http.Get(modrinthApiUrl + "version/" + versionId)
	if err != nil {
		return version, err
	}

	if resp.StatusCode == 404 {
		return version, errors.New("couldn't find version: " + versionId)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return version, err
	}

	err = json.Unmarshal(body, &version)
	if err != nil {
		return version, err
	}

	if version.ID == "" {
		return version, errors.New("invalid json whilst fetching version: " + versionId)
	}

	return version, nil
}

func (mod Mod) getSide() string {
	server := shouldDownloadOnSide(mod.ServerSide)
	client := shouldDownloadOnSide(mod.ClientSide)

	if server && client {
		return core.UniversalSide
	} else if server {
		return core.ServerSide
	} else if client {
		return core.ClientSide
	} else {
		return ""
	}
}

func shouldDownloadOnSide(side string) bool {
	return side == "required" || side == "optional"
}

func (v VersionFile) getBestHash() (string, string) {
	//try preferred hashes first
	val, exists := v.Hashes["sha256"]
	if exists {
		return "sha256", val
	}
	val, exists = v.Hashes["murmur2"]
	if exists {
		return "murmur2", val
	}
	val, exists = v.Hashes["sha512"]
	if exists {
		return "sha512", val
	}

	//none of the preferred hashes are present, just get the first one
	for key, val := range v.Hashes {
		return key, val
	}

	//No hashes were present
	return "", ""
}

func getLoader(pack core.Pack) string {
	dependencies := pack.Versions

	_, hasFabric := dependencies["fabric"]
	_, hasForge := dependencies["forge"]
	if hasFabric && hasForge {
		return "any"
	} else if hasFabric {
		return "fabric"
	} else if hasForge {
		return "forge"
	} else {
		return "any"
	}
}