import requests
import os
import json
import tkinter as tk
from tkinter import messagebox, Toplevel
from tkinter.ttk import Progressbar, Notebook
from threading import Thread
from tkinter.scrolledtext import ScrolledText

CONFIG_FILE = 'config.json'

# === Загрузка и сохранение конфигурации ===
def load_config():
    if not os.path.exists(CONFIG_FILE):
        default_config = {
            "mc_version": "",
            "mod_loader": "Forge",
            "default_keywords": [],
            "download_folder": "downloads",
            "user_agent": "ModPackBuilder/1.0 (+contact@yourdomain.com)",
        }
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(default_config, f, indent=4, ensure_ascii=False)
    with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
        return json.load(f)

def save_config(config):
    with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=4, ensure_ascii=False)

# === Modrinth API запросы и обработка ===
def search_projects_modrinth(description, project_type, mc_version=None, modloader=None, user_agent="ModPackBuilder"):
    url = 'https://api.modrinth.com/v2/search'
    headers = {'User-Agent': user_agent}
    facets = [
        [f"project_type:{project_type}"],
    ]
    if mc_version:
        facets.append([f"versions:{mc_version}"])
    if modloader and project_type == "mod":
        facets.append([f"categories:{modloader.lower()}"])
    params = {'query': description, 'facets': json.dumps(facets), 'limit': 100}
    response = requests.get(url, params=params, headers=headers)
    if response.status_code == 200:
        return response.json().get('hits', [])
    else:
        messagebox.showerror("Ошибка", f"Modrinth API Ошибка: {response.status_code}")
        return []

def get_latest_compatible_version_modrinth(project_id, mc_version=None, modloader=None, user_agent="ModPackBuilder"):
    url = f'https://api.modrinth.com/v2/project/{project_id}/version'
    headers = {'User-Agent': user_agent}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        versions = response.json()
        if mc_version and modloader:
            compatible_versions = [v for v in versions if mc_version in v['game_versions'] and modloader.lower() in [loader.lower() for loader in v.get('loaders', [])]]
        elif mc_version:
            compatible_versions = [v for v in versions if mc_version in v['game_versions']]
        else:
            compatible_versions = versions

        if compatible_versions:
            for version in compatible_versions:
                if version['version_type'] == 'release':
                    return version
            return compatible_versions[0]
    return None

def get_mod_dependencies(version):
    dependencies = version.get('dependencies', [])
    required = []
    for dep in dependencies:
        if dep['dependency_type'] == 'required':
            required.append(dep['project_id'])
    return required

def get_mod_by_id(mod_id, user_agent="ModPackBuilder"):
    url = f'https://api.modrinth.com/v2/project/{mod_id}'
    headers = {'User-Agent': user_agent}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json()
    return None

# === Spiget API для плагинов ===
def search_plugins_spiget(query, user_agent="ModPackBuilder"):
    url = f'https://api.spiget.org/v2/search/resources/{query}'
    headers = {'User-Agent': user_agent}
    params = {'size': 100}
    response = requests.get(url, params=params, headers=headers)
    if response.status_code == 200:
        return response.json()
    else:
        messagebox.showerror("Ошибка", f"Spiget API Ошибка: {response.status_code}")
        return []

def get_plugin_info_spiget(plugin_id, user_agent="ModPackBuilder"):
    url = f'https://api.spiget.org/v2/resources/{plugin_id}'
    headers = {'User-Agent': user_agent}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json()
    return None

# === Функция загрузки файла ===
def download_file(url, dest_folder, filename, progress_bar=None, user_agent="ModPackBuilder"):
    local_filename = os.path.join(dest_folder, filename)
    headers = {'User-Agent': user_agent}
    with requests.get(url, stream=True, headers=headers) as r:
        r.raise_for_status()
        total_size = int(r.headers.get('content-length', 0))
        with open(local_filename, 'wb') as f:
            downloaded = 0
            for chunk in r.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
                    downloaded += len(chunk)
                    if progress_bar and total_size > 0:
                        progress = (downloaded / total_size) * 100
                        progress_bar['value'] = progress
                        progress_bar.update()
    return local_filename

