package launcher

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

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

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,omitempty"`
		Time        string `json:"time,omitempty"`
		ReleaseTime string `json:"releaseTime,omitempty"`
	} `json:"versions"`
}

type VersionData map[string]interface{}

type Launcher struct {
	minelauncherPath string
	versionManifest  *VersionManifest
}

func NewLauncher() *Launcher {
	return &Launcher{
		minelauncherPath: ".legit", // Задаем путь по умолчанию
	}
}

func (l *Launcher) SetMinecraftPath(path string) {
	l.minelauncherPath = path
}

func (l *Launcher) GetMinecraftPath() string {
	return l.minelauncherPath
}

func (l *Launcher) loadCustomVersions() ([]struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	URL         string `json:"url,omitempty"`
	Time        string `json:"time,omitempty"`
	ReleaseTime string `json:"releaseTime,omitempty"`
}, error) {
	var customVersions []struct {
		ID          string `json:"id"`
		Type        string `json:"type"`
		URL         string `json:"url,omitempty"`
		Time        string `json:"time,omitempty"`
		ReleaseTime string `json:"releaseTime,omitempty"`
	}

	versionsDir := filepath.Join(l.minelauncherPath, "versions")
	entries, err := os.ReadDir(versionsDir)
	if err != nil {
		return nil, fmt.Errorf("ошибка при чтении директории версий: %v", err)
	}

	for _, entry := range entries {
		if entry.IsDir() {
			versionID := entry.Name()
			versionJsonPath := filepath.Join(versionsDir, versionID, versionID+".json")
			if _, err := os.Stat(versionJsonPath); err == nil {
				data, err := os.ReadFile(versionJsonPath)
				if err != nil {
					fmt.Printf("Предупреждение: не удалось прочитать файл версии %s: %v\n", versionJsonPath, err)
					continue
				}
				var versionData map[string]interface{}
				err = json.Unmarshal(data, &versionData)
				if err != nil {
					fmt.Printf("Предупреждение: ошибка при разборе данных версии %s: %v\n", versionJsonPath, err)
					continue
				}

				versionType, ok := versionData["type"].(string)
				if !ok {
					versionType = "custom" // Default type
				}

				// Append custom version
				customVersions = append(customVersions, struct {
					ID          string `json:"id"`
					Type        string `json:"type"`
					URL         string `json:"url,omitempty"`
					Time        string `json:"time,omitempty"`
					ReleaseTime string `json:"releaseTime,omitempty"`
				}{
					ID:   versionID,
					Type: versionType,
				})
			}
		}
	}

	return customVersions, nil
}

func (l *Launcher) getVersionManifest() (*VersionManifest, error) {
	fmt.Println("Получение манифеста версий...")

	// Fetch the official version manifest
	resp, err := http.Get(versionManifestURL)
	if err != nil {
		return nil, fmt.Errorf("не удалось получить манифест версий: %v", err)
	}
	defer resp.Body.Close()

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

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("не удалось прочитать тело ответа манифеста версий: %v", err)
	}

	var manifest VersionManifest
	err = json.Unmarshal(bodyBytes, &manifest)
	if err != nil {
		return nil, fmt.Errorf("ошибка при разборе манифеста версий: %v", err)
	}

	// Scan for custom versions in the 'versions' directory
	customVersions, err := l.loadCustomVersions()
	if err != nil {
		fmt.Printf("Предупреждение: не удалось загрузить пользовательские версии: %v\n", err)
	} else {
		manifest.Versions = append(manifest.Versions, customVersions...)
	}

	return &manifest, nil
}

func (l *Launcher) GetMinecraftVersions() ([]string, error) {
	if l.versionManifest == nil {
		manifest, err := l.getVersionManifest()
		if err != nil {
			return nil, err
		}
		l.versionManifest = manifest
	}

	versionsMap := make(map[string]string)
	for _, v := range l.versionManifest.Versions {
		if v.Type == "custom" {
			versionsMap[v.ID] = fmt.Sprintf("%s (custom)", v.ID)
		} else {
			versionsMap[v.ID] = fmt.Sprintf("%s (%s)", v.ID, v.Type)
		}
	}

	// Собираем версии в слайс
	versions := []string{}
	for _, v := range versionsMap {
		versions = append(versions, v)
	}
	return versions, nil
}

