import os
import shutil
import tempfile
import time
import libarchive
import requests
import orjson
from PySide6.QtWidgets import (QDialog, QTabWidget, QTableWidget,
                               QTableWidgetItem, QVBoxLayout, QWidget, QCheckBox,
                               QPushButton, QHeaderView, QMessageBox,
                               QLabel, QTextEdit, QHBoxLayout, QProgressBar,
                               QFrame, QSizePolicy, QAbstractItemView)
from PySide6.QtCore import Qt, QThread, Signal, QMutex, QWaitCondition, QTimer
import urllib.parse
from portprotonqt.config_utils import read_proxy_config, get_portproton_start_command
from portprotonqt.logger import get_logger
from portprotonqt.localization import _
from portprotonqt.version_utils import version_sort_key

logger = get_logger(__name__)


def get_cpu_level():
    """
    Determine CPU level based on feature flags
    Returns:
        int: CPU level (0-4) based on supported instruction sets
    """
    try:
        with open('/proc/cpuinfo') as f:
            # Read line by line to find flags without loading entire file
            for line in f:
                if line.startswith('flags'):
                    # Extract the actual flags
                    flags = set(line.split(':')[1].strip().split())
                    break
            else:
                # If no flags line found
                logger.warning("Could not find flags in /proc/cpuinfo, defaulting to CPU level 4")
                return 4
    except FileNotFoundError:
        logger.warning("Could not read /proc/cpuinfo, defaulting to CPU level 4")
        return 4  # Default to highest level if we can't read cpuinfo

    # Check for required flags for each level using a more efficient approach
    # Pre-define the flag requirements for each level
    level1_required = {'lm', 'cmov', 'cx8', 'fpu', 'fxsr', 'mmx', 'syscall', 'sse2'}
    if not level1_required.issubset(flags):
        return 0

    level2_required = {'cx16', 'lahf_lm', 'popcnt', 'sse4_1', 'sse4_2', 'ssse3'}
    if not level2_required.issubset(flags):
        return 1

    level3_required = {'avx', 'avx2', 'bmi1', 'bmi2', 'f16c', 'fma', 'abm', 'movbe', 'xsave'}
    if not level3_required.issubset(flags):
        return 2

    level4_required = {'avx512f', 'avx512bw', 'avx512cd', 'avx512dq', 'avx512vl'}
    if level4_required.issubset(flags):
        return 4

    return 3

class DownloadThread(QThread):
    progress = Signal(int)
    finished = Signal(str, bool)
    error = Signal(str)

    def __init__(self, download_url, filename):
        super().__init__()
        self.download_url = download_url
        self.filename = filename
        self._is_running = True
        self._mutex = QMutex()
        self._condition = QWaitCondition()

    def run(self):
        try:
            # Create a session with proxy support
            session = requests.Session()
            proxy = read_proxy_config() or {}
            if proxy:
                session.proxies.update(proxy)
            session.verify = True

            response = session.get(self.download_url, stream=True)
            response.raise_for_status()

            total_size = int(response.headers.get('content-length', 0))
            downloaded_size = 0

            with open(self.filename, 'wb') as file:
                for chunk in response.iter_content(chunk_size=8192):
                    self._mutex.lock()
                    if not self._is_running:
                        self._mutex.unlock()
                        # Если загрузка отменена, удаляем частично скачанный файл
                        if os.path.exists(self.filename):
                            os.remove(self.filename)
                        return
                    self._mutex.unlock()

                    if chunk:
                        file.write(chunk)
                        downloaded_size += len(chunk)
                        if total_size > 0:
                            progress = int((downloaded_size / total_size) * 100)
                            self.progress.emit(progress)

            self._mutex.lock()
            if self._is_running:
                self._mutex.unlock()
                self.finished.emit(self.filename, True)
            else:
                self._mutex.unlock()
                # Если загрузка отменена в последний момент, удаляем файл
                if os.path.exists(self.filename):
                    os.remove(self.filename)

        except Exception as e:
            self._mutex.lock()
            if self._is_running:
                self._mutex.unlock()
                # Удаляем частично скачанный файл при ошибке
                if os.path.exists(self.filename):
                    try:
                        os.remove(self.filename)
                    except OSError:
                        pass
                self.error.emit(str(e))
            else:
                self._mutex.unlock()

    def stop(self):
        """Безопасная остановка потока"""
        self._mutex.lock()
        self._is_running = False
        self._mutex.unlock()

        if self.isRunning():
            self.quit()
            if not self.wait(1000):  # Ждем до 1 секунды
                logger.warning("Thread did not stop gracefully, but continuing...")


