package main

import (
	"archive/zip"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

const versionManifestURL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"

// Переменная для хранения пользовательского пути
var minelauncherPath string

// Структура для хранения манифеста версий
type VersionManifest 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        string `json:"time"`
		ReleaseTime string `json:"releaseTime"`
	} `json:"versions"`
}

// Получение манифеста всех версий
func getVersionManifest() (*VersionManifest, error) {
	resp, err := http.Get(versionManifestURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var manifest VersionManifest
	err = json.Unmarshal(bodyBytes, &manifest)
	if err != nil {
		return nil, err
	}
	return &manifest, nil
}

// Получение данных выбранной версии
func getVersionDetails(versionURL string) (map[string]interface{}, error) {
	resp, err := http.Get(versionURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var versionData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &versionData)
	if err != nil {
		return nil, err
	}
	return versionData, nil
}

// Функция для скачивания файла по URL с повторными попытками
func downloadFile(url, filepath string) error {
	client := http.Client{
		Timeout: 30 * time.Second, // Устанавливаем тайм-аут
	}

	resp, err := client.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Проверяем код ответа
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("не удалось скачать файл: %s, статус код: %d", url, resp.StatusCode)
	}

	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	return err
}

// Скачивание файла с повторами
func downloadFileWithRetry(url, filepath string, retries int) error {
	for i := 0; i <= retries; i++ {
		err := downloadFile(url, filepath)
		if err == nil {
			return nil
		}
		fmt.Printf("Попытка %d скачать файл %s не удалась, ошибка: %v\n", i+1, url, err)
		time.Sleep(2 * time.Second) // Ждем перед повторной попыткой
	}
	return fmt.Errorf("не удалось скачать файл после %d попыток: %s", retries+1, url)
}

// Проверка хэша файла
func verifyFileChecksum(filePath, expectedHash string) bool {
	file, err := os.Open(filePath)
	if err != nil {
		return false
	}
	defer file.Close()

	hash := sha1.New()
	if _, err := io.Copy(hash, file); err != nil {
		return false
	}
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	return expectedHash == calculatedHash
}

// Скачивание клиентского JAR-файла
func downloadClientJar(versionData map[string]interface{}, versionID string) error {
	downloads, ok := versionData["downloads"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось найти downloads в данных версии")
	}
	client, ok := downloads["client"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось найти client в downloads")
	}
	url, ok := client["url"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить URL клиента")
	}
	expectedHash, ok := client["sha1"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить sha1 клиента")
	}

	// Путь для сохранения JAR-файла
	dirPath := filepath.Join(minelauncherPath, "versions", versionID)
	os.MkdirAll(dirPath, os.ModePerm)
	filePath := filepath.Join(dirPath, versionID+".jar")

	// Проверяем, существует ли файл и совпадает ли хэш
	if _, err := os.Stat(filePath); err == nil {
		if verifyFileChecksum(filePath, expectedHash) {
			fmt.Println("Клиентский JAR-файл уже существует и корректен, пропускаем скачивание.")
			return nil
		} else {
			fmt.Println("Хэш клиентского JAR-файла не совпадает, перезагружаем файл.")
			os.Remove(filePath)
		}
	}

	fmt.Println("Скачивание клиентского JAR-файла...")
	err := downloadFileWithRetry(url, filePath, 3)
	if err != nil {
		return err
	}

	// Проверяем хэш после загрузки
	if !verifyFileChecksum(filePath, expectedHash) {
		return fmt.Errorf("хэш клиентского JAR-файла не совпадает после загрузки")
	}

	return nil
}