func (l *Launcher) getVersionDetails(versionID string) (VersionData, error) {
	fmt.Printf("Получение данных для версии: %s\n", versionID)

	// Проверяем локальный JSON-файл версии
	versionJsonPath := filepath.Join(l.minelauncherPath, "versions", versionID, versionID+".json")
	if _, err := os.Stat(versionJsonPath); err == nil {
		// Локальный JSON существует, загружаем его
		data, err := os.ReadFile(versionJsonPath)
		if err != nil {
			return nil, fmt.Errorf("не удалось прочитать локальный файл версии %s: %v", versionJsonPath, err)
		}
		var versionData VersionData
		err = json.Unmarshal(data, &versionData)
		if err != nil {
			return nil, fmt.Errorf("ошибка при разборе локальных данных версии %s: %v", versionJsonPath, err)
		}
		return versionData, nil
	}

	// Иначе получаем данные версии из официального манифеста
	var versionURL string
	for _, version := range l.versionManifest.Versions {
		if version.ID == versionID {
			versionURL = version.URL
			break
		}
	}
	if versionURL == "" {
		return nil, fmt.Errorf("версия %s не найдена в манифесте", versionID)
	}

	resp, err := http.Get(versionURL)
	if err != nil {
		return nil, fmt.Errorf("не удалось получить данные версии: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("не удалось получить данные версии, статус код: %d", resp.StatusCode)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("не удалось прочитать тело ответа версии: %v", err)
	}

	var versionData VersionData
	err = json.Unmarshal(bodyBytes, &versionData)
	if err != nil {
		return nil, fmt.Errorf("ошибка при разборе данных версии: %v", err)
	}
	return versionData, nil
}

func (l *Launcher) mergeVersionData(versionData VersionData) (VersionData, error) {
	if parentID, ok := versionData["inheritsFrom"].(string); ok {
		parentData, err := l.getVersionDetails(parentID)
		if err != nil {
			return nil, fmt.Errorf("ошибка при получении родительской версии %s: %v", parentID, err)
		}
		// Рекурсивно объединяем данные родительской версии
		parentData, err = l.mergeVersionData(parentData)
		if err != nil {
			return nil, err
		}
		// Объединяем данные parentData в versionData
		mergedData := l.deepMergeVersionData(parentData, versionData)
		return mergedData, nil
	}
	return versionData, nil
}

func (l *Launcher) deepMergeVersionData(parent, child VersionData) VersionData {
	merged := make(VersionData)
	// Копируем данные родительской версии
	for k, v := range parent {
		merged[k] = v
	}
	// Переопределяем данными дочерней версии
	for k, v := range child {
		if k == "libraries" {
			// Объединяем массивы библиотек
			parentLibs, _ := parent["libraries"].([]interface{})
			childLibs, _ := v.([]interface{})
			mergedLibs := append(parentLibs, childLibs...)
			merged["libraries"] = mergedLibs
		} else if k == "arguments" {
			// Объединяем аргументы
			parentArgs, _ := parent["arguments"].(map[string]interface{})
			childArgs, _ := v.(map[string]interface{})
			mergedArgs := make(map[string]interface{})
			for ak, av := range parentArgs {
				if childArgValue, hasChildArg := childArgs[ak]; hasChildArg {
					if isNonEmpty(childArgValue) {
						// Если дочерний аргумент не пустой, используем его
						mergedArgs[ak] = childArgValue
					} else {
						// Если дочерний аргумент пустой, сохраняем родительский
						mergedArgs[ak] = av
					}
				} else {
					// Если в дочерней версии нет этого аргумента, сохраняем родительский
					mergedArgs[ak] = av
				}
			}
			// Добавляем новые аргументы из дочерней версии, которых нет в родительской
			for ak, av := range childArgs {
				if _, exists := parentArgs[ak]; !exists {
					mergedArgs[ak] = av
				}
			}
			merged["arguments"] = mergedArgs
		} else if k == "logging" {
			// Переопределяем logging
			merged["logging"] = v
		} else {
			merged[k] = v
		}
	}
	return merged
}

func isNonEmpty(value interface{}) bool {
	if value == nil {
		return false
	}
	switch v := value.(type) {
	case []interface{}:
		return len(v) > 0
	case map[string]interface{}:
		return len(v) > 0
	default:
		return true
	}
}

func (l *Launcher) downloadFile(url, filepath string) error {
	fmt.Printf("Скачивание файла: %s\n", url)
	client := http.Client{
		Timeout: 30 * time.Second,
	}

	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("ошибка при выполнении GET-запроса: %v", 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 fmt.Errorf("не удалось создать файл %s: %v", filepath, err)
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return fmt.Errorf("ошибка при записи файла %s: %v", filepath, err)
	}

	fmt.Printf("Файл скачан и сохранён: %s\n", filepath)
	return nil
}

func (l *Launcher) downloadFileWithRetry(url, filepath string, retries int) error {
	for i := 0; i <= retries; i++ {
		err := l.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 (l *Launcher) verifyFileChecksum(filePath, expectedHash string) bool {
	file, err := os.Open(filePath)
	if err != nil {
		fmt.Printf("Не удалось открыть файл для проверки хэша: %s, ошибка: %v\n", filePath, err)
		return false
	}
	defer file.Close()

	hash := sha1.New()
	if _, err := io.Copy(hash, file); err != nil {
		fmt.Printf("Не удалось вычислить хэш для файла: %s, ошибка: %v\n", filePath, err)
		return false
	}
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		fmt.Printf("Хэш не совпадает для файла: %s (ожидалось: %s, получено: %s)\n", filePath, expectedHash, calculatedHash)
		return false
	}
	return true
}

func (l *Launcher) shouldIncludeLibrary(libInfo map[string]interface{}) bool {
	allowed := true
	if rules, ok := libInfo["rules"].([]interface{}); ok {
		for _, ruleInterface := range rules {
			rule, ok := ruleInterface.(map[string]interface{})
			if !ok {
				continue
			}
			action, ok := rule["action"].(string)
			if !ok {
				continue
			}

			applies := 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 {
					applies = false
				}
			}

			if applies {
				if action == "allow" {
					allowed = true
				} else if action == "disallow" {
					allowed = false
				}
			}
		}
	}
	return allowed
}

func (l *Launcher) getNatives(libInfo map[string]interface{}) string {
	archType := "64"
	if runtime.GOARCH == "386" || runtime.GOARCH == "arm" {
		archType = "32"
	}

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

	var classifierName string
	switch runtime.GOOS {
	case "windows":
		if val, exists := natives["windows"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	case "darwin":
		if val, exists := natives["osx"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	case "linux":
		if val, exists := natives["linux"].(string); exists {
			classifierName = strings.ReplaceAll(val, "${arch}", archType)
		}
	default:
		return ""
	}

	return classifierName
}

func (l *Launcher) unzip(src, dest string) error {
	fmt.Printf("Распаковка архива: %s в %s\n", src, dest)
	r, err := zip.OpenReader(src)
	if err != nil {
		return fmt.Errorf("не удалось открыть ZIP-архив %s: %v", src, err)
	}
	defer r.Close()

	for _, f := range r.File {
		if strings.HasPrefix(f.Name, "META-INF") {
			continue
		}

		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() {
			err := os.MkdirAll(fpath, os.ModePerm)
			if err != nil {
				return fmt.Errorf("не удалось создать директорию %s: %v", fpath, err)
			}
			continue
		}

		if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return fmt.Errorf("не удалось создать директорию %s: %v", filepath.Dir(fpath), err)
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return fmt.Errorf("не удалось открыть файл для записи %s: %v", fpath, err)
		}

		rc, err := f.Open()
		if err != nil {
			outFile.Close()
			return fmt.Errorf("не удалось открыть файл внутри архива %s: %v", f.Name, err)
		}

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

		if err != nil {
			return fmt.Errorf("ошибка при копировании файла %s: %v", fpath, err)
		}
	}
	return nil
}

func (l *Launcher) verifyMainClass(clientJarPath string) bool {
	r, err := zip.OpenReader(clientJarPath)
	if err != nil {
		fmt.Printf("Не удалось открыть client.jar для проверки main class: %v\n", err)
		return false
	}
	defer r.Close()

	target := "net/minecraft/client/main/Main.class"
	for _, f := range r.File {
		if f.Name == target {
			return true
		}
	}
	fmt.Println("Main class не найден в client.jar")
	return false
}

func (l *Launcher) downloadClientJar(versionData VersionData, versionID string) error {
	fmt.Println("Скачивание клиентского JAR-файла...")

	dirPath := filepath.Join(l.minelauncherPath, "versions", versionID)
	os.MkdirAll(dirPath, os.ModePerm)
	filePath := filepath.Join(dirPath, versionID+".jar")

	// Проверяем, существует ли файл client.jar
	if _, err := os.Stat(filePath); err == nil {
		fmt.Println("Клиентский JAR-файл уже существует, пропускаем скачивание.")
		return nil
	}

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

			absFilePath, err := filepath.Abs(filePath)
			if err != nil {
				return fmt.Errorf("не удалось получить абсолютный путь к файлу %s: %v", filePath, err)
			}

			err = l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				return fmt.Errorf("ошибка при скачивании клиентского JAR-файла: %v", err)
			}

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

			if !l.verifyMainClass(absFilePath) {
				return fmt.Errorf("файл client.jar не содержит необходимого основного класса")
			}

			return nil
		}
	}

	// Если не удалось найти информацию о загрузке, проверяем наличие "inheritsFrom"
	if parentID, ok := versionData["inheritsFrom"].(string); ok {
		// Пытаемся использовать client.jar родительской версии
		parentClientJar := filepath.Join(l.minelauncherPath, "versions", parentID, parentID+".jar")
		if _, err := os.Stat(parentClientJar); err != nil {
			// Если файл не существует, скачиваем родительскую версию
			parentData, err := l.getVersionDetails(parentID)
			if err != nil {
				return fmt.Errorf("не удалось получить данные родительской версии %s: %v", parentID, err)
			}
			err = l.downloadClientJar(parentData, parentID)
			if err != nil {
				return err
			}
		}
		// Копируем client.jar из родительской версии
		err := copyFile(parentClientJar, filePath)
		if err != nil {
			return fmt.Errorf("не удалось скопировать client.jar из родительской версии: %v", err)
		}
		return nil
	}

	return fmt.Errorf("не удалось скачать или найти client.jar для версии %s", versionID)
}

