import json
import pathlib
import tempfile

import gnupg

from libs.osaf.k_osa import kOSA, AppEnv
from libs.osaf.osaf_core import OSAF_Core
from libs.spider_logger.logger import SpiderLog
from libs.vx_update.installer import UpdateInstaller
from libs.vx_update.mmd import download_with_mirrors
from datetime import datetime, timezone, timedelta
import os
import hashlib
import json
from typing import List, Dict, Optional

def pj(*parts) -> str:
    """
    Умный путь: соединяет части, нормализует, возвращает строку.

    Примеры:
        pj("/game", "saves", "..", "mods")  → "/game/mods"
        pj("C:", "Users", "Sera")           → "C:\\Users\\Sera" (на винде)
    """
    if not parts:
        return ""

    # pathlib делает всё правильно: кроссплатформенно, нормализует .. и .
    path = pathlib.Path(parts[0])
    for part in parts[1:]:
        path = path / part

    return str(path.resolve()) if path.is_absolute() else str(path)

# --- ДОБАВИТЬ В НАЧАЛО (после импортов) ---
import argparse
import sys




ev = kOSA().appenv_kick_t1(
    AppEnv(pathlib.Path(__file__).parent.resolve()),
    app_id="prvoxels.updater.v3",
    app_displayname="PR_Voxels Updater App",
    app_binname=pathlib.Path(__file__).name
)
ev.APP_VERSION = "3.0-lts-dev"

def parse_cli_args():
    parser = argparse.ArgumentParser(
        description="PR_Voxels Updater v3 — безопасная доставка обновлений",
        epilog="Пример: %(prog)s --launcher-dir \"C:\\Games\\PR_Voxels\" --auto",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )

    # --- Контекст запуска (обязательные для продакшена) ---
    parser.add_argument(
        '--launcher-dir', type=str, required=True,
        help='корень лаунчера (где лежат backups/, downloads/)'
    )
    parser.add_argument(
        '--game-dir', type=str, required=True,
        help='папка игры (где лежит prvoxels.json и mods/, config/ и т.д.)'
    )
    parser.add_argument(
        '--game-marker', type=str, default='prvoxels.json',
        help='имя файла-маркера игры (по умолчанию: prvoxels.json)'
    )

    # --- Поведение ---
    parser.add_argument(
        '--auto', action='store_true',
        help='автоматическая установка без подтверждения'
    )
    parser.add_argument(
        '--dry-run', action='store_true',
        help='показать план изменений, НЕ устанавливая'
    )
    parser.add_argument(
        '--no-backup', action='store_true',
        help='пропустить бекап сохранений (ТОЛЬКО ДЛЯ ТЕСТОВ!)'
    )

    # --- Источники ---
    parser.add_argument(
        '--channel', choices=['stable', 'test', 'nightly'], default='test',
        help='канал обновлений (по умолчанию: test)'
    )
    parser.add_argument(
        '--index-url', type=str,
        default='https://repo.java86.ru/etc/pr-voxels-updater/updates_v3/updates.json',
        help='URL индекса обновлений'
    )
    parser.add_argument(
        '--mirror', action='append', dest='mirrors',
        help='зеркало для загрузки (можно указать несколько раз)'
    )

    # --- Отладка ---
    parser.add_argument(
        '--skip-verify', action='store_true',
        help='пропустить GPG/SHA256 верификацию (ТОЛЬКО ДЛЯ ОТЛАДКИ!)'
    )
    parser.add_argument(
        '--version', action='version',
        version=f'%(prog)s {ev.APP_VERSION}'
    )

    return parser.parse_args()

osa : OSAF_Core = ev.osa
log : SpiderLog = osa.log

# --- ИСПОЛЬЗОВАНИЕ: сразу после инициализации ev ---
args = parse_cli_args()




# ev.PR_APPS_DIR = "C:\\Users\\Sera\\Desktop\\JavaVoxels2.2_01092025a\\"
# ev.GAME_FOLDER = "C:\\Users\\Sera\\Desktop\\JavaVoxels2.2_01092025a\\app\\jv2.2\\game"