// Функция для проверки правил библиотеки
func shouldIncludeLibrary(libInfo map[string]interface{}) bool {
	if rules, ok := libInfo["rules"].([]interface{}); ok {
		allowed := false
		for _, ruleInterface := range rules {
			rule, ok := ruleInterface.(map[string]interface{})
			if !ok {
				continue
			}
			action, ok := rule["action"].(string)
			if !ok {
				continue
			}
			osMatch := true
			if osDict, ok := rule["os"].(map[string]interface{}); ok {
				osName := runtime.GOOS
				if osName == "darwin" {
					osName = "osx"
				}
				if name, ok := osDict["name"].(string); ok && name != osName {
					osMatch = false
				}
			}
			if osMatch {
				if action == "allow" {
					allowed = true
				} else if action == "disallow" {
					allowed = false
				}
			}
		}
		return allowed
	}
	return true
}

// Скачивание библиотек
func downloadLibraries(versionData map[string]interface{}) error {
	fmt.Println("Скачивание библиотек...")
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки из данных версии")
	}
	var wg sync.WaitGroup
	semaphore := make(chan struct{}, 10) // Ограничиваем количество одновременных загрузок

	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}

		// Проверяем, следует ли включать эту библиотеку
		if !shouldIncludeLibrary(libInfo) {
			continue
		}

		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}

		artifact, ok := downloads["artifact"].(map[string]interface{})
		if !ok {
			continue
		}

		url, ok := artifact["url"].(string)
		if !ok {
			continue
		}
		path, ok := artifact["path"].(string)
		if !ok {
			continue
		}
		expectedHash, ok := artifact["sha1"].(string)
		if !ok {
			continue
		}

		// Путь для сохранения файла
		filePath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		dir := filepath.Dir(filePath)
		os.MkdirAll(dir, os.ModePerm)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(filePath); err == nil {
			if verifyFileChecksum(filePath, expectedHash) {
				// Файл существует и корректен, пропускаем скачивание
				continue
			} else {
				fmt.Println("Хэш библиотеки не совпадает, перезагружаем файл:", filePath)
				os.Remove(filePath)
			}
		}

		wg.Add(1)
		semaphore <- struct{}{} // блокирует, если семафор заполнен

		go func(url, filePath, expectedHash string) {
			defer wg.Done()
			defer func() { <-semaphore }()

			err := downloadFileWithRetry(url, filePath, 3)
			if err != nil {
				fmt.Println("\nОшибка при скачивании библиотеки:", err)
				return
			}

			if !verifyFileChecksum(filePath, expectedHash) {
				fmt.Printf("\nХэш библиотеки не совпадает после загрузки: %s\n", filePath)
				os.Remove(filePath)
			}
		}(url, filePath, expectedHash)
	}
	wg.Wait()
	return nil
}