func copyFile(src, dst string) error {
	input, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("не удалось открыть файл %s для копирования: %v", src, err)
	}
	defer input.Close()

	output, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("не удалось создать файл %s для копирования: %v", dst, err)
	}
	defer output.Close()

	_, err = io.Copy(output, input)
	if err != nil {
		return fmt.Errorf("ошибка при копировании файла %s в %s: %v", src, dst, err)
	}

	return nil
}

func (l *Launcher) downloadLibraries(versionData VersionData) 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 !l.shouldIncludeLibrary(libInfo) {
			continue
		}

		var downloadInfo map[string]interface{}
		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				downloadInfo = artifact
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			var url string
			if customURL, ok := libInfo["url"].(string); ok {
				url = customURL
			} else {
				url = "https://libraries.minecraft.net/"
			}

			path := l.mavenArtifactPath(name)
			fullURL := url + path

			downloadInfo = map[string]interface{}{
				"url":  fullURL,
				"path": path,
			}
		}

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

		filePath := filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		absFilePath, err := filepath.Abs(filePath)
		if err != nil {
			fmt.Printf("Не удалось получить абсолютный путь для файла %s: %v\n", filePath, err)
			continue
		}
		dir := filepath.Dir(absFilePath)
		os.MkdirAll(dir, os.ModePerm)

		// Проверяем, существует ли файл и совпадает ли хэш
		if _, err := os.Stat(absFilePath); err == nil {
			if expectedHash != "" && l.verifyFileChecksum(absFilePath, expectedHash) {
				continue
			} else {
				fmt.Printf("Хэш библиотеки не совпадает, перезагружаем файл: %s\n", absFilePath)
				os.Remove(absFilePath)
			}
		}

		wg.Add(1)
		semaphore <- struct{}{}

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

			err := l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				fmt.Printf("Ошибка при скачивании библиотеки %s: %v\n", url, err)
				return
			}

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