class ExtractionThread(QThread):
    progress = Signal(int)          # %
    speed = Signal(float)           # MB/s
    eta = Signal(int)               # seconds
    finished = Signal(str, bool)    # archive_path, success
    error = Signal(str)

    def __init__(self, archive_path: str, extract_dir: str):
        super().__init__()
        self.archive_path = archive_path
        self.extract_dir = extract_dir

        self._is_running = True
        self._mutex = QMutex()

    # ========================
    # Internal helpers
    # ========================

    def _should_stop(self) -> bool:
        self._mutex.lock()
        running = self._is_running
        self._mutex.unlock()
        return not running

    # ========================
    # Main logic
    # ========================

    def run(self):
        try:
            self.progress.emit(0)
            self.speed.emit(0.0)
            self.eta.emit(0)

            os.makedirs(self.extract_dir, exist_ok=True)

            archive_size = os.path.getsize(self.archive_path)
            if archive_size <= 0:
                archive_size = 1

            start_time = time.monotonic()
            last_emit_time = start_time
            last_progress = -1
            last_bytes_read = 0

            # Меняем рабочую директорию для корректной распаковки
            original_dir = os.getcwd()
            old_umask = os.umask(0)  # Сохраняем и сбрасываем umask
            os.chdir(self.extract_dir)

            try:
                # Список для отложенной установки времени модификации
                deferred_times = []

                with libarchive.file_reader(self.archive_path) as archive:
                    for entry in archive:
                        if self._should_stop():
                            return

                        entry_path = entry.pathname

                        # Создаём директории
                        if entry.isdir:
                            os.makedirs(entry_path, exist_ok=True)

                            # Права для директорий
                            if entry.mode:
                                try:
                                    os.chmod(entry_path, entry.mode)
                                except (OSError, PermissionError):
                                    pass

                            # Откладываем установку времени для директорий
                            if entry.mtime:
                                deferred_times.append((entry_path, entry.mtime))

                        # Извлекаем файлы
                        elif entry.isfile:
                            parent_dir = os.path.dirname(entry_path)
                            if parent_dir:
                                os.makedirs(parent_dir, exist_ok=True)

                            # Записываем содержимое файла
                            with open(entry_path, 'wb') as f:
                                for block in entry.get_blocks():
                                    if self._should_stop():
                                        return
                                    f.write(block)

                            # Устанавливаем права (включая execute bit)
                            if entry.mode:
                                try:
                                    os.chmod(entry_path, entry.mode)
                                except (OSError, PermissionError):
                                    pass

                            # Устанавливаем время модификации
                            if entry.mtime:
                                try:
                                    os.utime(entry_path, (entry.mtime, entry.mtime))
                                except (OSError, PermissionError):
                                    pass

                        # Символические ссылки
                        elif entry.issym:
                            parent_dir = os.path.dirname(entry_path)
                            if parent_dir:
                                os.makedirs(parent_dir, exist_ok=True)

                            if os.path.lexists(entry_path):
                                os.remove(entry_path)

                            try:
                                os.symlink(entry.linkpath, entry_path)
                            except (OSError, NotImplementedError):
                                pass

                        # Обновляем прогресс
                        bytes_read = archive.bytes_read
                        now = time.monotonic()
                        elapsed = now - start_time

                        if bytes_read != last_bytes_read:
                            last_bytes_read = bytes_read

                            if now - last_emit_time >= 0.1 or elapsed < 0.1:
                                progress = int((bytes_read / archive_size) * 100)
                                if progress != last_progress:
                                    self.progress.emit(min(progress, 99))
                                    last_progress = progress

                                if elapsed > 0:
                                    speed = (bytes_read / (1024 * 1024)) / elapsed
                                    self.speed.emit(round(speed, 2))

                                    if speed > 0:
                                        remaining_mb = (archive_size - bytes_read) / (1024 * 1024)
                                        self.eta.emit(max(0, int(remaining_mb / speed)))
                                    else:
                                        self.eta.emit(0)
                                else:
                                    self.speed.emit(0.0)
                                    self.eta.emit(0)

                                last_emit_time = now

                # Устанавливаем время модификации для директорий в обратном порядке
                # (чтобы родительские директории обновлялись последними)
                for dir_path, mtime in reversed(deferred_times):
                    try:
                        os.utime(dir_path, (mtime, mtime))
                    except (OSError, PermissionError):
                        pass

                self.progress.emit(100)
                self.speed.emit(0.0)
                self.eta.emit(0)
                self.finished.emit(self.archive_path, True)

            finally:
                os.chdir(original_dir)
                os.umask(old_umask)  # Восстанавливаем umask

        except Exception as e:
            if not self._should_stop():
                self.error.emit(str(e))

    # ========================
    # Thread control
    # ========================

    def stop(self):
        self._mutex.lock()
        self._is_running = False
        self._mutex.unlock()

        if self.isRunning():
            self.quit()
            self.wait(1000)