// Скачивание ассетов с параллельной загрузкой
func downloadAssets(versionData map[string]interface{}) error {
	fmt.Println("Скачивание ассетов...")
	assetIndex, ok := versionData["assetIndex"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось найти assetIndex в данных версии")
	}
	url, ok := assetIndex["url"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить URL assetIndex")
	}
	expectedHash, ok := assetIndex["sha1"].(string)
	if !ok {
		return fmt.Errorf("не удалось получить sha1 assetIndex")
	}

	// Скачиваем индекс ассетов
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	// Проверяем хэш индекса ассетов
	hash := sha1.New()
	hash.Write(bodyBytes)
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		return fmt.Errorf("хэш индекса ассетов не совпадает")
	}

	var assetIndexData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &assetIndexData)
	if err != nil {
		return err
	}

	// Сохраняем индекс ассетов
	assetsDir := filepath.Join(minelauncherPath, "assets", "indexes")
	os.MkdirAll(assetsDir, os.ModePerm)
	indexFilePath := filepath.Join(assetsDir, assetIndex["id"].(string)+".json")
	err = ioutil.WriteFile(indexFilePath, bodyBytes, os.ModePerm)
	if err != nil {
		return fmt.Errorf("ошибка при сохранении индекса ассетов: %v", err)
	}

	objects, ok := assetIndexData["objects"].(map[string]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить объекты ассетов")
	}

	totalAssets := len(objects)
	fmt.Printf("Всего ассетов для загрузки: %d\n", totalAssets)

	var wg sync.WaitGroup
	semaphore := make(chan struct{}, 20) // Увеличиваем количество одновременных загрузок для ассетов

	currentAsset := 0
	mutex := &sync.Mutex{}

	for _, obj := range objects {
		wg.Add(1)
		semaphore <- struct{}{} // блокирует, если семафор заполнен

		go func(obj interface{}) {
			defer wg.Done()
			defer func() { <-semaphore }() // освобождаем место в семафоре

			objInfo, ok := obj.(map[string]interface{})
			if !ok {
				return
			}
			hashStr, ok := objInfo["hash"].(string)
			if !ok {
				return
			}
			sizeFloat, ok := objInfo["size"].(float64)
			if !ok {
				return
			}
			size := int64(sizeFloat)
			subHash := hashStr[:2]

			assetURL := fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", subHash, hashStr)
			filePath := filepath.Join(minelauncherPath, "assets", "objects", subHash, hashStr)
			dir := filepath.Dir(filePath)
			os.MkdirAll(dir, os.ModePerm)

			// Проверяем, существует ли файл и его размер
			if fi, err := os.Stat(filePath); err == nil && fi.Size() == size {
				// Файл существует и его размер соответствует, пропускаем
			} else {
				err := downloadFileWithRetry(assetURL, filePath, 3)
				if err != nil {
					fmt.Println("\nОшибка при скачивании ассета:", err)
					return
				}
			}

			mutex.Lock()
			currentAsset++
			progress := float64(currentAsset) / float64(totalAssets) * 100
			fmt.Printf("\rСкачивание ассетов: %d/%d (%.2f%%)", currentAsset, totalAssets, progress)
			mutex.Unlock()
		}(obj)
	}

	wg.Wait()
	fmt.Println("\nЗагрузка ассетов завершена.")
	return nil
}

// Построение classpath для запуска игры
func buildClasspath(versionData map[string]interface{}, versionID string) string {
	var classpath []string

	// Добавляем библиотеки
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		fmt.Println("Не удалось получить библиотеки для classpath")
		return ""
	}
	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}
		if !shouldIncludeLibrary(libInfo) {
			continue
		}
		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}
		artifact, ok := downloads["artifact"].(map[string]interface{})
		if !ok {
			continue
		}
		path, ok := artifact["path"].(string)
		if !ok {
			continue
		}
		libPath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		classpath = append(classpath, libPath)
	}

	// Добавляем клиентский JAR
	clientJar := filepath.Join(minelauncherPath, "versions", versionID, versionID+".jar")
	classpath = append(classpath, clientJar)

	return strings.Join(classpath, string(os.PathListSeparator))
}

// Запуск игры
func launchMinecraft(versionData map[string]interface{}, authToken, username, uuid, versionID string) error {
	mainClass, ok := versionData["mainClass"].(string)
	if !ok {
		return fmt.Errorf("не удалось найти mainClass в данных версии")
	}
	fmt.Println("Main class:", mainClass)
	classpath := buildClasspath(versionData, versionID)
	fmt.Println("Classpath:", classpath)

	if classpath == "" {
		return fmt.Errorf("classpath пустой")
	}

	// Путь к директории natives
	nativesDir := filepath.Join(minelauncherPath, "versions", versionID, "natives")
	os.MkdirAll(nativesDir, os.ModePerm)

	// Распаковка нативных библиотек
	err := extractNatives(versionData, nativesDir)
	if err != nil {
		return err
	}

	// Аргументы JVM
	jvmArgs := []string{
		"-Djava.library.path=" + nativesDir,
		"-cp", classpath,
	}

	// Аргументы игры
	gameArgs := buildGameArguments(versionData, authToken, username, uuid, versionID)

	if len(gameArgs) == 0 {
		return fmt.Errorf("аргументы игры пустые")
	}

	// Объединяем аргументы
	args := append(jvmArgs, mainClass)
	args = append(args, gameArgs...)

	// Команда запуска
	cmd := exec.Command("java", args...)
	cmd.Dir = minelauncherPath // Устанавливаем рабочую директорию
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// Выводим команду запуска для отладки
	fmt.Println("Команда запуска:", strings.Join(cmd.Args, " "))

	err = cmd.Run()
	return err
}