func (l *Launcher) mavenArtifactPath(name string) string {
	parts := strings.Split(name, ":")
	if len(parts) < 3 {
		return ""
	}
	groupID := parts[0]
	artifactID := parts[1]
	version := parts[2]
	classifier := ""
	if len(parts) > 3 {
		classifier = "-" + parts[3]
	}
	path := fmt.Sprintf("%s/%s/%s/%s-%s%s.jar",
		strings.ReplaceAll(groupID, ".", "/"),
		artifactID,
		version,
		artifactID,
		version,
		classifier)
	return path
}

func (l *Launcher) downloadAssets(versionData VersionData) 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")
	}

	// Скачиваем индекс ассетов
	fmt.Println("Скачивание индекса ассетов...")
	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("не удалось скачать индекс ассетов: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("не удалось скачать индекс ассетов, статус код: %d", resp.StatusCode)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("не удалось прочитать индекс ассетов: %v", err)
	}

	hash := sha1.New()
	hash.Write(bodyBytes)
	calculatedHash := hex.EncodeToString(hash.Sum(nil))
	if expectedHash != calculatedHash {
		return fmt.Errorf("хэш индекса ассетов не совпадает (ожидалось: %s, получено: %s)", expectedHash, calculatedHash)
	}

	var assetIndexData map[string]interface{}
	err = json.Unmarshal(bodyBytes, &assetIndexData)
	if err != nil {
		return fmt.Errorf("ошибка при разборе индекса ассетов: %v", err)
	}

	fmt.Println("Сохранение индекса ассетов...")
	assetsDir := filepath.Join(l.minelauncherPath, "assets", "indexes")
	os.MkdirAll(assetsDir, os.ModePerm)
	indexFilePath := filepath.Join(assetsDir, fmt.Sprintf("%s.json", assetIndex["id"].(string)))
	err = os.WriteFile(indexFilePath, bodyBytes, 0644)
	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)
			if len(hashStr) < 2 {
				fmt.Printf("Некорректный хэш ассета: %s\n", hashStr)
				return
			}
			subHash := hashStr[:2]

			assetURL := fmt.Sprintf("https://resources.download.minecraft.net/%s/%s", subHash, hashStr)
			filePath := filepath.Join(l.minelauncherPath, "assets", "objects", subHash, hashStr)
			absFilePath, err := filepath.Abs(filePath)
			if err != nil {
				fmt.Printf("Не удалось получить абсолютный путь для ассета %s: %v\n", filePath, err)
				return
			}
			dir := filepath.Dir(absFilePath)
			os.MkdirAll(dir, os.ModePerm)

			if fi, err := os.Stat(absFilePath); err == nil && fi.Size() == size {
			} else {
				err := l.downloadFileWithRetry(assetURL, absFilePath, 4)
				if err != nil {
					fmt.Printf("\nОшибка при скачивании ассета %s: %v\n", assetURL, 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
}

func (l *Launcher) extractNatives(versionData VersionData, nativesDir string) error {
	fmt.Println("Извлечение нативных библиотек в:", nativesDir)
	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		return fmt.Errorf("не удалось получить библиотеки для распаковки нативов")
	}

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

		if !l.shouldIncludeLibrary(libInfo) {
			continue
		}

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

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

		classifierName := l.getNatives(libInfo)
		if classifierName == "" {
			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, _ := native["sha1"].(string)

		filePath := filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		absFilePath, err := filepath.Abs(filePath)
		if err != nil {
			fmt.Printf("Не удалось получить абсолютный путь для файла %s: %v\n", filePath, err)
			continue
		}
		dir := filepath.Dir(absFilePath)
		os.MkdirAll(dir, os.ModePerm)

		if _, err := os.Stat(absFilePath); err == nil {
			if expectedHash != "" && l.verifyFileChecksum(absFilePath, expectedHash) {
				fmt.Printf("Нативная библиотека уже существует и корректна: %s\n", absFilePath)
			} else {
				fmt.Printf("Хэш нативной библиотеки не совпадает, перезагружаем файл: %s\n", absFilePath)
				os.Remove(absFilePath)
			}
		}

		if _, err := os.Stat(absFilePath); os.IsNotExist(err) {
			fmt.Printf("Скачивание нативной библиотеки: %s\n", absFilePath)
			err := l.downloadFileWithRetry(url, absFilePath, 4)
			if err != nil {
				fmt.Printf("Ошибка при скачивании нативной библиотеки %s: %v\n", url, err)
				return err
			}
		}

		if expectedHash != "" && !l.verifyFileChecksum(absFilePath, expectedHash) {
			return fmt.Errorf("хэш нативной библиотеки не совпадает после загрузки: %s", absFilePath)
		}

		fmt.Printf("Распаковка нативных библиотек из %s в %s\n", absFilePath, nativesDir)
		err = l.unzip(absFilePath, nativesDir)
		if err != nil {
			return fmt.Errorf("ошибка при распаковке нативной библиотеки %s: %v", absFilePath, err)
		}
	}

	return nil
}

func (l *Launcher) buildClasspath(versionData VersionData, versionID string) string {
	var classpath []string

	libraries, ok := versionData["libraries"].([]interface{})
	if !ok {
		fmt.Println("Не удалось получить библиотеки для classpath")
		return ""
	}

	librarySet := make(map[string]struct{}) // Для избежания дублирования

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

		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				path, ok := artifact["path"].(string)
				if !ok {
					continue
				}
				libPath = filepath.Join("libraries", filepath.FromSlash(path))
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			path := l.mavenArtifactPath(name)
			libPath = filepath.Join("libraries", filepath.FromSlash(path))
		}

		if libPath != "" {
			if _, exists := librarySet[libPath]; !exists {
				librarySet[libPath] = struct{}{}
				classpath = append(classpath, libPath)
			}
		}
	}

	clientJar := filepath.Join("versions", versionID, versionID+".jar")
	classpath = append(classpath, clientJar)

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

