package main

import (
	"bytes"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"
	
)

const client_id string = "9305aeb8-5ecb-4e7a-b28f-c33aefcfbd8d"

type McProfile struct {
	Id string
	Name string
}

type Authentication struct {
	Access   authenticationResp
	XboxAuth xboxAuthResponse
	XboxAPI  xboxAuthResponse
	McAPI    xboxAuthResponse
	McAuth McAuthResponse
	Profile McProfile
}

type devCodeResp struct {
	User_code        string
	Device_code      string
	Verification_uri string
	Expires_in       string
	Interval         int
	Message          string
}

type authenticationResp struct {
	Access_token      string
	Token_type        string
	Refresh_token     string
	Expires_in        string
	Error             string
	Error_description string
}

type xboxAuthProperties struct {
	AuthMethod string
	SiteName   string
	RpsTicket  string
}

type xboxAuthRequest struct {
	Properties   xboxAuthProperties
	RelyingParty string
	TokenType    string
}

type xboxDisplayClaim struct {
	Uhs string
	Gtg string
	Xid string
	Agg string
	Usr string
	Utr string
	Prv string
}

type xboxDisplayClaims struct {
	Xui []xboxDisplayClaim
}

type xboxAuthResponse struct {
	IssueInstant  time.Time
	NotAfter      time.Time
	Token         string
	DisplayClaims xboxDisplayClaims
}

type XSTSProperties struct {
	SandboxId  string
	UserTokens []string
}

type XSTSRequest struct {
	Properties   XSTSProperties
	RelyingParty string
	TokenType    string
}

type McAuthRequest struct {
	Xtoken   string `json:"xtoken"`
	Platform string `json:"platform"`
}

type McAuthResponse struct {
	Username string
	Access_token string
	Expires_in int
	token_type string
}

type McVersionManifestEntry struct {
	Id string
	Type string
	Url string
	Time time.Time
	ReleaseTime time.Time
	Sha1 string
	ComplianceLevel int
}

type McLatestEntry struct {
	Release string
	Snapshot string
}

type McVersionManifest struct {
	Latest McLatestEntry
	Versions []McVersionManifestEntry
}

type McArguments struct {
	Game []string
	Jvm []string
}

type McAssetIndex struct {
	Id string
	Sha1 string
	Size int
	TotalSize int
	Url	string
}

type McDownload struct {
	Sha1 string
	Size int
	Url string
}

type McDownloads struct {
	Client McDownload
	Client_mappings McDownload
	Server McDownload
	Server_mappings McDownload
	
}

type McJavaVersion struct {
	Component string
	MajorVersion int
}

type McLibraryArtifact struct {
	Path string
	Sha1 string
	Size int
	Url string
}

type McLibraryDownload struct {
	Artifact McLibraryArtifact
}

type McRuleOs struct {
	Name string
	Version string
	Arch string
}

type McRule struct {
	Action string
	Features map[string]bool
	Os	McRuleOs
}

type McLibrary struct {
	Downloads McLibraryDownload
	Name string
	Rules []McRule
}

type McMetadata struct {
	Arguments McArguments
	AssetIndex McAssetIndex
	Assets string
	complianceLevel int
	Downloads McDownloads
	Id string
	JavaVersion McJavaVersion
	Libraries []McLibrary
	Logging interface{}
	MainClass string
	MinimumLauncherVersion int
	ReleaseTime time.Time
	Time time.Time
	Type string
}