// Построение аргументов игры
func buildGameArguments(versionData map[string]interface{}, authToken, username, uuid, versionID string) []string {
	var args []string

	// Проверяем, есть ли поле "arguments" (для новых версий)
	if arguments, ok := versionData["arguments"].(map[string]interface{}); ok {
		gameArgsList, ok := arguments["game"].([]interface{})
		if !ok {
			fmt.Println("Невозможно получить game arguments")
			return args
		}
		for _, arg := range gameArgsList {
			switch arg := arg.(type) {
			case string:
				arg = replaceVariables(arg, authToken, username, uuid, versionID, versionData)
				args = append(args, arg)
			case map[string]interface{}:
				// Обработка условных аргументов
				if rules, ok := arg["rules"].([]interface{}); ok {
					if checkRule(rules) {
						if value, ok := arg["value"].(string); ok {
							value = replaceVariables(value, authToken, username, uuid, versionID, versionData)
							args = append(args, value)
						} else if values, ok := arg["value"].([]interface{}); ok {
							for _, v := range values {
								if sv, ok := v.(string); ok {
									sv = replaceVariables(sv, authToken, username, uuid, versionID, versionData)
									args = append(args, sv)
								}
							}
						}
					}
				}
			}
		}
	} else {
		// Для старых версий используем поле "minecraftArguments"
		if minecraftArguments, ok := versionData["minecraftArguments"].(string); ok {
			splitArgs := strings.Split(minecraftArguments, " ")
			for _, arg := range splitArgs {
				arg = replaceVariables(arg, authToken, username, uuid, versionID, versionData)
				args = append(args, arg)
			}
		}
	}

	return args
}

// Функция для проверки правил аргументов
func checkRule(rules []interface{}) bool {
	// Здесь можно добавить более точную проверку правил
	// Для простоты текущего примера возвращаем true
	return true
}

// Функция для замены переменных в аргументах
func replaceVariables(arg, authToken, username, uuid, versionID string, versionData map[string]interface{}) string {
	// Получаем индекс ассетов из versionData
	assetIndex := versionDataGetAssetIndex(versionData)

	arg = strings.ReplaceAll(arg, "${auth_player_name}", username)
	arg = strings.ReplaceAll(arg, "${version_name}", versionID)
	arg = strings.ReplaceAll(arg, "${game_directory}", minelauncherPath)
	arg = strings.ReplaceAll(arg, "${assets_root}", filepath.Join(minelauncherPath, "assets"))
	arg = strings.ReplaceAll(arg, "${assets_index_name}", getAssetIndexName(assetIndex))
	arg = strings.ReplaceAll(arg, "${auth_uuid}", uuid)
	arg = strings.ReplaceAll(arg, "${auth_access_token}", authToken)
	arg = strings.ReplaceAll(arg, "${user_type}", "msa")
	arg = strings.ReplaceAll(arg, "${version_type}", "release")
	return arg
}

// Получение имени индекса ассетов
func getAssetIndexName(assetIndex map[string]interface{}) string {
	if id, ok := assetIndex["id"].(string); ok {
		return id
	}
	return "legacy"
}

// Получение assetIndex из versionData
func versionDataGetAssetIndex(versionData map[string]interface{}) map[string]interface{} {
	if assetIndex, ok := versionData["assetIndex"].(map[string]interface{}); ok {
		return assetIndex
	}
	return map[string]interface{}{}
}