# ev.GAME_MARKER = "prvoxels.json"
# ev.GAME_UPDATE_CHANNEL = "test"


# ev.SOURCE_URLS = ["https://repo.java86.ru/etc/pr-voxels-updater/updates_v2", "http://mirror1.java86.ru/pr-voxels-updater/v2/"]


ev.PR_APPS_DIR = os.path.abspath(args.launcher_dir) or "C:\\Users\\Sera\\Desktop\\JavaVoxels2.2_01092025a\\"
ev.GAME_FOLDER = os.path.abspath(args.game_dir) or "C:\\Users\\Sera\\Desktop\\JavaVoxels2.2_01092025a\\app\\jv2.2\\game"
ev.GAME_MARKER = args.game_marker or "prvoxels.json"
ev.GAME_UPDATE_CHANNEL = args.channel or "test"
ev.MASTER_SOURCE_INDEX = args.index_url.rstrip()
ev.SOURCE_URLS = (
    [m.rstrip() for m in args.mirrors] if args.mirrors else
    [f"https://repo.java86.ru/etc/pr-voxels-updater/updates_v3/{ev.GAME_UPDATE_CHANNEL}/"]
)
ev.SKIP_VERIFICATION = args.skip_verify
ev.BACKUP_DIR = pj(ev.PR_APPS_DIR, "backups", ev.app_id)
ev.DL_DIR = "downloads"


ev.SOURCE_URLS = ["https://repo.java86.ru/etc/pr-voxels-updater/updates_v3/test/"]
ev.MASTER_SOURCE_INDEX = "https://repo.java86.ru/etc/pr-voxels-updater/updates_v3/updates.json"



# Валидация путей
if not os.path.isdir(ev.PR_APPS_DIR):
    log.e(f"✗ launcher-dir не существует: {ev.PR_APPS_DIR}")
    exit(101)

if not os.path.isdir(ev.GAME_FOLDER):
    log.e(f"✗ game-dir не существует: {ev.GAME_FOLDER}")
    exit(102)

marker_path = pj(ev.GAME_FOLDER, ev.GAME_MARKER)
if not os.path.isfile(marker_path):
    log.e(f"✗ Маркер игры не найден: {marker_path}")
    exit(103)

# Определяем текущую версию игры из маркера
try:
    ev.GAME_VERSION = json.loads(osa.files.read_str(marker_path)).get("app_version", "unknown")
except Exception as e:
    log.e(f"✗ Не удалось прочитать {ev.GAME_MARKER}: {e}")
    exit(104)

log.i(f"Канал обновлений: {ev.GAME_UPDATE_CHANNEL}")
if ev.SKIP_VERIFICATION:
    log.w("⚠️  ВЕРИФИКАЦИЯ ОТКЛЮЧЕНА — только для отладки!")

log.i(f"Лаунчер: {ev.PR_APPS_DIR}")
log.i(f"Игра:    {ev.GAME_FOLDER}")
log.i(f"Версия:  {ev.GAME_VERSION}")
log.i(f"Канал:   {ev.GAME_UPDATE_CHANNEL}")