func (l *Launcher) replaceVariables(arg, username, versionID string, versionData VersionData, uuid, authToken string) string {
	assetIndex := l.versionDataGetAssetIndex(versionData)

	arg = strings.ReplaceAll(arg, "${auth_player_name}", username)
	arg = strings.ReplaceAll(arg, "${version_name}", versionID)
	arg = strings.ReplaceAll(arg, "${game_directory}", l.minelauncherPath)
	arg = strings.ReplaceAll(arg, "${assets_root}", filepath.Join(l.minelauncherPath, "assets"))
	arg = strings.ReplaceAll(arg, "${assets_index_name}", l.getAssetIndexName(assetIndex))
	arg = strings.ReplaceAll(arg, "${auth_uuid}", uuid)
	arg = strings.ReplaceAll(arg, "${auth_access_token}", authToken)
	arg = strings.ReplaceAll(arg, "${clientid}", "")
	arg = strings.ReplaceAll(arg, "${auth_xuid}", "")
	arg = strings.ReplaceAll(arg, "${version_type}", l.versionDataGetVersionType(versionData))
	arg = strings.ReplaceAll(arg, "${user_type}", "legacy")
	arg = strings.ReplaceAll(arg, "--demo", "")
	arg = strings.ReplaceAll(arg, "${natives_directory}", filepath.Join(l.minelauncherPath, "versions", versionID, "natives"))
	arg = strings.ReplaceAll(arg, "${launcher_name}", "MyCustomLauncher")
	arg = strings.ReplaceAll(arg, "${launcher_version}", "1.0")

	// Устанавливаем значения разрешения (ширины и высоты)
	resolutionWidth := "854"  // Значение ширины по умолчанию
	resolutionHeight := "480" // Значение высоты по умолчанию

	if resolutionWidth != "" {
		arg = strings.ReplaceAll(arg, "${resolution_width}", resolutionWidth)
	} else {
		// Удаляем опцию '--width' и ее значение из аргументов
		arg = strings.ReplaceAll(arg, "--width ${resolution_width}", "")
	}

	if resolutionHeight != "" {
		arg = strings.ReplaceAll(arg, "${resolution_height}", resolutionHeight)
	} else {
		// Удаляем опцию '--height' и ее значение из аргументов
		arg = strings.ReplaceAll(arg, "--height ${resolution_height}", "")
	}

	return arg
}