// Распаковка нативных библиотек
func extractNatives(versionData map[string]interface{}, nativesDir string) error {
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки для распаковки нативов")
	}
	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}

		// Проверяем, следует ли включать эту библиотеку
		if !shouldIncludeLibrary(libInfo) {
			continue
		}

		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}
		classifiers, ok := downloads["classifiers"].(map[string]interface{})
		if !ok {
			continue
		}

		// Определяем текущую ОС
		osName := runtime.GOOS
		var classifierName string
		switch osName {
		case "windows":
			classifierName = "natives-windows"
		case "darwin":
			classifierName = "natives-macos"
		case "linux":
			classifierName = "natives-linux"
		default:
			continue
		}

		native, ok := classifiers[classifierName].(map[string]interface{})
		if !ok {
			continue
		}

		url, ok := native["url"].(string)
		if !ok {
			continue
		}
		path, ok := native["path"].(string)
		if !ok {
			continue
		}
		expectedHash, ok := native["sha1"].(string)
		if !ok {
			continue
		}
		filePath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		dir := filepath.Dir(filePath)
		os.MkdirAll(dir, os.ModePerm)

		fmt.Printf("Скачивание нативной библиотеки: %s\n", filePath)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(filePath); err == nil {
			if verifyFileChecksum(filePath, expectedHash) {
				// Файл существует и корректен, пропускаем скачивание
			} else {
				fmt.Println("Хэш нативной библиотеки не совпадает, перезагружаем файл:", filePath)
				os.Remove(filePath)
			}
		}

		if _, err := os.Stat(filePath); os.IsNotExist(err) {
			err := downloadFileWithRetry(url, filePath, 3)
			if err != nil {
				fmt.Println("Ошибка при скачивании нативной библиотеки:", err)
				return err
			}
		}

		// Проверяем хэш после загрузки
		if !verifyFileChecksum(filePath, expectedHash) {
			fmt.Printf("Хэш нативной библиотеки не совпадает после загрузки: %s\n", filePath)
			return fmt.Errorf("хэш нативной библиотеки не совпадает")
		}

		// Распаковываем нативы
		err := unzip(filePath, nativesDir)
		if err != nil {
			fmt.Println("Ошибка при распаковке нативной библиотеки:", err)
			return err
		}
	}
	return nil
}

// Функция для распаковки ZIP-архива
func unzip(src, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer r.Close()

	for _, f := range r.File {
		fpath := filepath.Join(dest, f.Name)
		// Предотвращение папок, создаваемых внутри архива
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("недопустимый путь в архиве: %s", fpath)
		}

		if f.FileInfo().IsDir() {
			os.MkdirAll(fpath, os.ModePerm)
			continue
		}

		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return err
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return err
		}

		rc, err := f.Open()
		if err != nil {
			outFile.Close()
			return err
		}

		_, err = io.Copy(outFile, rc)

		outFile.Close()
		rc.Close()

		if err != nil {
			return err
		}
	}
	return nil
}

// Проверка наличия всех необходимых файлов перед запуском
func verifyAllFiles(versionData map[string]interface{}, versionID string) error {
	// Проверка клиентского JAR
	clientJar := filepath.Join(minelauncherPath, "versions", versionID, versionID+".jar")
	if _, err := os.Stat(clientJar); os.IsNotExist(err) {
		return fmt.Errorf("клиентский JAR-файл отсутствует: %s", clientJar)
	}

	// Проверка библиотек
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки для проверки")
	}
	for _, lib := range libraries {
		libInfo, ok := lib.(map[string]interface{})
		if !ok {
			continue
		}
		if !shouldIncludeLibrary(libInfo) {
			continue
		}
		downloads, ok := libInfo["downloads"].(map[string]interface{})
		if !ok {
			continue
		}
		artifact, ok := downloads["artifact"].(map[string]interface{})
		if !ok {
			continue
		}
		path, ok := artifact["path"].(string)
		if !ok {
			continue
		}
		libPath := filepath.Join(minelauncherPath, "libraries", filepath.FromSlash(path))
		if _, err := os.Stat(libPath); os.IsNotExist(err) {
			return fmt.Errorf("отсутствует библиотека: %s", libPath)
		}
	}

	// Проверка ассетов (опционально)
	// Здесь можно добавить дополнительные проверки, если необходимо

	return nil
}