# === GUI ===
class ModPackBuilder:
    def __init__(self, root, config):
        self.root = root
        self.config = config
        self.root.title("ModPack Builder")

        # Разрешаем изменение размера окна
        self.root.resizable(True, True)

        # Создаем общий фрейм для размещения виджетов
        self.main_frame = tk.Frame(root)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # === Переменные ===
        self.mc_version_var = tk.StringVar(value=config["mc_version"])
        self.modloader_var = tk.StringVar(value=config.get("mod_loader", "Forge"))
        self.description_var = tk.StringVar(value=", ".join(config["default_keywords"]))
        self.build_type_var = tk.StringVar(value="client")

        # Добавляем переменные для категорий модов
        self.mod_categories = {
            "Магия": tk.BooleanVar(),
            "Технические": tk.BooleanVar(),
            "Хардкор": tk.BooleanVar(),
            "Приключения": tk.BooleanVar(),
            "Декоративные": tk.BooleanVar(),
            "Оптимизация": tk.BooleanVar(),
        }

        # Добавляем переменные для исключаемых категорий
        self.exclude_categories = {
            "Читерные": tk.BooleanVar(),
            "Экспериментальные": tk.BooleanVar(),
            "Несовместимые": tk.BooleanVar(),
        }

        # === Создаем вкладки ===
        self.tab_control = Notebook(self.main_frame)
        self.tab_control.pack(expand=1, fill="both")

        # Вкладки
        self.mods_tab = tk.Frame(self.tab_control)
        self.resourcepacks_tab = tk.Frame(self.tab_control)
        self.shaders_tab = tk.Frame(self.tab_control)
        self.plugins_tab = tk.Frame(self.tab_control)

        self.tab_control.add(self.mods_tab, text='Моды')
        self.tab_control.add(self.resourcepacks_tab, text='Ресурс-паки')
        self.tab_control.add(self.shaders_tab, text='Шейдеры')
        self.tab_control.add(self.plugins_tab, text='Плагины')

        # === Моды ===
        self.setup_mods_tab()

        # === Ресурс-паки ===
        self.setup_resourcepacks_tab()

        # === Шейдеры ===
        self.setup_shaders_tab()

        # === Плагины ===
        self.setup_plugins_tab()

        # === Настройки сборки ===
        self.setup_build_settings()

        # === Кнопка скачивания ===
        tk.Button(self.main_frame, text="Скачать сборку", command=self.download_build).pack(pady=5)

        # При изменении размера окна обновляем размеры виджетов
        self.root.bind('<Configure>', self.on_resize)

        # Словари для хранения выбранных элементов
        self.selected_mods_info = {}
        self.selected_resourcepacks_info = {}
        self.selected_shaders_info = {}
        self.selected_plugins_info = {}

    def setup_mods_tab(self):
        tk.Label(self.mods_tab, text="Версия Minecraft:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        tk.Entry(self.mods_tab, textvariable=self.mc_version_var).grid(row=0, column=1, sticky="ew", padx=5, pady=5)

        tk.Label(self.mods_tab, text="Mod Loader:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
        tk.OptionMenu(self.mods_tab, self.modloader_var, "Forge", "Fabric", "Quilt", "Liteloader", "Cauldron").grid(row=1, column=1, sticky="ew", padx=5, pady=5)

        # Добавляем чекбоксы для категорий модов
        tk.Label(self.mods_tab, text="Выберите категории модов:").grid(row=2, column=0, sticky="w", padx=5, pady=5)
        categories_frame = tk.Frame(self.mods_tab)
        categories_frame.grid(row=3, column=0, columnspan=2, sticky="w", padx=5)

        col = 0
        for category, var in self.mod_categories.items():
            chk = tk.Checkbutton(categories_frame, text=category, variable=var)
            chk.grid(row=0, column=col, sticky="w", padx=5)
            col += 1

        # Поле для дополнительных ключевых слов
        tk.Label(self.mods_tab, text="Дополнительные ключевые слова:").grid(row=4, column=0, sticky="w", padx=5, pady=5)
        self.additional_keywords_var = tk.StringVar()
        tk.Entry(self.mods_tab, textvariable=self.additional_keywords_var).grid(row=4, column=1, sticky="ew", padx=5, pady=5)

        # Добавляем чекбоксы для исключаемых категорий модов
        tk.Label(self.mods_tab, text="Исключить категории модов:").grid(row=5, column=0, sticky="w", padx=5, pady=5)
        exclude_frame = tk.Frame(self.mods_tab)
        exclude_frame.grid(row=6, column=0, columnspan=2, sticky="w", padx=5)

        col = 0
        for category, var in self.exclude_categories.items():
            chk = tk.Checkbutton(exclude_frame, text=category, variable=var)
            chk.grid(row=0, column=col, sticky="w", padx=5)
            col += 1

        # Кнопка поиска модов
        tk.Button(self.mods_tab, text="Поиск модов", command=self.search_mods).grid(row=7, column=0, columnspan=2, pady=5)

        # Список модов
        self.mods_list = tk.Listbox(self.mods_tab, selectmode="multiple")
        self.mods_list.grid(row=8, column=0, columnspan=3, sticky="nsew", padx=5, pady=5)
        self.mods_tab.rowconfigure(8, weight=1)
        self.mods_tab.columnconfigure(1, weight=1)

        self.mods_scrollbar = tk.Scrollbar(self.mods_tab, orient="vertical", command=self.mods_list.yview)
        self.mods_scrollbar.grid(row=8, column=3, sticky="ns")
        self.mods_list.config(yscrollcommand=self.mods_scrollbar.set)

        # Добавляем поле для отображения деталей мода
        self.mod_details_text = ScrolledText(self.mods_tab, height=10)
        self.mod_details_text.grid(row=9, column=0, columnspan=3, sticky="nsew", padx=5, pady=5)
        self.mods_tab.rowconfigure(9, weight=1)

        # Привязываем событие выбора элемента в списке
        self.mods_list.bind('<<ListboxSelect>>', self.show_mod_details)

        tk.Button(self.mods_tab, text="Проверить совместимость", command=self.check_compatibility).grid(row=10, column=0, columnspan=2, pady=5)

        # Кнопки сохранения и загрузки конфигурации
        buttons_frame = tk.Frame(self.mods_tab)
        buttons_frame.grid(row=11, column=0, columnspan=3, pady=5)

        tk.Button(buttons_frame, text="Сохранить конфигурацию", command=self.save_user_config).grid(row=0, column=0, padx=5)
        tk.Button(buttons_frame, text="Загрузить конфигурацию", command=self.load_user_config).grid(row=0, column=1, padx=5)

        # Сортировка
        tk.Label(self.mods_tab, text="Сортировать по:").grid(row=12, column=0, sticky="w", padx=5, pady=5)
        self.sort_by_var = tk.StringVar(value="Релевантности")
        tk.OptionMenu(self.mods_tab, self.sort_by_var, "Релевантности", "Скачиваниям", "Подписчикам").grid(row=12, column=1, sticky="w", padx=5)

    def setup_resourcepacks_tab(self):
        self.resourcepack_description_var = tk.StringVar()
        tk.Label(self.resourcepacks_tab, text="Описание (ключевые слова):").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        tk.Entry(self.resourcepacks_tab, textvariable=self.resourcepack_description_var).grid(row=0, column=1, sticky="ew", padx=5, pady=5)
        tk.Button(self.resourcepacks_tab, text="Поиск ресурс-паков", command=self.search_resourcepacks).grid(row=1, column=0, columnspan=2, pady=5)

        # Список ресурс-паков
        self.resourcepacks_list = tk.Listbox(self.resourcepacks_tab, selectmode="multiple")
        self.resourcepacks_list.grid(row=2, column=0, columnspan=2, sticky="nsew", padx=5, pady=5)
        self.resourcepacks_tab.rowconfigure(2, weight=1)
        self.resourcepacks_tab.columnconfigure(1, weight=1)

        self.resourcepacks_scrollbar = tk.Scrollbar(self.resourcepacks_tab, orient="vertical", command=self.resourcepacks_list.yview)
        self.resourcepacks_scrollbar.grid(row=2, column=2, sticky="ns")
        self.resourcepacks_list.config(yscrollcommand=self.resourcepacks_scrollbar.set)

    def setup_shaders_tab(self):
        self.shader_description_var = tk.StringVar()
        tk.Label(self.shaders_tab, text="Описание (ключевые слова):").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        tk.Entry(self.shaders_tab, textvariable=self.shader_description_var).grid(row=0, column=1, sticky="ew", padx=5, pady=5)
        tk.Button(self.shaders_tab, text="Поиск шейдеров", command=self.search_shaders).grid(row=1, column=0, columnspan=2, pady=5)

        # Список шейдеров
        self.shaders_list = tk.Listbox(self.shaders_tab, selectmode="multiple")
        self.shaders_list.grid(row=2, column=0, columnspan=2, sticky="nsew", padx=5, pady=5)
        self.shaders_tab.rowconfigure(2, weight=1)
        self.shaders_tab.columnconfigure(1, weight=1)

        self.shaders_scrollbar = tk.Scrollbar(self.shaders_tab, orient="vertical", command=self.shaders_list.yview)
        self.shaders_scrollbar.grid(row=2, column=2, sticky="ns")
        self.shaders_list.config(yscrollcommand=self.shaders_scrollbar.set)

    def setup_plugins_tab(self):
        self.plugin_description_var = tk.StringVar()
        tk.Label(self.plugins_tab, text="Поиск плагинов:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        tk.Entry(self.plugins_tab, textvariable=self.plugin_description_var).grid(row=0, column=1, sticky="ew", padx=5, pady=5)
        tk.Button(self.plugins_tab, text="Поиск плагинов", command=self.search_plugins).grid(row=1, column=0, columnspan=2, pady=5)

        # Список плагинов
        self.plugins_list = tk.Listbox(self.plugins_tab, selectmode="multiple")
        self.plugins_list.grid(row=2, column=0, columnspan=2, sticky="nsew", padx=5, pady=5)
        self.plugins_tab.rowconfigure(2, weight=1)
        self.plugins_tab.columnconfigure(1, weight=1)

        self.plugins_scrollbar = tk.Scrollbar(self.plugins_tab, orient="vertical", command=self.plugins_list.yview)
        self.plugins_scrollbar.grid(row=2, column=2, sticky="ns")
        self.plugins_list.config(yscrollcommand=self.plugins_scrollbar.set)

    def setup_build_settings(self):
        settings_frame = tk.Frame(self.main_frame)
        settings_frame.pack(fill=tk.X, padx=5, pady=5)

        tk.Label(settings_frame, text="Тип сборки:").grid(row=0, column=0, sticky="w", padx=5)
        tk.OptionMenu(settings_frame, self.build_type_var, "client", "server", "both").grid(row=0, column=1, sticky="w", padx=5)

    def on_resize(self, event):
        # Обновляем размеры виджетов при изменении размера окна
        self.main_frame.update_idletasks()

    # === Функции для вкладки модов ===
    def search_mods(self):
        # Получаем выбранные категории
        selected_categories = [category for category, var in self.mod_categories.items() if var.get()]
        categories_query = " ".join(selected_categories)

        # Получаем дополнительные ключевые слова
        additional_keywords = self.additional_keywords_var.get()

        # Формируем общий запрос
        description = f"{categories_query} {additional_keywords}".strip()

        mc_version = self.mc_version_var.get()
        modloader = self.modloader_var.get()

        self.config["mc_version"] = mc_version
        self.config["mod_loader"] = modloader
        self.config["default_keywords"] = description.split(", ")
        save_config(self.config)

        self.mods_list.delete(0, tk.END)
        self.mods = []

        mods = search_projects_modrinth(description, "mod", mc_version, modloader, self.config["user_agent"])

        # Получаем исключаемые категории
        excluded_categories = [category for category, var in self.exclude_categories.items() if var.get()]

        # Если выбраны категории для исключения, получаем их идентификаторы
        excluded_category_ids = []
        if excluded_categories:
            # Получаем список всех категорий с их идентификаторами
            all_categories = self.get_all_categories()
            for name in excluded_categories:
                cat_id = self.get_category_id_by_name(name, all_categories)
                if cat_id:
                    excluded_category_ids.append(cat_id)

        sort_by = self.sort_by_var.get()
        if sort_by == "Скачиваниям":
            mods.sort(key=lambda m: m.get('downloads', 0), reverse=True)
        elif sort_by == "Подписчикам":
            mods.sort(key=lambda m: m.get('follows', 0), reverse=True)
        # Если "Релевантности", оставляем порядок по умолчанию

        for mod in mods:
            # Проверяем, не входит ли мод в исключаемые категории
            if excluded_category_ids:
                if set(mod.get('categories', [])).intersection(excluded_category_ids):
                    continue  # Пропускаем мод
            self.mods.append({"source": "Modrinth", "data": mod})
            # Получаем информацию о популярности
            downloads = mod.get('downloads', 0)
            followers = mod.get('follows', 0)
            # Отображаем название мода, описание и показатели популярности
            self.mods_list.insert(tk.END, f"{mod['title']} ({downloads} скачиваний, {followers} подписчиков) - {mod['description']}")

    def get_all_categories(self):
        url = 'https://api.modrinth.com/v2/tag/category'
        headers = {'User-Agent': self.config["user_agent"]}
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
        else:
            return []

    def get_category_id_by_name(self, name, categories):
        for category in categories:
            if category['name'].lower() == name.lower():
                return category['slug']
        return None

    def show_mod_details(self, event):
        selection = self.mods_list.curselection()
        if selection:
            index = selection[0]
            mod_entry = self.mods[index]
            mod = mod_entry['data']
            description = mod.get('description', 'Нет описания')
            downloads = mod.get('downloads', 0)
            followers = mod.get('follows', 0)
            # Очищаем и заполняем поле с деталями
            self.mod_details_text.delete('1.0', tk.END)
            self.mod_details_text.insert(tk.END, f"Название: {mod['title']}\n")
            self.mod_details_text.insert(tk.END, f"Скачивания: {downloads}\n")
            self.mod_details_text.insert(tk.END, f"Подписчики: {followers}\n")
            self.mod_details_text.insert(tk.END, f"\n{description}")

    def check_compatibility(self):
        selected_indices = self.mods_list.curselection()
        if not selected_indices:
            messagebox.showwarning("Предупреждение", "Выберите хотя бы один мод для проверки.")
            return

        mc_version = self.config["mc_version"]
        modloader = self.config["mod_loader"].lower()

        incompatible_mods = []
        mod_map = {}

        # Получение информации о выбранных модах и их версиях
        for idx in selected_indices:
            mod_entry = self.mods[idx]
            mod = mod_entry["data"]
            mod_id = mod['project_id']
            mod_title = mod['title']
            version = get_latest_compatible_version_modrinth(mod_id, mc_version, modloader, self.config["user_agent"])
            if version:
                mod_map[mod_id] = {
                    'title': mod_title,
                    'version': version,
                    'dependencies': get_mod_dependencies(version),
                }
            else:
                incompatible_mods.append(mod_title)

        if incompatible_mods:
            messagebox.showerror("Несовместимые моды", f"Следующие моды не совместимы с версией {mc_version} и загрузчиком {modloader.capitalize()}:\n" + "\n".join(incompatible_mods))
            return

        # Проверка зависимостей и автоматическое добавление недостающих
        missing_dependencies = []
        processed_mods = set()
        mods_to_process = list(mod_map.keys())

        while mods_to_process:
            current_mods = mods_to_process
            mods_to_process = []
            for mod_id in current_mods:
                if mod_id in processed_mods:
                    continue
                mod_info = mod_map[mod_id]
                processed_mods.add(mod_id)
                for dep_id in mod_info['dependencies']:
                    if dep_id not in mod_map:
                        dep_project = get_mod_by_id(dep_id, self.config["user_agent"])
                        if dep_project:
                            dep_title = dep_project['title']
                            dep_version = get_latest_compatible_version_modrinth(dep_id, mc_version, modloader, self.config["user_agent"])
                            if dep_version:
                                # Добавляем зависимость в список модов
                                mod_map[dep_id] = {
                                    'title': dep_title,
                                    'version': dep_version,
                                    'dependencies': get_mod_dependencies(dep_version),
                                }
                                self.mods.append({"source": "Modrinth", "data": dep_project})
                                self.mods_list.insert(tk.END, f"{dep_title} - {dep_project['description']}")
                                # Отмечаем зависимость как выбранную
                                self.mods_list.select_set(tk.END)
                                # Добавляем в список для дальнейшей обработки зависимостей этой зависимости
                                mods_to_process.append(dep_id)
                            else:
                                missing_dependencies.append(dep_title)
                        else:
                            missing_dependencies.append(dep_id)

        if missing_dependencies:
            messagebox.showwarning("Отсутствующие зависимости", f"Следующие зависимости отсутствуют или несовместимы:\n" + "\n".join(set(missing_dependencies)))
            return

        # Сохраняем информацию о выбранных модах и их зависимостях для дальнейшего использования при скачивании
        self.selected_mods_info = mod_map

        messagebox.showinfo("Совместимость", "Выбранные моды совместимы между собой и с текущей версией Minecraft. Все необходимые зависимости будут скачаны.")

    # === Функции для вкладки ресурс-паков ===
    def search_resourcepacks(self):
        description = self.resourcepack_description_var.get()

        self.resourcepacks_list.delete(0, tk.END)
        self.resourcepacks = []

        resourcepacks = search_projects_modrinth(description, "resourcepack", user_agent=self.config["user_agent"])
        for rp in resourcepacks:
            self.resourcepacks.append({"source": "Modrinth", "data": rp})
            self.resourcepacks_list.insert(tk.END, f"{rp['title']} - {rp['description']}")

    # === Функции для вкладки шейдеров ===
    def search_shaders(self):
        description = self.shader_description_var.get()

        self.shaders_list.delete(0, tk.END)
        self.shaders = []

        shaders = search_projects_modrinth(description, "shader", user_agent=self.config["user_agent"])
        for shader in shaders:
            self.shaders.append({"source": "Modrinth", "data": shader})
            self.shaders_list.insert(tk.END, f"{shader['title']} - {shader['description']}")

    # === Функции для вкладки плагинов ===
    def search_plugins(self):
        query = self.plugin_description_var.get()

        self.plugins_list.delete(0, tk.END)
        self.plugins = []

        plugins = search_plugins_spiget(query, self.config["user_agent"])
        for plugin in plugins:
            self.plugins.append(plugin)
            self.plugins_list.insert(tk.END, f"{plugin['name']} - {plugin.get('tag', '')}")

    # === Функция скачивания сборки ===
    def download_build(self):
        build_type = self.build_type_var.get()

        if build_type in ["client", "both"]:
            self.download_client_build()

        if build_type in ["server", "both"]:
            self.download_server_build()

    def download_client_build(self):
        if not os.path.exists(self.config["download_folder"]):
            os.makedirs(self.config["download_folder"])

        self.progress_window = Toplevel(self.root)
        self.progress_window.title("Прогресс скачивания клиента")

        progress_bar = Progressbar(self.progress_window, orient="horizontal", length=300, mode="determinate")
        progress_bar.pack(pady=20)

        def download():
            # Скачивание модов
            mod_items = list(self.selected_mods_info.items())
            total_mods = len(mod_items)
            for idx, (mod_id, mod_info) in enumerate(mod_items):
                mod_title = mod_info['title']
                version = mod_info['version']
                files = [file for file in version['files'] if file['primary']]
                if files:
                    file_info = files[0]
                    filename = file_info['filename']
                    url = file_info['url']
                    dest_folder = os.path.join(self.config["download_folder"], "mods")
                    if not os.path.exists(dest_folder):
                        os.makedirs(dest_folder)
                    download_file(url, dest_folder, filename, progress_bar, self.config["user_agent"])
                else:
                    messagebox.showinfo("Информация", f"Нет файлов для мода {mod_title}.")

                progress = ((idx + 1) / total_mods) * 100
                progress_bar['value'] = progress
                progress_bar.update()

            # Скачивание ресурс-паков
            selected_indices = self.resourcepacks_list.curselection()
            for idx in selected_indices:
                rp_entry = self.resourcepacks[idx]
                rp = rp_entry["data"]
                version = get_latest_compatible_version_modrinth(rp['project_id'], user_agent=self.config["user_agent"])
                if version:
                    files = version['files']
                    if files:
                        file_info = files[0]
                        filename = file_info['filename']
                        url = file_info['url']
                        dest_folder = os.path.join(self.config["download_folder"], "resourcepacks")
                        if not os.path.exists(dest_folder):
                            os.makedirs(dest_folder)
                        download_file(url, dest_folder, filename, progress_bar, self.config["user_agent"])

            # Скачивание шейдеров
            selected_indices = self.shaders_list.curselection()
            for idx in selected_indices:
                shader_entry = self.shaders[idx]
                shader = shader_entry["data"]
                version = get_latest_compatible_version_modrinth(shader['project_id'], user_agent=self.config["user_agent"])
                if version:
                    files = version['files']
                    if files:
                        file_info = files[0]
                        filename = file_info['filename']
                        url = file_info['url']
                        dest_folder = os.path.join(self.config["download_folder"], "shaderpacks")
                        if not os.path.exists(dest_folder):
                            os.makedirs(dest_folder)
                        download_file(url, dest_folder, filename, progress_bar, self.config["user_agent"])

            messagebox.showinfo("Готово", "Клиентская сборка успешно скачана!")
            self.progress_window.destroy()

        Thread(target=download).start()

    def download_server_build(self):
        if not os.path.exists(self.config["download_folder"]):
            os.makedirs(self.config["download_folder"])

        self.progress_window = Toplevel(self.root)
        self.progress_window.title("Прогресс скачивания сервера")

        progress_bar = Progressbar(self.progress_window, orient="horizontal", length=300, mode="determinate")
        progress_bar.pack(pady=20)

        def download():
            # Скачивание серверных файлов
            server_folder = os.path.join(self.config["download_folder"], "server")
            if not os.path.exists(server_folder):
                os.makedirs(server_folder)

            mc_version = self.config["mc_version"]
            modloader = self.config["mod_loader"]
            user_agent = self.config["user_agent"]

            if modloader.lower() == "forge":
                # Скачиваем Forge Installer
                forge_version = self.get_forge_version(mc_version)
                if forge_version:
                    url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{mc_version}-{forge_version}/forge-{mc_version}-{forge_version}-installer.jar"
                    filename = f"forge-{mc_version}-{forge_version}-installer.jar"
                    download_file(url, server_folder, filename, progress_bar, user_agent)
                    # Запускаем установщик сервера
                    installer_path = os.path.join(server_folder, filename)
                    os.system(f'java -jar "{installer_path}" --installServer')
                    # Принимаем EULA
                    self.accept_eula(server_folder)
                else:
                    messagebox.showerror("Ошибка", "Не удалось найти версию Forge для данной версии Minecraft.")
                    return
            else:
                messagebox.showwarning("Внимание", f"Автоматическая установка сервера для {modloader} не поддерживается.")
                return

            # Скачивание модов
            mod_items = list(self.selected_mods_info.items())
            total_mods = len(mod_items)
            for idx, (mod_id, mod_info) in enumerate(mod_items):
                mod_title = mod_info['title']
                version = mod_info['version']
                files = [file for file in version['files'] if file['primary']]
                if files:
                    file_info = files[0]
                    filename = file_info['filename']
                    url = file_info['url']
                    dest_folder = os.path.join(server_folder, "mods")
                    if not os.path.exists(dest_folder):
                        os.makedirs(dest_folder)
                    download_file(url, dest_folder, filename, progress_bar, user_agent)
                else:
                    messagebox.showinfo("Информация", f"Нет файлов для мода {mod_title}.")

                progress = ((idx + 1) / total_mods) * 100
                progress_bar['value'] = progress
                progress_bar.update()

            # Скачивание плагинов
            selected_indices = self.plugins_list.curselection()
            for idx in selected_indices:
                plugin = self.plugins[idx]
                plugin_id = plugin['id']
                url = f'https://api.spiget.org/v2/resources/{plugin_id}/download'
                dest_folder = os.path.join(server_folder, "plugins")
                if not os.path.exists(dest_folder):
                    os.makedirs(dest_folder)
                filename = f"{plugin['name']}.jar"
                download_file(url, dest_folder, filename, progress_bar, user_agent)

            messagebox.showinfo("Готово", "Серверная сборка успешно скачана!")
            self.progress_window.destroy()

        Thread(target=download).start()

    def get_forge_version(self, mc_version):
        # Получение последней версии Forge для заданной версии Minecraft
        url = f"https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json"
        response = requests.get(url)
        if response.status_code == 200:
            data = response.json()
            promotions = data.get('promos', {})
            key = f"{mc_version}-latest"
            forge_version = promotions.get(key)
            return forge_version
        return None

    def accept_eula(self, server_folder):
        eula_path = os.path.join(server_folder, 'eula.txt')
        with open(eula_path, 'w') as file:
            file.write('eula=true\n')

    def save_user_config(self):
        user_config = {
            "selected_mods": [self.mods_list.get(i) for i in self.mods_list.curselection()],
            "mod_categories": {k: v.get() for k, v in self.mod_categories.items()},
            "exclude_categories": {k: v.get() for k, v in self.exclude_categories.items()},
            "additional_keywords": self.additional_keywords_var.get(),
            "mc_version": self.mc_version_var.get(),
            "mod_loader": self.modloader_var.get(),
        }
        with open('user_config.json', 'w', encoding='utf-8') as f:
            json.dump(user_config, f, ensure_ascii=False, indent=4)
        messagebox.showinfo("Сохранение", "Конфигурация сохранена.")

    def load_user_config(self):
        if not os.path.exists('user_config.json'):
            messagebox.showwarning("Загрузка", "Конфигурация не найдена.")
            return
        with open('user_config.json', 'r', encoding='utf-8') as f:
            user_config = json.load(f)
        # Восстанавливаем версию Minecraft и модлоадер
        self.mc_version_var.set(user_config.get("mc_version", ""))
        self.modloader_var.set(user_config.get("mod_loader", "Forge"))
        # Восстанавливаем выбранные категории
        for k, v in user_config.get("mod_categories", {}).items():
            if k in self.mod_categories:
                self.mod_categories[k].set(v)
        for k, v in user_config.get("exclude_categories", {}).items():
            if k in self.exclude_categories:
                self.exclude_categories[k].set(v)
        # Восстанавливаем ключевые слова
        self.additional_keywords_var.set(user_config.get("additional_keywords", ""))
        # Обновляем список модов
        self.search_mods()
        # Восстанавливаем выбранные моды
        selected_mods = user_config.get("selected_mods", [])
        for idx, mod_entry in enumerate(self.mods):
            mod_str = self.mods_list.get(idx)
            if mod_str in selected_mods:
                self.mods_list.select_set(idx)
        messagebox.showinfo("Загрузка", "Конфигурация загружена.")


# === Запуск приложения ===
if __name__ == '__main__':
    config = load_config()
    root = tk.Tk()
    app = ModPackBuilder(root, config)
    root.mainloop()

Модуль обучения
Обратная связь

Пользователь может оценить подбор модов и отметить, какие из них были удачными, а какие — нет.
Локальная база данных

Хранение информации о пользовательских предпочтениях и часто используемых модах.
Рекомендации на основе трендов

Предложение популярных модов, которые соответствуют описанию сборки.