ev.BUNDLED_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEaX13bxYJKwYBBAHaRw8BAQdAjuQg4Sj4Fs6MfPTRv+BDbW7fnv5s3vfhgejL
ffGFQ7O0HeOCu+ODqSA8c2VyZ2V5a3prb0BnbWFpbC5jb20+iLUEExYKAF0WIQRj
l0cl4C5PEZwN+vIrnwqA9OiONwUCaX13bxsUgAAAAAAEAA5tYW51MiwyLjUrMS4x
MSwyLDECGwMFCQWk1rEFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQK58K
gPTojjevEAEAy/g8Eb3A4wr1HBwosMwhO5ZScIzjNq1Hbq6DleuhXTUA+wS/aqrH
83D4qK0ro9qIY2rDM8Z+QymcfWCYAtXfHOYBuDgEaX13bxIKKwYBBAGXVQEFAQEH
QAGqp39k+5ih6W8dVYilC8OKnuG5iyM6EnbeS+rIRC8XAwEIB4iaBBgWCgBCFiEE
Y5dHJeAuTxGcDfryK58KgPTojjcFAml9d28bFIAAAAAABAAObWFudTIsMi41KzEu
MTEsMiwxAhsMBQkFpNaxAAoJECufCoD06I43LCEA/3TJN/qeSzsI458KFm3C1qT3
10kH2hCAHqv5RQTOVnoFAP48y6Dhchlna83FoLH2bf0uALaT2Ukv3kEpszeLEIaJ
AQ==
=Ftjm
-----END PGP PUBLIC KEY BLOCK-----
"""

# get index
# todo: add index verification
_, __s = osa.web.getText(ev.MASTER_SOURCE_INDEX)
index = json.loads(__s).get(ev.GAME_UPDATE_CHANNEL)
print(index)


##########



def expand_update_files(versions: list[str]) -> list[str]:
    """
    Раскрыть список версий в полный список файлов обновлений.

    Пример:
        expand_update_files(['v2-20250901', 'v2-20250911'])
        ['v2-20250901.zip', 'v2-20250901.zip.asc', 'v2-20250901.zip.sha256',
         'v2-20250911.zip', 'v2-20250911.zip.asc', 'v2-20250911.zip.sha256']
    """
    artifacts = ['.zip', '.zip.asc', '.zip.sha256']
    return [f"{ver}{artifact}" for ver in versions for artifact in artifacts]

def get_pending_updates(available_versions: list[str], current_version: str) -> list[str]:
    """
    Определить список обновлений, которые нужно установить.

    Порядок в available_versions считается хронологическим.
    Возвращает все версии, идущие ПОСЛЕ текущей в этом порядке.

    :param available_versions: Список доступных версий в хронологическом порядке
    :param current_version: Текущая установленная версия
    :return: Список версий для установки (в порядке применения)

    Примеры:
        get_pending_updates(['v2-20250901', 'v2-20250911', 'v2-20250913'], 'v2-20250911')
        ['v2-20250913']

        get_pending_updates(['v2-20250901', 'v2-20250911'], 'v2-20250913')
        [] # текущая версия новее всех доступных

        get_pending_updates(['v2-20250901', 'v2-20250911'], 'unknown')
        ['v2-20250901', 'v2-20250911'] # версия не найдена → все обновления
    """
    try:
        idx = available_versions.index(current_version)
        return available_versions[idx + 1:]
    except ValueError:
        # Текущая версия не найдена в списке:
        # - пользователь на кастомной/устаревшей сборке
        # - или только что установил игру (нет версии)
        # Возвращаем ВСЕ доступные обновления — безопаснее для пользователя
        return available_versions.copy()

def backup_saves(game_dir: str, backup_root: str, updater_app_version: str = None, game_version: str = None, channel: str = None) -> dict:
    """
    Создать бекап папки saves с метаинформацией.

    :param game_dir: Путь к корню игры (где лежит папка saves/)
    :param backup_root: Корневая директория для бекапов
    :param game_version: Текущая версия игры (опционально, для метаданных)
    :param channel: Канал обновлений (stable/test)
    :return: {
        'success': bool,
        'backup_path': str | None,
        'meta_path': str | None,      # путь к backup_meta.json
        'source_path': str,
        'file_count': int,
        'size_bytes': int,
        'backup_id': str,             # saves_backup_20260201_143022
        'error': str | None
    }
    """
    result = {
        'success': False,
        'backup_path': None,
        'meta_path': None,
        'source_path': pj(game_dir, "saves"),
        'file_count': 0,
        'size_bytes': 0,
        'backup_id': None,
        'error': None
    }

    # Проверяем существование saves/
    if not os.path.exists(result['source_path']):
        result['error'] = f"saves не найдены: {result['source_path']}"
        return result

    if not os.path.isdir(result['source_path']):
        result['error'] = f"saves не является директорией: {result['source_path']}"
        return result

    # Генерируем уникальный ID бекапа
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    backup_id = f"saves_backup_{timestamp}"
    backup_path = pj(backup_root, backup_id)
    meta_path = pj(backup_path, "backup_meta.json")

    try:
        # Создаём директорию бекапа
        os.makedirs(backup_path, exist_ok=True)

        # Копируем saves/
        if hasattr(ev.osa.files, 'copytree'):
            ev.osa.files.copytree(result['source_path'], pj(backup_path, "saves"))
        else:
            import shutil
            shutil.copytree(result['source_path'], pj(backup_path, "saves"))

        # Собираем статистику
        saves_copy_path = pj(backup_path, "saves")
        for dirpath, _, filenames in os.walk(saves_copy_path):
            for filename in filenames:
                filepath = pj(dirpath, filename)
                if os.path.exists(filepath):
                    result['size_bytes'] += os.path.getsize(filepath)
                    result['file_count'] += 1

        # Формируем метаинформацию
        meta = {
            "backup_id": backup_id,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "source_game_dir": os.path.abspath(game_dir),
            "source_saves_path": os.path.abspath(result['source_path']),
            "game_version": game_version or "unknown",
            "update_channel": channel or "unknown",
            "file_count": result['file_count'],
            "total_size_bytes": result['size_bytes'],
            "backup_tool": ev.app_id,
            "backup_tool_version": updater_app_version or "unknown"
        }

        # Сохраняем мета в JSON
        with open(meta_path, 'w', encoding='utf-8') as f:
            json.dump(meta, f, indent=2, ensure_ascii=False)

        # Обновляем результат
        result['success'] = True
        result['backup_path'] = backup_path
        result['meta_path'] = meta_path
        result['backup_id'] = backup_id

        # Логируем
        size_mb = result['size_bytes'] / (1024 * 1024)
        ev.log.i(f"✓ Бекап сохранений: {backup_id} "
                 f"({result['file_count']} файлов, {size_mb:.1f} МБ)")
        ev.log.d(f"  Мета: {meta_path}")

        return result

    except Exception as e:
        result['error'] = str(e)
        ev.log.e(f"✗ Ошибка бекапа saves: {e}")
        # Пытаемся убрать мусор от частично созданного бекапа
        try:
            if os.path.exists(backup_path):
                ev.osa.files.rmtree(backup_path)
        except:
            pass
        return result

def cleanup_old_backups(backup_root: str, keep_last: int = 5, max_age_days: int = None) -> dict:
    """
    Очистить старые бекапы сохранений.

    Сохраняет:
      - Последние `keep_last` бекапов (по времени создания)
      - Все бекапы младше `max_age_days` дней (если указано)

    :param backup_root: Корневая директория с бекапами
    :param keep_last: Сколько последних бекапов сохранить (по умолчанию 5)
    :param max_age_days: Максимальный возраст бекапа в днях (опционально)
    :return: {
        'deleted': int,           # число удалённых бекапов
        'kept': int,              # число сохранённых бекапов
        'freed_bytes': int,       # освобождено места
        'deleted_list': list[str] # список удалённых backup_id
    }
    """
    result = {
        'deleted': 0,
        'kept': 0,
        'freed_bytes': 0,
        'deleted_list': []
    }

    if not os.path.exists(backup_root):
        ev.log.w(f"Директория бекапов не существует: {backup_root}")
        return result

    # Собираем все бекапы с метаданными
    backups = []
    for entry in os.listdir(backup_root):
        backup_dir = pj(backup_root, entry)
        meta_file = pj(backup_dir, "backup_meta.json")

        if os.path.isdir(backup_dir) and os.path.exists(meta_file):
            try:
                with open(meta_file, 'r', encoding='utf-8') as f:
                    meta = json.load(f)

                # Парсим время создания
                created_at = datetime.fromisoformat(meta['created_at'].replace('Z', '+00:00'))
                backups.append({
                    'id': meta.get('backup_id', entry),
                    'path': backup_dir,
                    'meta_path': meta_file,
                    'created_at': created_at,
                    'size_bytes': meta.get('total_size_bytes', 0),
                    'game_version': meta.get('game_version', 'unknown')
                })
            except Exception as e:
                ev.log.w(f"Пропускаю повреждённый бекап {entry}: {e}")
                continue

    if not backups:
        ev.log.d("Нет бекапов для очистки")
        return result

    # Сортируем по времени создания (новые → старые)
    backups.sort(key=lambda b: b['created_at'], reverse=True)

    # Определяем, какие бекапы удалять
    now = datetime.now(timezone.utc)
    to_keep = set()
    to_delete = []

    # Правило 1: сохраняем последние N
    for backup in backups[:keep_last]:
        to_keep.add(backup['path'])

    # Правило 2: сохраняем все младше max_age_days
    if max_age_days is not None:
        cutoff = now - timedelta(days=max_age_days)
        for backup in backups:
            if backup['created_at'].replace(tzinfo=timezone.utc) >= cutoff:
                to_keep.add(backup['path'])

    # Остальное — на удаление
    for backup in backups:
        if backup['path'] not in to_keep:
            to_delete.append(backup)

    # Удаляем
    for backup in to_delete:
        try:
            size_before = backup['size_bytes']
            ev.osa.files.remove(backup['path'])

            result['deleted'] += 1
            result['freed_bytes'] += size_before
            result['deleted_list'].append(backup['id'])

            ev.log.i(f"🗑️ Удалён бекап {backup['id']} "
                     f"(версия: {backup['game_version']}, "
                     f"{size_before // (1024 * 1024)} МБ)")
        except Exception as e:
            ev.log.w(f"Не удалось удалить {backup['id']}: {e}")

    result['kept'] = len(backups) - result['deleted']

    # Итоги
    if result['deleted'] > 0:
        freed_mb = result['freed_bytes'] / (1024 * 1024)
        ev.log.i(f"Очистка завершена: удалено {result['deleted']}, "
                 f"оставлено {result['kept']}, освобождено {freed_mb:.1f} МБ")
    else:
        ev.log.d(f"Очистка: все {len(backups)} бекапов сохранены")

    return result

def verify_updates_batch(
        public_key: str,
        downloads_dir: str,
        versions: List[str],
        gpg_binary: Optional[str] = None
) -> Dict:
    """
    Верифицировать пакет обновлений (SHA256 + GPG).

      :param public_key: Публичный ключ в формате ASCII-armored (строка)
    :param downloads_dir: Директория с загруженными файлами (.zip, .zip.asc, .zip.sha256)
    :param versions: Список версий для проверки, например ['v2-20260109', 'v2-20260115']
    :param gpg_binary: Путь к gpg бинарнику (опционально)
    :return: {
        'success': bool,
        'verified': List[str],
        'failed': List[Dict],
        'details': Dict[version, Dict]
    }
    """
    result = {
        'success': False,
        'verified': [],
        'failed': [],
        'details': {}
    }

    # Создаём изолированный GPG home
    with tempfile.TemporaryDirectory(prefix="gpg_verify_") as gpg_home:
        try:
            gpg_kwargs = {'gnupghome': gpg_home}
            if gpg_binary:
                gpg_kwargs['gpgbinary'] = gpg_binary

            gpg = gnupg.GPG(**gpg_kwargs)

            # Импортируем доверенный ключ
            import_result = gpg.import_keys(public_key)
            if not import_result.fingerprints:
                raise RuntimeError(f"Не удалось импортировать публичный ключ")

            trusted_fp = import_result.fingerprints[0].replace(' ', '').upper()
            ev.log.i(f"✓ Ключ верификации: {trusted_fp[:16]}...")

            # Проверяем каждую версию
            for version in versions:
                ver_result = _verify_single_update(
                    gpg=gpg,
                    trusted_fp=trusted_fp,
                    downloads_dir=downloads_dir,
                    version=version
                )

                result['details'][version] = ver_result

                if ver_result['valid']:
                    result['verified'].append(version)
                    ev.log.i(f"✓✓ {version}")
                else:
                    result['failed'].append({
                        'version': version,
                        'reason': ver_result['error_type'],
                        'error': ver_result['error']
                    })
                    ev.log.e(f"✗✗ {version}: {ver_result['error']}")
                    _cleanup_version_files(downloads_dir, version)

            result['success'] = len(result['failed']) == 0

            if result['success']:
                ev.log.i(f"✓✓✓ Все обновления верифицированы: {len(versions)}")
            else:
                ev.log.e(f"✗✗✗ Провалено: {len(result['failed'])}/{len(versions)}")

            return result

        except Exception as e:
            ev.log.e(f"Критическая ошибка верификации: {e}")
            raise

def _verify_single_update(
        gpg: gnupg.GPG,
        trusted_fp: str,
        downloads_dir: str,
        version: str
) -> Dict:
    """Верифицировать одну версию обновления с упрощённым SHA256"""
    archive = f"{version}.zip"
    sig = f"{version}.zip.asc"
    sha256_file = f"{version}.zip.sha256"

    archive_path = pj(downloads_dir, archive)
    sig_path = pj(downloads_dir, sig)
    sha256_path = pj(downloads_dir, sha256_file)

    result = {
        'valid': False,
        'error': None,
        'error_type': None,
        'sha256_ok': False,
        'signature_ok': False,
        'key_fingerprint': None,
        'timestamp': None
    }

    # === Шаг 1: Проверка существования файлов ===
    missing = []
    for name, path in [('архив', archive_path), ('подпись', sig_path), ('хеш', sha256_path)]:
        if not os.path.exists(path):
            missing.append(name)

    if missing:
        result['error'] = f"Нет файлов: {', '.join(missing)}"
        result['error_type'] = 'missing_files'
        return result

    # === Шаг 2: Проверка SHA256 (упрощённый формат) ===
    try:
        # Читаем ТОЛЬКО хеш из файла — первая непустая строка
        with open(sha256_path, 'r', encoding='utf-8') as f:
            lines = [line.strip() for line in f if line.strip()]
            if not lines:
                raise ValueError("Файл хеша пустой")

            expected_hash = lines[0].lower()

            # Валидация формата хеша (64 шестнадцатеричных символа)
            if len(expected_hash) != 64 or not all(c in '0123456789abcdef' for c in expected_hash):
                raise ValueError(f"Неверный формат хеша: {expected_hash[:16]}...")

        # Вычисляем реальный хеш архива
        hasher = hashlib.sha256()
        with open(archive_path, 'rb') as f:
            for chunk in iter(lambda: f.read(131072), b''):  # 128 КБ чанки
                hasher.update(chunk)
        actual_hash = hasher.hexdigest()

        # Сравнение с защитой от тайминг-атак
        if not _hashes_equal(actual_hash, expected_hash):
            result['error'] = f"SHA256 не совпадает"
            result['error_type'] = 'sha256_mismatch'
            ev.log.d(f"  Ожидалось: {expected_hash}")
            ev.log.d(f"  Получено:  {actual_hash}")
            return result

        result['sha256_ok'] = True
        ev.log.d(f"  SHA256 ✓ {archive}")

    except Exception as e:
        result['error'] = f"Ошибка SHA256: {e}"
        result['error_type'] = 'sha256_error'
        return result


    # === Шаг 3: Проверка GPG-подписи ===
    try:
        data_file_path = archive_path
        signature_file_path = sig_path

        if not os.path.exists(data_file_path):
            print(f"Error: Data file not found at {data_file_path}")
            exit()
        if not os.path.exists(signature_file_path):
            print(f"Error: Signature file not found at {signature_file_path}")
            exit()
        with open(signature_file_path, 'rb') as sig_file:
            # Verify the signature against the original data file
            # The first argument is the signature file handle/path,
            # and the second argument is the original signed data file path.
            verified = gpg.verify_file(sig_file, data_file_path)

        if not verified.valid:
            result['error'] = f"Подпись недействительна: {verified.status or 'unknown'}"
            result['error_type'] = 'invalid_signature'
            return result

        actual_fp = verified.fingerprint.replace(' ', '').upper() if verified.fingerprint else None
        if actual_fp != trusted_fp:
            result['error'] = f"Подпись чужим ключом"
            result['error_type'] = 'wrong_key'
            ev.log.d(f"  Ожидался: {trusted_fp}")
            ev.log.d(f"  Получен:  {actual_fp}")
            return result

        result['signature_ok'] = True
        result['key_fingerprint'] = actual_fp
        result['timestamp'] = (
            datetime.fromtimestamp(int(verified.timestamp), tz=timezone.utc)
            if verified.timestamp else None
        )
        ev.log.d(f"  GPG ✓ {archive}")

    except Exception as e:
        result['error'] = f"GPG ошибка: {e}"
        result['error_type'] = 'gpg_error'
        return result

    # === Всё прошло ===
    result['valid'] = True
    return result

def _hashes_equal(a: str, b: str) -> bool:
    """Безопасное сравнение хешей (защита от тайминг-атак)"""
    if len(a) != len(b):
        return False
    result = 0
    for x, y in zip(a.lower(), b.lower()):
        result |= ord(x) ^ ord(y)
    return result == 0

def _cleanup_version_files(downloads_dir: str, version: str):
    """Удалить все файлы версии после провала верификации"""

    for ext in ['.zip', '.zip.asc', '.zip.sha256']:
        path = pj(downloads_dir, f"{version}{ext}")
        if os.path.exists(path):
            try:
                os.remove(path)
                ev.log.d(f"  Удалён: {os.path.basename(path)}")
            except Exception as e:
                ev.log.w(f"  Не удалён {path}: {e}")

def get_game_version():
    return json.loads(osa.files.read_str(pj(ev.GAME_FOLDER, ev.GAME_MARKER))).get("app_version")

##########




ev.GAME_VERSION = get_game_version()
log.i(f'Current game version: {ev.GAME_VERSION}')


updates = get_pending_updates(index, ev.GAME_VERSION)
if len(updates) > 0:
    files_for_download = expand_update_files(updates)
    osa.files.remove(ev.DL_DIR)
    report = download_with_mirrors(
        osa=ev.osa,
        mirrors=ev.SOURCE_URLS,
        files=files_for_download,
        dest_dir=ev.DL_DIR
    )
    if report['total_failed'] > 0:
        log.e(f"Не удалось загрузить: {report['failed']}")
        print({"error": "download_failed", "details": report['failed']})
        exit(2)

    # files verification

    verify_result = verify_updates_batch(
        public_key=ev.BUNDLED_PUBLIC_KEY,
        downloads_dir=ev.DL_DIR,
        versions=updates,
        gpg_binary="bin\\win64\\GnuPG\\bin\\gpg.exe",
    )

    if not verify_result['success']:
        log.e("ОТМЕНА: обновления не прошли верификацию!")
        for fail in verify_result['failed']:
            log.e(f"  {fail['version']}: {fail['reason']} — {fail['error']}")
        print("Верификация обновлений провалена")
        exit(666)

    log.i("✓✓✓ Все обновления проверены и готовы к установке")
    log.d(cleanup_old_backups(ev.BACKUP_DIR, keep_last=4))
    log.d(backup_saves(ev.GAME_FOLDER, ev.BACKUP_DIR, ev.APP_VERSION, ev.GAME_VERSION, ev.GAME_UPDATE_CHANNEL))
    log.i("Приступаем к установке обновлений")

    installer = UpdateInstaller(
        downloads_dir=ev.DL_DIR,
        game_dir=ev.GAME_FOLDER,
        launcher_dir=ev.PR_APPS_DIR,
        log=log,
        osa=osa
    )

    for upd_code in updates:
        success = installer.install(upd_code)