func main() {
	// Запрашиваем путь для хранения файлов Minecraft
	fmt.Print("Введите путь для хранения файлов Minecraft (по умолчанию '.legit'): ")
	fmt.Scanln(&minelauncherPath)
	if minelauncherPath == "" {
		minelauncherPath = ".legit"
	}
	// Создаем директорию, если ее нет
	os.MkdirAll(minelauncherPath, os.ModePerm)

	// Получаем манифест версий
	manifest, err := getVersionManifest()
	if err != nil {
		fmt.Println("Ошибка при получении манифеста:", err)
		return
	}

	// Выводим список доступных версий
	fmt.Println("Доступные версии Minecraft:")
	for _, version := range manifest.Versions {
		fmt.Printf("- %s (%s)\n", version.ID, version.Type)
	}

	// Запрашиваем у пользователя выбор версии
	var selectedVersionID string
	fmt.Print("Введите ID версии для скачивания и запуска: ")
	fmt.Scanln(&selectedVersionID)

	// Находим URL выбранной версии
	var selectedVersionURL string
	for _, version := range manifest.Versions {
		if version.ID == selectedVersionID {
			selectedVersionURL = version.URL
			break
		}
	}

	if selectedVersionURL == "" {
		fmt.Println("Версия не найдена.")
		return
	}

	// Получаем данные выбранной версии
	versionData, err := getVersionDetails(selectedVersionURL)
	if err != nil {
		fmt.Println("Ошибка при получении данных версии:", err)
		return
	}

	// Сохраняем данные версии в файл version.json
	versionDir := filepath.Join(minelauncherPath, "versions", selectedVersionID)
	os.MkdirAll(versionDir, os.ModePerm)
	versionJsonPath := filepath.Join(versionDir, selectedVersionID+".json")

	versionDataBytes, err := json.MarshalIndent(versionData, "", "  ")
	if err != nil {
		fmt.Println("Ошибка при сериализации данных версии:", err)
		return
	}

	err = ioutil.WriteFile(versionJsonPath, versionDataBytes, 0644)
	if err != nil {
		fmt.Println("Ошибка при сохранении файла version.json:", err)
		return
	}

	// Скачиваем необходимые файлы
	err = downloadClientJar(versionData, selectedVersionID)
	if err != nil {
		fmt.Println("Ошибка при скачивании клиентского JAR-файла:", err)
		return
	}

	err = downloadLibraries(versionData)
	if err != nil {
		fmt.Println("Ошибка при скачивании библиотек:", err)
		return
	}

	err = downloadAssets(versionData)
	if err != nil {
		fmt.Println("Ошибка при скачивании ассетов:", err)
		return
	}

	// Проверяем наличие всех необходимых файлов
	err = verifyAllFiles(versionData, selectedVersionID)
	if err != nil {
		fmt.Println("Ошибка валидации файлов:", err)
		return
	}

	// Создаем необходимые директории и файлы
	os.MkdirAll(filepath.Join(minelauncherPath, "saves"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "resourcepacks"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "logs"), os.ModePerm)
	os.MkdirAll(filepath.Join(minelauncherPath, "runtime"), os.ModePerm)
	optionsFilePath := filepath.Join(minelauncherPath, "options.txt")
	if _, err := os.Stat(optionsFilePath); os.IsNotExist(err) {
		ioutil.WriteFile(optionsFilePath, []byte(""), 0644)
	}

	// Проверяем наличие Java и ее версии
	javaVersionCmd := exec.Command("java", "-version")
	javaVersionOutput, err := javaVersionCmd.CombinedOutput()
	if err != nil {
		fmt.Println("Ошибка при проверке версии Java:", err)
		return
	}
	fmt.Println("Java version:", string(javaVersionOutput))

	// Здесь должна быть реализация аутентификации пользователя
	// Для простоты используем заглушки
	authToken := ``
	username := "NAMAAA"
	uuid := ``

	// Запускаем игру
	err = launchMinecraft(versionData, authToken, username, uuid, selectedVersionID)
	if err != nil {
		fmt.Println("Ошибка при запуске Minecraft:", err)
		return
	}
}