func (l *Launcher) getAssetIndexName(assetIndex map[string]interface{}) string {
	if id, ok := assetIndex["id"].(string); ok {
		return id
	}
	return "legacy"
}

func (l *Launcher) versionDataGetAssetIndex(versionData VersionData) map[string]interface{} {
	if assetIndex, ok := versionData["assetIndex"].(map[string]interface{}); ok {
		return assetIndex
	}
	return map[string]interface{}{}
}

func (l *Launcher) versionDataGetVersionType(versionData VersionData) string {
	if versionType, ok := versionData["type"].(string); ok {
		return versionType
	}
	return "release"
}

func (l *Launcher) checkRule(rules []interface{}) bool {
	allowed := false
	for _, ruleInterface := range rules {
		rule, ok := ruleInterface.(map[string]interface{})
		if !ok {
			continue
		}
		action, ok := rule["action"].(string)
		if !ok {
			continue
		}

		applies := 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 {
				applies = false
			}
		}

		if applies {
			if action == "allow" {
				allowed = true
			} else if action == "disallow" {
				allowed = false
			}
		}
	}
	return allowed
}

func (l *Launcher) buildGameArguments(versionData VersionData, username, versionID, uuid, authToken string) []string {
	var args []string

	if arguments, ok := versionData["arguments"].(map[string]interface{}); ok {
		if gameArgsList, ok := arguments["game"].([]interface{}); ok {
			for _, arg := range gameArgsList {
				switch arg := arg.(type) {
				case string:
					if strings.Contains(strings.ToLower(arg), "quickplay") {
						continue
					}
					arg = l.replaceVariables(arg, username, versionID, versionData, uuid, authToken)
					args = append(args, arg)
				case map[string]interface{}:
					if rules, ok := arg["rules"].([]interface{}); ok {
						if l.checkRule(rules) {
							if value, ok := arg["value"].(interface{}); ok {
								args = l.appendArgumentValueWithoutQuickPlay(args, value, username, versionID, versionData, uuid, authToken)
							}
						}
					} else {
						if value, ok := arg["value"].(interface{}); ok {
							args = l.appendArgumentValueWithoutQuickPlay(args, value, username, versionID, versionData, uuid, authToken)
						}
					}
				}
			}
		}
	} else if minecraftArguments, ok := versionData["minecraftArguments"].(string); ok {
		splitArgs := strings.Split(minecraftArguments, " ")
		for _, arg := range splitArgs {
			if strings.Contains(strings.ToLower(arg), "quickplay") {
				continue
			}
			arg = l.replaceVariables(arg, username, versionID, versionData, uuid, authToken)
			args = append(args, arg)
		}
	}

	return args
}

func (l *Launcher) appendArgumentValueWithoutQuickPlay(args []string, value interface{}, username, versionID string, versionData VersionData, uuid, authToken string) []string {
	switch v := value.(type) {
	case string:
		if strings.Contains(strings.ToLower(v), "quickplay") {
			return args
		}
		v = l.replaceVariables(v, username, versionID, versionData, uuid, authToken)
		args = append(args, v)
	case []interface{}:
		for _, item := range v {
			if sv, ok := item.(string); ok {
				if strings.Contains(strings.ToLower(sv), "quickplay") {
					continue
				}
				sv = l.replaceVariables(sv, username, versionID, versionData, uuid, authToken)
				args = append(args, sv)
			}
		}
	}
	return args
}