func main() {
	
	auth := Authentication{}
	dir, _ := os.UserConfigDir()
	if _, err := os.Stat(filepath.Join(dir, "minecraft_test", "authentication.json")); err != nil {
		resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", url.Values{
			"client_id": {client_id},
			"scope":     {"XboxLive.SignIn XboxLive.offline_access"},
		})
		if err != nil {
			fmt.Printf("Device Auth Step: %s\n", err)
			return
		}
			defer resp.Body.Close()
			if resp.StatusCode != 200 {
				fmt.Printf("Device Auth Step: %v\n", resp.Status)
				return
			}
			data, _ := io.ReadAll(resp.Body)
			codeResp := devCodeResp{}
			json.Unmarshal(data, &codeResp)
			fmt.Println(codeResp.Message)
			ticker := time.NewTicker(time.Second * time.Duration(codeResp.Interval))
			defer ticker.Stop()
			for range ticker.C {
				resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
					"client_id":   {client_id},
					"grant_type":  {"urn:ietf:params:oauth:grant-type:device_code"},
					"device_code": {codeResp.Device_code},
				})
				if err != nil {
					fmt.Printf("Authentication Request Error: %s\n", err)
				}
				defer resp.Body.Close()
				//if resp.StatusCode != 200 {
				//	fmt.Printf("Authentication Request Error: %s\n", resp.Status)
				//}
				data, _ := io.ReadAll(resp.Body)
				authResp := authenticationResp{}
				json.Unmarshal(data, &authResp)
				if authResp.Error == "" {
					fmt.Printf("Authenticated!\n")
					auth.Access = authResp
					break
				}

			}
		} else {
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "authentication.json"), os.O_CREATE|os.O_RDWR, 0755)
			data, _ := io.ReadAll(f)
			json.Unmarshal(data, &auth.Access)
			resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
				"client_id":   {client_id},
				"grant_type":  {"refresh_token"},
				"refresh_token": {auth.Access.Refresh_token},
				"scope":     {"XboxLive.SignIn XboxLive.offline_access"},
			})
			if err != nil {
				fmt.Printf("Authentication Request Error: %s\n", err)
			}
			defer resp.Body.Close()
			//if resp.StatusCode != 200 {
			//	fmt.Printf("Authentication Request Error: %s\n", resp.Status)
			//}
			data, _ = io.ReadAll(resp.Body)
			authResp := authenticationResp{}
			json.Unmarshal(data, &authResp)
			if authResp.Error == "" {
				fmt.Printf("Authenticated!\n")
				auth.Access = authResp
			}


		}
		os.MkdirAll(filepath.Join(dir, "minecraft_test"), 0755)
		f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "authentication.json"), os.O_CREATE|os.O_RDWR, 0755)
		defer f.Close()
		data, _ := json.Marshal(auth.Access)
		f.Write(data)

		req, _ := json.Marshal(xboxAuthRequest{Properties: xboxAuthProperties{AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: "d=" + auth.Access.Access_token}, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"})
		client := http.Client{}
		httpReq, _ := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewBuffer(req))
		httpReq.Header.Add("x-xbl-contract-version", "1")
		httpReq.Header.Add("Content-Type", "application/json")
		httpReq.Header.Add("Accept", "application/json")
		httpResp, err := client.Do(httpReq)
		if err != nil {
			fmt.Printf("XboxLive Error: %s\n", err)
		}
		defer httpResp.Body.Close()
		if httpResp.StatusCode != 200 {
			fmt.Printf("XboxLive Error: %s\n", httpResp.Status)
		}
		d, _ := io.ReadAll(httpResp.Body)
		json.Unmarshal(d, &auth.XboxAuth)
		xstsData, _ := json.Marshal(XSTSRequest{Properties: XSTSProperties{SandboxId: "RETAIL", UserTokens: []string{auth.XboxAuth.Token}}, RelyingParty: "http://xboxlive.com", TokenType: "JWT"})
		httpXstsReq, _ := http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(xstsData))
		httpXstsReq.Header.Add("Content-Type", "application/json")
		httpResp, err = client.Do(httpXstsReq)
		if err != nil {
			fmt.Printf("XboxLive STS error: %s\n", err)
		}
		defer httpResp.Body.Close()
		if httpResp.StatusCode != 200 {
			fmt.Printf("XboxLive STS error: %s\n", httpResp.Status)
		}
		d, _ = io.ReadAll(httpResp.Body)
		json.Unmarshal(d, &auth.XboxAPI)

		xstsData, _ = json.Marshal(XSTSRequest{Properties: XSTSProperties{SandboxId: "RETAIL", UserTokens: []string{auth.XboxAuth.Token}}, RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"})
		httpXstsReq, _ = http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(xstsData))
		httpXstsReq.Header.Add("Content-Type", "application/json")
		httpResp, err = client.Do(httpXstsReq)
		if err != nil {
			fmt.Printf("Minecraft STS error: %s\n", err)
		}
		defer httpResp.Body.Close()
		if httpResp.StatusCode != 200 {
			fmt.Printf("Minecraft STS error: %s\n", httpResp.Status)
		}
		d, _ = io.ReadAll(httpResp.Body)
		json.Unmarshal(d, &auth.McAPI)
		if auth.McAPI.DisplayClaims.Xui[0].Uhs != auth.XboxAPI.DisplayClaims.Xui[0].Uhs {
			fmt.Printf("Warning: Inconsistant user hash!")
		}
		mcAuthData, _ := json.Marshal(McAuthRequest{Xtoken: "XBL3.0 x=" + auth.McAPI.DisplayClaims.Xui[0].Uhs + ";" + auth.McAPI.Token, Platform: "PC_LAUNCHER"})
		httpReqMC, _ := http.NewRequest("POST", "https://api.minecraftservices.com/launcher/login", bytes.NewBuffer(mcAuthData))
		httpReqMC.Header.Add("Content-Type", "application/json")
		httpReqMC.Header.Add("Accept", "application/json")
		resp, err := client.Do(httpReqMC)
		if err != nil {
			fmt.Printf("MC Auth Error: %s\n", err)
		}
		defer resp.Body.Close()
		if resp.StatusCode != 200 {
			fmt.Printf("MC Auth Error: %s\n", resp.Status)
		}
		d, _ = io.ReadAll(resp.Body)
		json.Unmarshal(d, &auth.McAuth)
		httpReq, err = http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", new(bytes.Buffer))
		httpReq.Header.Add("Content-Type", "application/json")
		httpReq.Header.Add("Accept", "application/json")
		httpReq.Header.Add("Authorization", "Bearer "+auth.McAuth.Access_token)
		resp, _ = client.Do(httpReq)
		if err != nil {
			fmt.Printf("Error obtaining mc profile information: %s\n", err)
		}
		defer resp.Body.Close()
		data, _ = io.ReadAll(resp.Body)
		json.Unmarshal(data, &auth.Profile)
		resp, err = http.Get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
		if err != nil {
			fmt.Printf("manifest error")
		}
		defer resp.Body.Close()
		data, _ = io.ReadAll(resp.Body)
		versionManifest := McVersionManifest{}
		json.Unmarshal(data, &versionManifest)
		resp, err = http.Get(versionManifest.Versions[0].Url)
		if err != nil {
			fmt.Printf("metadata error")
		}
		defer resp.Body.Close()
		data, _ = io.ReadAll(resp.Body)
		var metadata McMetadata
		json.Unmarshal(data, &metadata)
		os.MkdirAll(filepath.Join(dir, "minecraft_test", "assets", "indexes"), 0755)
		os.MkdirAll(filepath.Join(dir, "minecraft_test", "assets", "objects"), 0755)
		download_index := false
		if _, err := os.Stat(filepath.Join(dir, "minecraft_test", "assets", "indexes", metadata.Assets+".json")); err == nil {
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "assets", "indexes", metadata.Assets+".json"), os.O_RDONLY, 0755)
			defer f.Close()
			data, _ = io.ReadAll(f)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != metadata.AssetIndex.Sha1 {
				download_index = true
			}
		} else {
			download_index = true
		}
		if download_index {
			resp, err = http.Get(metadata.AssetIndex.Url)
			if err != nil {
				fmt.Printf("asset index error")
			}
			defer resp.Body.Close()
			data, _ = io.ReadAll(resp.Body)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != metadata.AssetIndex.Sha1 {
				fmt.Printf("Error downloading asset index")
				return
			}
			f, _ = os.OpenFile(filepath.Join(dir, "minecraft_test", "assets", "indexes", metadata.Assets + ".json"), os.O_CREATE|os.O_RDWR, 0755)
			defer f.Close()
			f.Write(data)
		}
		var index map[string]interface{}
		json.Unmarshal(data, &index)
		index = index["objects"].(map[string]interface{})
	for name, asset := range index {
			asset_map := asset.(map[string]interface{})
			path := asset_map["hash"].(string)[:2] + "/" + asset_map["hash"].(string)
			if _, err = os.Stat(filepath.Join(dir, "minecraft_test", "assets", "objects", path)); err == nil {
				f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "assets", "objects", path), os.O_RDONLY, 0755)
				data, _ := io.ReadAll(f)
				sha := sha1.Sum(data)
				if hex.EncodeToString(sha[:20]) == asset_map["hash"].(string) {
					continue
				}
			}
			fmt.Printf("Downloading %s: ", name)
			resp, err = http.Get(fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", asset_map["hash"].(string)[:2], asset_map["hash"].(string)))
			if err != nil {
				fmt.Printf("Error\n")
				return
			}
			defer resp.Body.Close()
			data, _ = io.ReadAll(resp.Body)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != asset_map["hash"].(string) {
				fmt.Printf("Sha1 Mismatch\n")
				return
			}
			fmt.Printf("Ok\n")
			os.MkdirAll(filepath.Join(dir, "minecraft_test", "assets", "objects", asset_map["hash"].(string)[:2]), 0755)
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "assets", "objects", path), os.O_CREATE|os.O_RDWR, 0755)	
			defer f.Close()
			f.Write(data)
		}
		
		for _, lib := range metadata.Libraries {
			if _, err = os.Stat(filepath.Join(dir, "minecraft_test", "lib", lib.Downloads.Artifact.Path)); err == nil {
				f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "lib", lib.Downloads.Artifact.Path), os.O_RDONLY, 0755)
				defer f.Close()
				data, _ := io.ReadAll(f)
				sha := sha1.Sum(data)
				if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 {
					continue
				}
			}
			fmt.Printf("Downloading %s: ", lib.Name)
			resp, err = http.Get(lib.Downloads.Artifact.Url)
			if err != nil {
				fmt.Printf("Error\n")
				return
			}
			defer resp.Body.Close()
			data, _ = io.ReadAll(resp.Body)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != lib.Downloads.Artifact.Sha1 {
				fmt.Printf("Sha1 Mismatch\n")
				return
			}
			path := ""
			tokens := strings.Split(lib.Downloads.Artifact.Path, "/")
			for ind, token := range tokens {
				if ind != len(tokens)-1 {
					path = filepath.Join(path, token)
				}
			}
			os.MkdirAll(filepath.Join(dir, "minecraft_test", "lib", path), 0755)
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "lib", lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
			defer f.Close()
			f.Write(data)
			fmt.Printf("OK\n")
		}
		download := false
		if os.Stat(filepath.Join(dir, "minecraft_test", "bin", metadata.Id, "client.jar")); err == nil {
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "bin", metadata.Id, "client.jar"), os.O_RDONLY, 0755)
			defer f.Close()
			data, _ := io.ReadAll(f)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != metadata.Downloads.Client.Sha1 {
				download = true
			}
		} else {
			download = true
		}

		if download {
			fmt.Printf("Downloading minecraft.jar: ")
			req, err := http.Get(metadata.Downloads.Client.Url)
			if err != nil {
				fmt.Printf("ERROR\n")
				return
			}
			defer req.Body.Close()
			data, _ := io.ReadAll(req.Body)
			sha := sha1.Sum(data)
			if hex.EncodeToString(sha[:20]) != metadata.Downloads.Client.Sha1 {
				fmt.Printf("Sha Mismatch\n")
				return
			}
			fmt.Printf("OK\n")
			os.MkdirAll(filepath.Join(dir, "minecraft_test", "bin", metadata.Id), 0755)
			f, _ := os.OpenFile(filepath.Join(dir, "minecraft_test", "bin", metadata.Id, "client.jar"), os.O_CREATE|os.O_RDWR, 0755)
			defer f.Close()
			f.Write(data)
		}
		os.MkdirAll(filepath.Join(dir, "minecraft_test", "minecraft"), 0755)
		args := []string{}
		args = append(args, "-Xms512m")
		args = append(args, "-Xmx1024m")
		for _, val := range metadata.Arguments.Jvm {
			val = strings.ReplaceAll(val, "${natives_directory}", filepath.Join(dir, "minecraft_test", "minecraft", "natives"))
			switch val {
				case "-cp":
					args = append(args, val)
			case "${classpath}":
				arg := ""
				for _, lib := range metadata.Libraries {
					if lib.Rules != nil && lib.Rules[0].Os.Name != runtime.GOOS {
						continue
					}
					arg += filepath.Join(dir, "minecraft_test", "lib", lib.Downloads.Artifact.Path) + ":"
				}
				arg += filepath.Join(dir, "minecraft_test", "bin", metadata.Id, "client.jar")
				args = append(args, arg)
			default:
				//args = append(args, val)
			}
		}

		args = append(args, "net.minecraft.client.main.Main")

		for _, val := range metadata.Arguments.Game {
			switch val{
			case "${auth_player_name}":
				args = append(args, auth.Profile.Name)
			case "${version_name}":
				args = append(args, metadata.Id)
			case "${game_directory}":
				args = append(args, filepath.Join(dir, "minecraft_test", "minecraft"))
			case "${assets_root}":
				args = append(args, filepath.Join(dir, "minecraft_test", "assets"))
			case "${auth_uuid}":
				args = append(args, auth.Profile.Id)
			case "${auth_access_token}":
				args = append(args, auth.McAuth.Access_token)
			case "${clientid}":
				args = append(args, client_id)
			case "${auth_xuid}":
				args = append(args, auth.Profile.Id)
			case "${version_type}":
				args = append(args, "release")
			case "${user_type}":
				args = append(args, "mojang")
			case "${assets_index_name}":
				args = append(args, metadata.Assets)
	
		default:
				args = append(args, val)
			}
		}

		fmt.Printf("Args: %+v", args)

		cmd := exec.Command("/usr/lib/jvm/java-21-openjdk/bin/java", args...)
		out, err := cmd.CombinedOutput()
		if err != nil {
			fmt.Printf("Error: %s\n", err)
		}
		fmt.Printf("Output: %s\n", out)


	/*
		fmt.Println("Requesting Oauth")
		token, err := auth.RequestLiveToken()
		if err != nil {
			fmt.Println(err)
			return
		} else {
			ts := auth.RefreshTokenSource(token)
			fmt.Println("Generating Key")
			key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			if err != nil {
				fmt.Println(err)
				return
			}
			ctx := context.Background()
			fmt.Println("Requesting XBL Token")
			token, err = ts.Token()
			if err != nil {
				fmt.Println(err)
				return
			}
			xbl, err := auth.RequestXBLToken(ctx, token, "rp://api.minecraftservices.com/")
			if err != nil {
				fmt.Println(err)
				return
			}
			fmt.Println("Requesting Minecraft Chain")
			ctx = context.Background()
			_, err = auth.RequestMinecraftChain(ctx, xbl, key)
			if err != nil {
				fmt.Println(err)
				return
			}
		}*/
}