Vanilla#
Installing a vanilla version is the basis for everything else. I will write about installing loaders like Fabric and Forge later on, but this is the most basic thing launchers should do.

0. The working directory#
The working directory depends on the system. In any case it is a specific directory reserved for Minecraft. For the official launcher this is one single directory, while in multi-instance launchers like MultiMC or HMCL, this directory can be customized according to the launcher. However, most of the files you download / generate will be in this directory.

This directory will henceforth be referred to as .minecraft for convenience. This is the directory used on Linux.

1. The Version Manifest#
The version manifest is the single source of truth for what versions there are. The URL is https://launchermeta.mojang.com/mc/game/version_manifest.json, and the format is approximately like this:

{
  "latest": {
    "release": "1.18.2",
    "snapshot": "22w16b"
  },
  "versions": [
    {
      "id": "22w16b",
      "type": "snapshot",
      "url": "https://launchermeta.mojang.com/v1/packages/891d3bb98263c95e02d37aed36ebfa00d2d6bf9d/22w16b.json",
      "time": "2022-04-20T17:30:03+00:00",
      "releaseTime": "2022-04-20T17:25:32+00:00"
    }
    // ...
  ]
}
Obviously, the latest key corresponds to the latest versions for releases and snapshots. In the versions list, there is an ID (the name of the version), the type (release or snapshot), the URL to the version data, and the time and release time (which don’t really matter that much). The version data URL is going to be used in the next step.

2. The Version Data#
After selecting the version, the JSON data from "url" would be downloaded to .minecraft/versions/<version being downloaded>/<version>.json. This version data JSON has a few sections of data:

"arguments" - arguments to pass

This contains the arguments to run the main JAR with. The "jvm" flags are to be provided before the main JAR, while the "game" flags are provided after the main JAR. So a launch script would look something like this:

java [jvm flags] [main class] [game flags]
The flags provided can either be a string or an object with the type

interface Rule {
  action: "allow" | "disallow";
  features?: { [feature: string]: boolean };
  os?: {
    name?: "osx" | "windows" | "linux";
    version?: string;
    arch?: string;
  };
}

interface OptionalArgument {
  rules: Rule[];
  value: string | string[];
}
The rules feature is also used in the downloading of libraries in a later step. For platform-specific arguments, there are a list of rules to match, for either operating system, architecture, or the feature (e.g. demo user).

Do note that there are a few substitutions needed in these arguments. For the JVM arguments, these are the substitutions needed:

${natives_directory}: the directory of the platform natives (to be mentioned later on)
${launcher_name}: the name of the launcher
${launcher_version}: the version of the launcher
${classpath}: a list of the paths of all the library JARs and the main JAR downloaded joined with :s
${classpath_separator}: :
${primary_jar}: the path to main JAR
${library_directory}: the overall folder in which the libraries were downloaded
${game_directory}: the “working directory” mentioned before
For game arguments, there are also some substitutions needed:

${auth_player_name}: the username of the player
${version_name}: the name of the version (e.g. 1.18.2)
${game_directory}: same as JVM
${assets_root}: the root folder of the downloaded assets, typically .minecraft/assets
${assets_index_name}: the asset index version to use, typically the minor version (e.g. 1.18)
${auth_uuid}: the authentication UUID provided by MSA
${user_type} : all mojang nowadays
${version_type}: the version type, release or snapshot (although this can display any string)
For the JVM args, you can also add any optimization arguments you like, such as enabling G1GC.

"assetIndex" - the index of assets to download