class ProtonManager(QDialog):
    def __init__(self, parent=None, portproton_location=None):
        super().__init__(parent)
        self.selected_assets = {}  # {unique_id: asset_data}
        self.current_extraction_thread = None
        self.current_download_thread = None  # Still keep this for compatibility with Downloader's thread
        self.is_downloading = False  # Actually extraction in progress now
        self.assets_to_download = []
        self.current_download_index = 0
        self.portproton_location = portproton_location
        self.initUI()
        self.load_proton_data_from_json()

    def initUI(self):
        self.setWindowTitle(_('Get other Wine'))
        self.resize(800, 600)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(5, 5, 5, 5)
        layout.setSpacing(5)

        # Tab widget - основной растягивающийся элемент
        self.tab_widget = QTabWidget()
        self.tab_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        layout.addWidget(self.tab_widget, 1)

        # Инфо-блок для показа выбранного (компактный для информации по выбранным закачкам)
        selection_widget = QWidget()
        selection_layout = QVBoxLayout(selection_widget)
        selection_layout.setContentsMargins(0, 2, 0, 2)
        selection_layout.setSpacing(2)

        selection_label = QLabel(_("Selected assets:"))
        selection_label.setMaximumHeight(20)
        selection_layout.addWidget(selection_label)

        self.selection_text = QTextEdit()
        self.selection_text.setMaximumHeight(80)
        self.selection_text.setReadOnly(True)
        self.selection_text.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
        self.selection_text.setPlainText(_("No assets selected"))
        selection_layout.addWidget(self.selection_text)

        layout.addWidget(selection_widget)

        # Область прогресса загрузки
        self.download_frame = QFrame()
        self.download_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        self.download_frame.setVisible(False)
        self.download_frame.setMaximumHeight(80)

        download_layout = QVBoxLayout(self.download_frame)
        download_layout.setContentsMargins(10, 5, 10, 5)
        download_layout.setSpacing(5)

        self.download_info_label = QLabel(_("Downloading: "))
        download_layout.addWidget(self.download_info_label)

        progress_layout = QHBoxLayout()
        self.download_progress = QProgressBar()
        self.download_progress.setMinimum(0)
        self.download_progress.setMaximum(100)
        self.cancel_btn = QPushButton(_('Cancel'))
        self.cancel_btn.clicked.connect(self.cancel_current_download)
        progress_layout.addWidget(self.download_progress, 4)
        progress_layout.addWidget(self.cancel_btn, 1)
        download_layout.addLayout(progress_layout)

        layout.addWidget(self.download_frame)

        # Кнопки управления
        button_layout = QHBoxLayout()
        self.download_btn = QPushButton(_('Download Selected'))
        self.download_btn.clicked.connect(self.download_selected)
        self.download_btn.setEnabled(False)
        self.download_btn.setMinimumHeight(40)
        self.clear_btn = QPushButton(_('Clear All'))
        self.clear_btn.clicked.connect(self.clear_selection)
        self.clear_btn.setMinimumHeight(40)
        button_layout.addWidget(self.download_btn)
        button_layout.addWidget(self.clear_btn)
        layout.addLayout(button_layout)

    def load_proton_data_from_json(self):
        """Загружаем данные по Протонам из файла JSON"""
        json_url = "https://git.linux-gaming.ru/Boria138/PortProton-Wine-Metadata/raw/branch/main/wine_metadata.json"

        try:
            logger.debug(f"Loading JSON metadata from: {json_url}")
            # Create a session with proxy support
            session = requests.Session()
            proxy = read_proxy_config() or {}
            if proxy:
                session.proxies.update(proxy)
            session.verify = True
            response = session.get(json_url, timeout=30)
            response.raise_for_status()

            metadata = orjson.loads(response.content)
            logger.info(f"Successfully loaded JSON metadata with {len(metadata)} entries")
            self.process_metadata(metadata)


        except requests.exceptions.RequestException as e:
            logger.error(f"Network error loading JSON: {e}")
        except orjson.JSONDecodeError as e:
            logger.error(f"JSON parsing error: {e}")
        except Exception as e:
            logger.error(f"Error loading metadata: {e}")

    def process_metadata(self, metadata):
        """Обработка JSON, создание Табов"""
        successful_tabs = 0

        # Get CPU level to filter incompatible versions
        self.cpu_level = get_cpu_level()
        logger.info(f"Detected CPU level: {self.cpu_level}")

        # Собираем табы в словарь для сортировки
        tabs_dict = {}

        for source_key, entries in metadata.items():
            # Пропускаем таб "gdk_proton" (вроде ненужный протон, скипаем)
            if source_key.lower() == 'gdk_proton':
                logger.debug(f"Skipping tab: {source_key}")
                continue

            # Filter entries based on CPU compatibility
            filtered_entries = self.filter_entries_by_cpu_level(entries, source_key)
            tabs_dict[source_key] = filtered_entries

        # Proton_LG в самое начало кидаем
        if 'proton_lg' in tabs_dict:
            if self.create_tab_from_entries('proton_lg', tabs_dict['proton_lg']):
                successful_tabs += 1
            del tabs_dict['proton_lg']

        # Остальные табы после Proton_LG, сортируем по алфавиту
        for source_key in sorted(tabs_dict.keys()):
            entries = tabs_dict[source_key]
            if self.create_tab_from_entries(source_key, entries):
                successful_tabs += 1

        return successful_tabs

    def filter_entries_by_cpu_level(self, entries, source_name):
        """Filter entries based on CPU compatibility"""
        if self.cpu_level >= 4:
            # If CPU supports all features, return all entries
            return entries

        filtered_entries = []

        for entry in entries:
            # Get the filename from the entry
            url = entry.get('url', '')
            filename = entry.get('name', '')

            if url:
                parsed_url = urllib.parse.urlparse(url)
                url_filename = os.path.basename(parsed_url.path)
                if url_filename:
                    filename = url_filename

            # Check if the filename contains version indicators that require specific CPU levels
            should_include = True

            # Check for v2, v3, v4 indicators in the filename
            if 'v2' in filename and self.cpu_level < 2:
                should_include = False
            elif 'v3' in filename and self.cpu_level < 3:
                should_include = False
            elif 'v4' in filename and self.cpu_level < 4:
                should_include = False

            if should_include:
                filtered_entries.append(entry)

        logger.info(f"Filtered {len(entries)} -> {len(filtered_entries)} entries for {source_name} based on CPU level {self.cpu_level}")
        return filtered_entries


    def create_tab_from_entries(self, source_name, entries):
        """Создаем вкладку с таблицей для источника Proton из записей JSON"""

        try:
            logger.debug(f"Processing {len(entries)} entries for source: {source_name}")

            tab = QWidget()
            tab.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

            layout = QVBoxLayout(tab)
            layout.setContentsMargins(2, 2, 2, 2)
            layout.setSpacing(2)

            table = QTableWidget()
            table.verticalHeader().setVisible(False)
            table.setColumnCount(2)  # Только Checkbox и Имя
            table.setHorizontalHeaderLabels(['', _('Asset Name')])
            table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)

            header = table.horizontalHeader()
            header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
            header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)

            # Include all entries (both installed and non-installed)
            all_entries = []
            for entry in entries:
                # Извлекаем имя файла из URL
                url = entry.get('url', '')

                if url:
                    parsed_url = urllib.parse.urlparse(url)
                    url_filename = os.path.basename(parsed_url.path)
                    if url_filename:
                        entry['filename'] = url_filename

                all_entries.append(entry)

            # Sort entries by version before displaying
            all_entries.sort(key=version_sort_key)

            table.setRowCount(len(all_entries))
            table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

            table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
            table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
            table.cellClicked.connect(self.on_cell_clicked)

            for row_index, entry in enumerate(all_entries):
                self.add_asset_row_from_json(table, row_index, entry, source_name)

            layout.addWidget(table, 1)

            tab_name = (self.get_short_source_name(source_name) or "UNKNOWN").upper()  # Название для Таба в верхний регистр
            self.tab_widget.addTab(tab, tab_name)

            logger.info(f"Successfully created tab for {source_name} with {len(all_entries)} assets (filtered from {len(entries)})")
            return True

        except Exception as e:
            logger.error(f"Error creating tab for {source_name}: {e}")
            return False

    def get_short_source_name(self, full_name):
        """Получаем короткое имя для вкладки (Таба) из полного имени источника"""
        if full_name is None:
            return "UNKNOWN"

        short_names = {
            'proton_lg': 'PROTON_LG',
            'proton_ge': 'PROTON_GE',
            'wine_kron4ek': 'WINE_KRON4EK',
            'wine_ge': 'WINE_GE',
            'proton_cachyos': 'PROTON_CACHYOS',
            'winepak': 'WINEPAK',
            'proton_sarek': 'PROTON_SAREK',
            'wine_staging': 'WINE_STAGING',
            'wine_valve': 'WINE_VALVE',
            'proton_valve': 'PROTON_VALVE',
            'proton_em': 'PROTON_EM'
        }

        return short_names.get(full_name.lower(), full_name.upper())

    def add_asset_row_from_json(self, table, row_index, entry, source_name):
        """Добавляем строку для определенной позиции из JSON"""
        # Извлекаем имя файла из URL
        url = entry.get('url', '')
        filename = entry.get('name', '')

        if url:
            parsed_url = urllib.parse.urlparse(url)
            url_filename = os.path.basename(parsed_url.path)
            if url_filename:
                filename = url_filename

        # Извлекаем версию для уникального ID
        version_from_name = self.extract_version_from_name(filename)

        # Проверяем, установлен ли уже этот ассет
        uppercase_filename = filename.upper() # Преобразование имени WINE в верхний регистр
        is_installed = self.is_asset_installed(uppercase_filename, source_name)

        checkbox_widget = QWidget()
        checkbox_layout = QHBoxLayout(checkbox_widget)
        checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
        checkbox_layout.setContentsMargins(0, 0, 0, 0)

        checkbox = QCheckBox()

        # Создаем структуру для позиции (элемента)
        asset_data = {
            'name': filename,  # имя с расширением
            'browser_download_url': url,
        }

        if is_installed:
            # If asset is already installed, disable the checkbox
            checkbox.setEnabled(False)
            checkbox.setChecked(False)  # Ensure it's not checked
        else:
            # Only connect the signal if the asset is not installed
            checkbox.stateChanged.connect(lambda state, a=asset_data, v=version_from_name,
                                                 s=source_name:
                                          self.on_asset_toggled_json(state, a, v, s))

        checkbox_layout.addWidget(checkbox)

        table.setCellWidget(row_index, 0, checkbox_widget)

        # Remove .tar.xz and .tar.gz extensions completely
        display_name = filename
        if filename.lower().endswith(('.tar.xz', '.tar.gz')):
            display_name = filename[:-7]

        asset_name_item = QTableWidgetItem(display_name)

        if is_installed:
            # Make the item disabled and add "(installed)" suffix
            asset_name_item.setFlags(asset_name_item.flags() & ~Qt.ItemFlag.ItemIsEnabled)
            # Add "(installed)" suffix to indicate it's already installed
            asset_name_item.setText(_('{display_name} (installed)').format(display_name=display_name))

        table.setItem(row_index, 1, asset_name_item)

        # Собираем метаданные в данных элемента
        unique_id = f"{source_name}_{version_from_name}_{filename}"
        for col in range(table.columnCount()):
            item = table.item(row_index, col)
            if item:
                item.setData(Qt.ItemDataRole.UserRole, {
                    'asset': asset_data,
                    'unique_id': unique_id,
                    'json_entry': entry,
                    'source_name': source_name,
                    'version': version_from_name
                })

    def extract_version_from_name(self, name):
        """Получаем версию из имени элемента"""
        if not name:
            return "N/A"

        # Убираем расширение файла
        basename = os.path.splitext(name)[0]
        basename = os.path.splitext(basename)[0]  # Для двойных расширений .tar.gz

        # Получаем версию по паттернам
        if 'GE-Proton' in basename:
            parts = basename.split('-')
            if len(parts) >= 2:
                return '-'.join(parts[:2])
        elif 'wine-' in basename.lower():
            parts = basename.split('-')
            if len(parts) >= 2:
                return parts[1]
        elif 'proton-' in basename.lower():
            parts = basename.split('-')
            if len(parts) >= 2:
                return parts[1]

        # Общий случай для всего
        return basename.split('-')[0] if '-' in basename else basename

    def is_asset_installed(self, asset_filename, source_name):
        """Check if asset is already installed in PortProton data/dist"""
        if not self.portproton_location:
            return False

        # Determine the directory name without extensions
        name_without_ext = asset_filename
        for ext in ['.tar.gz', '.tar.xz']:
            if name_without_ext.lower().endswith(ext):
                name_without_ext = name_without_ext[:-len(ext)]
                break

        # Check if the corresponding directory exists in data/dist
        dist_path = os.path.join(self.portproton_location, "data", "dist")
        expected_dir = os.path.join(dist_path, name_without_ext)

        return os.path.exists(expected_dir)

    def on_cell_clicked(self, row):
        """Обработка клика по ячейке - переключение флажка при клике по любой ячейке в строке"""
        tab = self.tab_widget.currentWidget()
        table = tab.findChild(QTableWidget)
        if table:
            checkbox_widget = table.cellWidget(row, 0)
            if checkbox_widget:
                checkbox = checkbox_widget.findChild(QCheckBox)
                if checkbox and checkbox.isEnabled():
                    checkbox.setChecked(not checkbox.isChecked())

    def on_asset_toggled_json(self, state, asset, version, source_name):
        """Обработка выбора/отмены выбора элемента из данных JSON"""
        # Всегда извлекаем имя файла из URL
        url = asset.get('browser_download_url', '')
        filename = asset.get('name', '')  # Исходное имя из JSON

        # Получаем имя файла из URL (оно всегда с расширением)
        if url:
            parsed_url = urllib.parse.urlparse(url)
            url_filename = os.path.basename(parsed_url.path)
            if url_filename:  # Если удалось получить имя из URL
                filename = url_filename

        unique_id = f"{source_name}_{version}_{filename}"

        if state == Qt.CheckState.Checked.value:
            self.selected_assets[unique_id] = {
                'source_name': source_name,
                'version': version,
                'asset': asset,
                'asset_name': filename,  # Используем имя файла с расширением
                'download_url': asset['browser_download_url']
            }
        else:
            if unique_id in self.selected_assets:
                del self.selected_assets[unique_id]

        self.update_selection_display()

    def update_selection_display(self):
        """Обновляем отображение выбора"""
        if self.selected_assets:
            selection_text = _('Selected {} assets:\n').format(len(self.selected_assets))

            for i, asset_data in enumerate(self.selected_assets.values(), 1):
                selection_text += f"{i}. {asset_data['source_name'].upper()} - {asset_data['asset_name']}\n"

            self.selection_text.setPlainText(selection_text)
            self.download_btn.setEnabled(True)
        else:
            self.selection_text.setPlainText(_("No assets selected"))
            self.download_btn.setEnabled(False)

    def clear_selection(self):
        """Очищаем (сбрасываем) всё выбранное"""
        if self.is_downloading:
            QMessageBox.warning(self, _("Downloading in Progress"), _("Cannot clear selection while extraction is in progress."))
            return

        self.selected_assets.clear()

        for tab_index in range(self.tab_widget.count()):
            tab = self.tab_widget.widget(tab_index)
            table = tab.findChild(QTableWidget)
            if table:
                for row in range(table.rowCount()):
                    checkbox_widget = table.cellWidget(row, 0)
                    if checkbox_widget:
                        checkbox = checkbox_widget.findChild(QCheckBox)
                        if checkbox and checkbox.isEnabled():
                            checkbox.setChecked(False)

        self.update_selection_display()

    def download_selected(self):
        """Extract all selected archives"""
        if not self.selected_assets:
            QMessageBox.warning(self, _("No Selection"), _("Please select at least one archive to download."))
            return

        if self.is_downloading:
            QMessageBox.warning(self, _("Downloading in Progress"), _("Please wait for current downloading to complete."))
            return

        downloads_dir = "proton_downloads"
        if not os.path.exists(downloads_dir):
            os.makedirs(downloads_dir)

        self.assets_to_download = list(self.selected_assets.values())
        self.current_download_index = 0
        self.is_downloading = True
        self.start_next_download()

    def start_next_download(self):
        """Start extraction of next archive in the list"""
        if self.current_download_index >= len(self.assets_to_download):
            # All extractions completed
            self.download_frame.setVisible(False)
            self.download_btn.setEnabled(True)
            self.clear_btn.setEnabled(True)
            self.is_downloading = False
            QMessageBox.information(self, _("Downloading Complete"), _("All selected archives have been downloaded!"))
            return

        asset_data = self.assets_to_download[self.current_download_index]
        self.download_asset(asset_data)

    def download_asset(self, asset_data):
        """Download and then extract the archive"""

        # DEBUG FEATURE: Check if proton_downloads folder exists in repo root and contains the file
        # This is a debug feature to use local files instead of downloading - remember to remove for production
        repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # Go up from portprotonqt/
        proton_downloads_path = os.path.join(repo_root, "proton_downloads")

        local_file_path = None
        if os.path.exists(proton_downloads_path) and os.path.isdir(proton_downloads_path):
            # Look for the asset file in proton_downloads
            for filename in os.listdir(proton_downloads_path):
                if filename == asset_data['asset_name']:
                    local_file_path = os.path.join(proton_downloads_path, filename)
                    logger.info(f"DEBUG: Using local file instead of downloading: {local_file_path}")
                    break

        if local_file_path and os.path.exists(local_file_path):
            # Use local file, skip download
            logger.info(f"DEBUG: Skipping download, using local file: {local_file_path}")
            download_info = f"{asset_data['source_name'].upper()} - {asset_data['asset_name']} (DEBUG: local)"
            if len(download_info) > 80:
                download_info = download_info[:77] + "..."
            self.download_progress.setValue(0)
            self.download_frame.setVisible(True)
            self.download_btn.setEnabled(False)
            self.clear_btn.setEnabled(False)

            # Simulate download completion and start extraction immediately
            QTimer.singleShot(100, lambda: self.start_extraction_for_asset(asset_data, local_file_path))
        else:
            # Normal download process
            # Create a temporary file path for download
            temp_dir = tempfile.mkdtemp(prefix="portproton_wine_")
            filename = os.path.join(temp_dir, asset_data['asset_name'])
            download_url = asset_data['download_url']

            download_info = f"{asset_data['source_name'].upper()} - {asset_data['asset_name']}"
            if len(download_info) > 80:
                download_info = download_info[:77] + "..."
            self.download_progress.setValue(0)
            self.download_frame.setVisible(True)
            self.download_btn.setEnabled(False)
            self.clear_btn.setEnabled(False)

            # Create and start download thread
            self.current_download_thread = DownloadThread(download_url, filename)

            def update_download_progress(progress):
                self.download_progress.setValue(progress)
                self.download_info_label.setText(_("Downloading: {0} ({1}%)").format(asset_data['asset_name'], progress))

            def download_finished(filepath, success):
                if success:
                    logger.info(f"Successfully downloaded: {filepath}")
                    # Now start extraction
                    self.start_extraction_for_asset(asset_data, filepath)
                else:
                    logger.error(f"Failed to download: {filepath}")
                    # Clean up temp directory
                    temp_dir = os.path.dirname(filepath)
                    try:
                        shutil.rmtree(temp_dir)
                    except (OSError, FileNotFoundError):
                        pass
                    self.current_download_index += 1
                    QTimer.singleShot(100, self.start_next_download)

            def download_error(error_msg):
                logger.error(f"Download error: {error_msg}")
                QMessageBox.critical(self, "Download Error", f"Failed to download archive: {error_msg}")
                # Clean up temp directory
                temp_dir = os.path.dirname(filename)
                try:
                    shutil.rmtree(temp_dir)
                except (OSError, FileNotFoundError):
                    pass
                self.current_download_index += 1
                QTimer.singleShot(100, self.start_next_download)

            self.current_download_thread.progress.connect(update_download_progress)
            self.current_download_thread.finished.connect(download_finished)
            self.current_download_thread.error.connect(download_error)
            self.current_download_thread.start()

    def start_extraction_for_asset(self, asset_data, filepath):
        """Start extraction for a downloaded asset"""
        # Update progress bar to show extraction phase
        self.download_info_label.setText(_("Extracting: {0}").format(asset_data['asset_name']))

        # Extract archive to PortProton data/dist directory using a separate thread
        if self.portproton_location:
            try:
                # Extract directly to dist directory - let the archive create its own folder structure
                dist_path = os.path.join(self.portproton_location, "data", "dist")
                extract_dir = dist_path

                # Create and start extraction thread
                self.current_extraction_thread = ExtractionThread(filepath, extract_dir)

                # Store speed and ETA values to use in the display
                current_speed = 0.0
                current_eta = 0

                def update_extraction_progress(progress):
                    self.download_progress.setValue(progress)
                    # Update the info label to show current progress during extraction
                    eta_text = _(', ETA: {}s').format(current_eta) if current_eta > 0 else ""
                    speed_text = _(', Speed: {:.1f}MB/s').format(current_speed) if current_speed > 0 else ""
                    self.download_info_label.setText(_("Extracting: {0}{1}{2}").format(
                        asset_data['asset_name'], speed_text, eta_text))

                def update_extraction_speed(speed):
                    nonlocal current_speed
                    current_speed = speed

                def update_extraction_eta(eta):
                    # Use ETA to pass actual ETA information during extraction
                    nonlocal current_eta
                    # Now we only use ETA for actual estimated time of arrival
                    current_eta = eta

                def extraction_finished(archive_path, success):
                    if success:
                        logger.info(f"Successfully extracted: {archive_path}")

                        # Run the initial command after successful extraction
                        import subprocess
                        try:
                            # Get the proper PortProton start command
                            start_cmd = get_portproton_start_command()
                            if start_cmd:
                                result = subprocess.run(start_cmd + ["cli", "--initial"], timeout=10)
                                if result.returncode != 0:
                                    logger.warning(f"Initial PortProton command returned non-zero exit code: {result.returncode}")
                            else:
                                logger.warning("Could not determine PortProton start command, skipping initial command")
                        except subprocess.TimeoutExpired:
                            logger.warning("Initial PortProton command timed out")
                        except Exception as e:
                            logger.error(f"Error running initial PortProton command: {e}")
                    else:
                        logger.error(f"Failed to extract: {archive_path}")
                        QMessageBox.critical(self, _("Extraction Error"), _("Failed to extract archive: {0}").format(archive_path))

                    # Clean up temp directory after extraction
                    temp_dir = os.path.dirname(filepath)
                    try:
                        shutil.rmtree(temp_dir)
                        logger.debug(f"Cleaned up temporary directory: {temp_dir}")
                    except Exception as e:
                        logger.warning(f"Could not clean up temporary directory {temp_dir}: {e}")

                    if self.current_extraction_thread and self.current_extraction_thread.isRunning():
                        logger.debug("Waiting for extraction thread to finish...")
                        if not self.current_extraction_thread.wait(500):
                            logger.warning("Extraction thread still running, but continuing...")

                    self.current_download_index += 1
                    QTimer.singleShot(100, self.start_next_download)

                def extraction_error(error_msg):
                    logger.error(f"Extraction error: {error_msg}")
                    QMessageBox.critical(self, "Extraction Error", f"Failed to extract archive: {error_msg}")

                    # Clean up temp directory after error
                    temp_dir = os.path.dirname(filepath)
                    try:
                        shutil.rmtree(temp_dir)
                        logger.debug(f"Cleaned up temporary directory after error: {temp_dir}")
                    except Exception as e:
                        logger.warning(f"Could not clean up temporary directory {temp_dir}: {e}")

                    if self.current_extraction_thread and self.current_extraction_thread.isRunning():
                        logger.debug("Waiting for extraction thread to finish after error...")
                        if not self.current_extraction_thread.wait(500):
                            logger.warning("Extraction thread still running after error, but continuing...")

                    self.current_download_index += 1
                    QTimer.singleShot(100, self.start_next_download)

                self.current_extraction_thread.progress.connect(update_extraction_progress)
                self.current_extraction_thread.speed.connect(update_extraction_speed)
                self.current_extraction_thread.eta.connect(update_extraction_eta)
                self.current_extraction_thread.finished.connect(extraction_finished)
                self.current_extraction_thread.error.connect(extraction_error)
                self.current_extraction_thread.start()

            except Exception as e:
                # Clean up temp directory in case of exception
                temp_dir = os.path.dirname(filepath)
                try:
                    shutil.rmtree(temp_dir)
                    logger.debug(f"Cleaned up temporary directory after exception: {temp_dir}")
                except Exception as cleanup_error:
                    logger.warning(f"Could not clean up temporary directory {temp_dir}: {cleanup_error}")

                logger.error(f"Error starting extraction thread for {filepath}: {e}")
                QMessageBox.critical(self, "Extraction Error", f"Failed to start extraction: {e}")
                self.current_download_index += 1
                QTimer.singleShot(100, self.start_next_download)
        else:
            # Clean up temp directory if portproton location is not provided
            temp_dir = os.path.dirname(filepath)
            try:
                shutil.rmtree(temp_dir)
                logger.debug(f"Cleaned up temporary directory: {temp_dir}")
            except Exception as e:
                logger.warning(f"Could not clean up temporary directory {temp_dir}: {e}")

            logger.warning("PortProton location not provided, skipping extraction")
            self.current_download_index += 1
            QTimer.singleShot(100, self.start_next_download)

    def cancel_current_download(self):
        """Cancel current download or extraction"""
        # Stop extraction thread if running
        if self.current_extraction_thread and self.current_extraction_thread.isRunning():
            self.current_extraction_thread.stop()
            if not self.current_extraction_thread.wait(1000):
                logger.warning("Extraction thread did not stop gracefully")

        # Stop download thread if running
        try:
            if (self.current_download_thread and
                hasattr(self.current_download_thread, 'isRunning') and
                self.current_download_thread.isRunning()):
                if hasattr(self.current_download_thread, 'stop'):
                    self.current_download_thread.stop()
                if not self.current_download_thread.wait(1000):
                    logger.warning("Download thread did not stop gracefully")
        except RuntimeError:
            # Object already deleted, which is fine
            logger.debug("Download thread object already deleted during cancel")

        # Очищаем список загрузок
        self.assets_to_download = []
        self.current_download_index = 0
        self.is_downloading = False

        # Сброс/перезапуск UI
        self.download_frame.setVisible(False)
        self.download_btn.setEnabled(True)
        self.clear_btn.setEnabled(True)

        QMessageBox.information(self, _("Operation Cancelled"), _("Download or extraction has been cancelled."))

    def closeEvent(self, event):
        """Проверка, что все потоки останавливаются при закрытии приложения"""
        logger.debug("Closing ProtonManager dialog...")

        # Stop extraction thread if running
        if self.current_extraction_thread and self.current_extraction_thread.isRunning():
            logger.debug("Stopping current extraction thread...")
            self.current_extraction_thread.stop()
            if not self.current_extraction_thread.wait(2000):
                logger.warning("Extraction thread did not stop gracefully during close")

        # Stop download thread if running
        try:
            if (self.current_download_thread and
                hasattr(self.current_download_thread, 'isRunning') and
                self.current_download_thread.isRunning()):
                logger.debug("Stopping current download thread...")
                if hasattr(self.current_download_thread, 'stop'):
                    self.current_download_thread.stop()
                if not self.current_download_thread.wait(2000):
                    logger.warning("Download thread did not stop gracefully during close")
        except RuntimeError:
            # Object already deleted, which is fine
            logger.debug("Download thread object already deleted during close")

        event.accept()



def show_proton_manager(parent=None, portproton_location=None):
    """
    Shows the Proton/WINE archive extractor dialog.

    Args:
        parent: Parent widget for the dialog
        portproton_location: Location of PortProton installation

    Returns:
        ProtonManager dialog instance
    """
    dialog = ProtonManager(parent, portproton_location)
    dialog.exec()  # Use exec() for modal dialog
    return dialog