func (l *Launcher) buildJvmArguments(versionData VersionData, nativesDir string) []string {
	var args []string

	// Добавляем стандартные JVM аргументы
	args = append(args, "-Djava.library.path="+nativesDir)
	args = append(args, "-Xmx1G")   // Максимальный объем памяти
	args = append(args, "-Xms512M") // Минимальный объем памяти

	if arguments, ok := versionData["arguments"].(map[string]interface{}); ok {
		if jvmArgsList, ok := arguments["jvm"].([]interface{}); ok {
			for _, arg := range jvmArgsList {
				switch arg := arg.(type) {
				case string:
					arg = l.replaceVariables(arg, "", "", versionData, "", "")
					args = append(args, arg)
				case map[string]interface{}:
					if rules, ok := arg["rules"].([]interface{}); ok {
						if l.checkRule(rules) {
							if value, ok := arg["value"].(interface{}); ok {
								args = l.appendJvmArgumentValue(args, value, versionData)
							}
						}
					} else {
						if value, ok := arg["value"].(interface{}); ok {
							args = l.appendJvmArgumentValue(args, value, versionData)
						}
					}
				}
			}
		}
	}

	return args
}

func (l *Launcher) appendJvmArgumentValue(args []string, value interface{}, versionData VersionData) []string {
	switch v := value.(type) {
	case string:
		v = l.replaceVariables(v, "", "", versionData, "", "")
		args = append(args, v)
	case []interface{}:
		for _, item := range v {
			if sv, ok := item.(string); ok {
				sv = l.replaceVariables(sv, "", "", versionData, "", "")
				args = append(args, sv)
			}
		}
	}
	return args
}

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

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

	nativesDir := filepath.Join(l.minelauncherPath, "versions", versionID, "natives")
	absNativesDir, err := filepath.Abs(nativesDir)
	if err != nil {
		return fmt.Errorf("не удалось получить абсолютный путь для nativesDir %s: %v", nativesDir, err)
	}
	os.MkdirAll(absNativesDir, os.ModePerm)

	// Распаковка нативных библиотек
	err = l.extractNatives(versionData, absNativesDir)
	if err != nil {
		return fmt.Errorf("ошибка при извлечении нативных библиотек: %v", err)
	}

	// Build JVM and game arguments
	jvmArgs := l.buildJvmArguments(versionData, absNativesDir)
	gameArgs := l.buildGameArguments(versionData, username, versionID, uuid, authToken)

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

	// Combine all arguments
	args := append(jvmArgs, "-cp", classpath, mainClass)
	args = append(args, gameArgs...)

	// Write arguments to a temporary file
	argsFilePath := filepath.Join(os.TempDir(), "minecraft_args.txt")
	err = l.writeArgsToFile(argsFilePath, args)
	if err != nil {
		return fmt.Errorf("ошибка при создании файла аргументов: %v", err)
	}

	// Prepare the command to use the argument file
	cmd := exec.Command("java", "@"+argsFilePath)
	cmd.Dir = l.minelauncherPath
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	fmt.Println("Команда запуска: java @" + argsFilePath)

	err = cmd.Run()
	if err != nil {
		return fmt.Errorf("ошибка при запуске Minecraft: %v", err)
	}

	return nil
}

func (l *Launcher) writeArgsToFile(filePath string, args []string) error {
	var contentBuilder strings.Builder
	for _, arg := range args {
		arg = strings.ReplaceAll(arg, "\\", "\\\\")
		arg = strings.ReplaceAll(arg, `"`, `\"`)

		if strings.ContainsAny(arg, " \t\n\"") {
			arg = `"` + arg + `"`
		}

		contentBuilder.WriteString(arg)
		contentBuilder.WriteString(" ")
	}

	content := contentBuilder.String()
	return os.WriteFile(filePath, []byte(content), 0644)
}

func (l *Launcher) verifyAllFiles(versionData VersionData, versionID string) error {
	fmt.Println("Проверка наличия всех необходимых файлов...")
	clientJar := filepath.Join(l.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 !l.shouldIncludeLibrary(libInfo) {
			continue
		}
		var libPath string

		downloads, hasDownloads := libInfo["downloads"].(map[string]interface{})
		if hasDownloads {
			artifact, hasArtifact := downloads["artifact"].(map[string]interface{})
			if hasArtifact {
				path, ok := artifact["path"].(string)
				if !ok {
					continue
				}
				libPath = filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
			}
		} else {
			// Обработка старого формата
			name, ok := libInfo["name"].(string)
			if !ok {
				continue
			}
			path := l.mavenArtifactPath(name)
			libPath = filepath.Join(l.minelauncherPath, "libraries", filepath.FromSlash(path))
		}

		if libPath == "" {
			fmt.Printf("Пустой путь для библиотеки: %+v\n", libInfo)
			continue
		}

		if _, err := os.Stat(libPath); os.IsNotExist(err) {
			return fmt.Errorf("отсутствует библиотека: %s", libPath)
		}
	}

	fmt.Println("Все необходимые файлы присутствуют.")
	return nil
}