The textures and music and UI controls are all assets, and there is typically an asset index for each minor version. This object has the following type definition:

interface AssetIndex {
  id: string;
  sha1: string;
  size: number;
  totalSize: number;
  url: string;
}
where "sha1" is the SHA-1 hash of the file, "size" is the size of the JSON file, "totalSize" is the size of all the assets combined, and "url" is yet another JSON file to get for the assets.

"downloads" - the main JARs to download

This includes the JARs that you will actually call with java on launch. It’s an object containing downloads for both clients and servers in the format { sha1: string; size: number; url: string; }. Again, the SHA-1 hash is provided. The file in "url" is downloaded to .minecraft/versions/<version>/<version>.jar. For clients, the file to download is ["downloads"]["client"]["url"].

"libraries" - the libraries to download

This is arguably the most complex part of the downloading process. It will be detailed in its own dedicated section in step 3.

"logging" - the logging configuration file to provide to log4j

logging: {
  client: {
    argument: string;

    file: {
      id: string;
      sha1: string;
      size: number;
      url: string;
    }

    type: "log4j2-xml";
  }
}
This contains an XML config file to provide to log4j. Download from ["logging"]["file"]["url"]. The argument in ["logging"]["client"]["argument"] should have the special keyword ${path} replaced with the path the config file was downloaded to. This can be anywhere, but typically in .minecraft/versions/<version>/log4j2.xml.

"mainClass": the main class to call in the execution of java

The others are not very important to our discussion here, and are pretty straightforward, so just look at the JSON if need be.

3. The Assets#
As you may recall, in the "assetIndex" section there was a JSON file to download to .minecraft/assets/indexes/<id>.json. This JSON file contains a single "objects" key in which there are asset files in the following scheme:

"icons/icon_16x16.png": {
  "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a",
  "size": 3665
}
The name of the file does not matter. The file URL is derived from the hash as https://resources.download.minecraft.net/<first 2 hex letters of hash>/<whole hash>. This file is downloaded to .minecraft/assets/objects/<first two letters of hash>/<whole hash>, basically mirroring the URL structure.

4. The Libraries#
This section downloads the libraries needed for the game. Each library is in this standardized format:

interface LibraryDownload {
  path: string;
  sha1: string;
  size: number;
  url: string;
}

interface MojangLibrary {
  downloads: {
    artifact?: LibraryDownload;
    classifiers?: { [classifier: string]: LibraryDownload };
  };
  name: string;
  rules?: Rule[];
  natives?: { [os: string]: string };
}
The LibraryDownload type is pretty similar to the ones before. The "rules" object functions identically to the one for JVM and game arguments. For most libraries, there will be an "artifact" key, and all you need is to download library.downloads.artifact.url to .minecraft/libraries/<library.downloads.artifact.path>. However, for libraries with natives, it is a bit different.

Natives are binaries such as dynamic libraries and DLLs that are specific for each platform. If the "natives" key is present, the classifier needed to download will be in library["natives"][os]. Then put this classifier into library.downloads.classifiers[classifier], and download the JAR in there to a temporary location. Then, unzip (since JARs are zips) the JAR and take the natives (glob: *.{dylib,so,dll}) and put them in a directory for the version, typically .minecraft/versions/<version>/natives.

This directory is what you substitute ${natives_directory} for. This is necessary for some libraries like LWJGL and java-objc-bridge to work, since they have platform-specific bindings.

5. Actually launching#
When you launch, simply run the substitutions on the JVM and game arguments previously mentioned. Then execute java like this:

java [JVM args] [main class] [game args]
And there you have it! Downloading and launching vanilla Minecraft.

перепиши весь код ведь сечас ошибка Error: Could not find or load main class net.minecraft.client.main.Main
Caused by: java.lang.ClassNotFoundException: net.minecraft.client.main.Main 

из за того что не хватает нужных фалов то есть нужных фалов не скачивается например файлы в natives там чтото долно быть но она пуста исправляй