func (l *Launcher) generateOfflineUUID() string {
	uuid := make([]byte, 16)
	_, err := rand.Read(uuid)
	if err != nil {
		panic(err)
	}

	uuid[6] = (uuid[6] & 0x0F) | 0x40
	uuid[8] = (uuid[8] & 0x3F) | 0x80

	return fmt.Sprintf("%x-%x-%x-%x-%x",
		uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}

func (l *Launcher) PlayMinecraftVersion(versionID string, username string) error {
	absPath, err := filepath.Abs(l.minelauncherPath)
	if err != nil {
		return fmt.Errorf("не удалось получить абсолютный путь для %s: %v", l.minelauncherPath, err)
	}
	l.minelauncherPath = absPath

	err = os.MkdirAll(l.minelauncherPath, os.ModePerm)
	if err != nil {
		return fmt.Errorf("не удалось создать директорию %s: %v", l.minelauncherPath, err)
	}

	versionData, err := l.getVersionDetails(versionID)
	if err != nil {
		return err
	}

	// Объединяем данные версий с учетом inheritsFrom
	versionData, err = l.mergeVersionData(versionData)
	if err != nil {
		return err
	}

	versionDir := filepath.Join(l.minelauncherPath, "versions", versionID)
	err = os.MkdirAll(versionDir, os.ModePerm)
	if err != nil {
		return fmt.Errorf("не удалось создать директорию %s: %v", versionDir, err)
	}
	versionJsonPath := filepath.Join(versionDir, versionID+".json")

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

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

	err = l.downloadClientJar(versionData, versionID)
	if err != nil {
		return fmt.Errorf("ошибка при скачивании клиентского JAR-файла: %v", err)
	}

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

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

	err = l.verifyAllFiles(versionData, versionID)
	if err != nil {
		return fmt.Errorf("ошибка валидации файлов: %v", err)
	}

	dirs := []string{
		filepath.Join(l.minelauncherPath, "saves"),
		filepath.Join(l.minelauncherPath, "resourcepacks"),
		filepath.Join(l.minelauncherPath, "logs"),
		filepath.Join(l.minelauncherPath, "runtime"),
	}
	for _, dir := range dirs {
		err := os.MkdirAll(dir, os.ModePerm)
		if err != nil {
			return fmt.Errorf("не удалось создать директорию %s: %v", dir, err)
		}
	}

	optionsFilePath := filepath.Join(l.minelauncherPath, "options.txt")
	if _, err := os.Stat(optionsFilePath); os.IsNotExist(err) {
		err := os.WriteFile(optionsFilePath, []byte(""), 0644)
		if err != nil {
			return fmt.Errorf("не удалось создать файл %s: %v", optionsFilePath, err)
		}
	}

	javaVersionCmd := exec.Command("java", "-version")
	javaVersionOutput, err := javaVersionCmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("ошибка при проверке версии Java: %v", err)
	}
	fmt.Println("Java версия:", string(javaVersionOutput))

	uname := username
	uuid := l.generateOfflineUUID()
	authToken := ""

	err = l.launchMinecraft(versionData, authToken, uname, uuid, versionID)
	if err != nil {
		return fmt.Errorf("ошибка при запуске Minecraft: %v", err)
	}

	fmt.Println("Minecraft запущен успешно!")
	return nil
}


[18:25:20] [Render thread/INFO]: Setting user: MAKAR566
[18:25:20] [Render thread/INFO]: Backend library: LWJGL version 3.2.1 build 12
[18:25:20] [Render thread/INFO]: [STDERR]: [LWJGL] [ERROR] Incompatible Java and native library versions detected.
Possible reasons:
        a) -Djava.library.path is set to a folder containing shared libraries of an older LWJGL version.
        b) The classpath contains jar files of an older LWJGL version.
Possible solutions:
        a) Make sure to not set -Djava.library.path (it is not needed for developing with LWJGL 3) or make
           sure the folder it points to contains the shared libraries of the correct LWJGL version.
        b) Check the classpath and make sure to only have jar files of the same LWJGL version in it.
[18:25:20] [Render thread/INFO]: [STDERR]: [LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers.
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ff97510cb76, pid=7376, tid=12360
#
# JRE version: Java(TM) SE Runtime Environment (21.0.4+8) (build 21.0.4+8-LTS-274)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (21.0.4+8-LTS-274, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# V  [jvm.dll+0x43cb76]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# D:\JBProjects\GOLAND\MineLauncherTest\.legit\hs_err_pid7376.log
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
#
panic: ошибка при запуске Minecraft: ошибка при запуске Minecraft: exit status 1

goroutine 1 [running]:
main.main()
        D:/JBProjects/GOLAND/MineLauncherTest/main.go:31 +0x252


сделай атк чтобы natives всегда были в versions/version/natives понятно

НАПИШИ КОД ЦЕЛИКОМ И ПОЛНОСТЬЮ