#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Python Package Manager v2.2
Графический менеджер для управления Python пакетами и виртуальными окружениями
"""

import os
import sys
import subprocess
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, filedialog
from pathlib import Path
import shutil
import json
import requests
import logging
import re
import threading
import time
import shlex
import tempfile
import platform
from typing import Optional, List, Dict, Tuple, Any, Union, Callable
from functools import wraps
from datetime import datetime
from dataclasses import dataclass

# ==================== НАСТРОЙКА ЛОГИРОВАНИЯ ====================
logging.basicConfig(
    filename='package_manager.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)


# ==================== КАСТОМНЫЕ ИСКЛЮЧЕНИЯ ====================
class PackageManagerError(Exception):
    """Базовое исключение для Package Manager"""
    pass


class PipError(PackageManagerError):
    """Ошибка при работе с pip"""
    pass


class PackageNotFoundError(PipError):
    """Пакет не найден"""
    pass


class VenvError(PackageManagerError):
    """Ошибка при работе с виртуальным окружением"""
    pass


class OperationCancelledError(PackageManagerError):
    """Операция была отменена пользователем"""
    pass


class CommandInjectionError(PackageManagerError):
    """Обнаружена попытка инъекции команд"""
    pass


# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
def sanitize_arg(arg: str) -> str:
    """
    Санитизация аргумента для безопасного выполнения команд
    
    Args:
        arg: Аргумент для санитизации
    
    Returns:
        str: Санитизированный аргумент
    
    Raises:
        CommandInjectionError: При обнаружении подозрительных символов
    """
    if not arg:
        return arg
    
    # Опасные символы для shell (исключаем дефис, так как он используется в опциях)
    dangerous_chars = r'[;&|`$(){}[\]<>!?*\\]'
    if re.search(dangerous_chars, arg):
        logger.warning(f"Potential command injection detected in: {arg}")
        raise CommandInjectionError(f"Command injection attempt detected: {arg}")
    
    # Проверка на потенциально опасные последовательности
    if '../' in arg or '..\\' in arg:
        raise CommandInjectionError(f"Path traversal attempt detected: {arg}")
    
    # Экранирование с помощью shlex.quote только для не-опций
    if arg.startswith('-'):
        # Опции должны соответствовать допустимому формату
        if not re.match(r'^-[a-zA-Z0-9\-]+$', arg):
            raise CommandInjectionError(f"Invalid option format: {arg}")
        return arg
    else:
        return shlex.quote(arg)


def validate_package_name(name: str) -> bool:
    """
    Проверка валидности имени пакета
    
    Args:
        name: Имя пакета
    
    Returns:
        bool: True если валидное
    """
    if not name or len(name) > 100:
        return False
    # Разрешаем буквы, цифры, дефис, подчеркивание, точку
    pattern = r'^[a-zA-Z0-9\-_.]+$'
    return bool(re.match(pattern, name))


def validate_version(version: str) -> bool:
    """
    Проверка валидности версии пакета
    
    Args:
        version: Строка версии
    
    Returns:
        bool: True если валидная
    """
    if not version:
        return True
    if len(version) > 50:
        return False
    # Проверка на опасные символы
    pattern = r'^[a-zA-Z0-9\-_.+=<>~!*]+$'
    return bool(re.match(pattern, version))


def get_python_executable(venv_path: Path) -> Optional[Path]:
    """
    Получить путь к Python в виртуальном окружении
    
    Args:
        venv_path: Путь к виртуальному окружению
    
    Returns:
        Optional[Path]: Путь к Python или None
    """
    # Linux/Mac
    python_path = venv_path / 'bin' / 'python'
    if python_path.exists():
        return python_path
    
    # Windows
    python_path = venv_path / 'Scripts' / 'python.exe'
    if python_path.exists():
        return python_path
    
    return None


def is_windows() -> bool:
    """Проверка, является ли ОС Windows"""
    return platform.system() == 'Windows'


# ==================== ДЕКОРАТОРЫ ====================
def safe_execute(func: Callable) -> Callable:
    """
    Декоратор для безопасного выполнения методов с обработкой ошибок
    """
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        try:
            return func(self, *args, **kwargs)
        except OperationCancelledError:
            logger.info(f"Operation cancelled in {func.__name__}")
            if hasattr(self, 'status_var'):
                self.status_var.set("Cancelled")
        except Exception as e:
            logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
            messagebox.showerror(
                "Error", 
                f"An error occurred in {func.__name__}:\n\n{str(e)}"
            )
            if hasattr(self, 'status_var'):
                self.status_var.set("Error")
        return None
    return wrapper


class CancellableThread(threading.Thread):
    """
    Поток с возможностью отмены
    """
    def __init__(self, target: Callable = None, args: tuple = (), kwargs: dict = None):
        super().__init__(target=target, args=args, kwargs=kwargs or {})
        self._stop_event = threading.Event()
        self.daemon = True
    
    def stop(self):
        """Запрос на остановку потока"""
        self._stop_event.set()
    
    def is_cancelled(self) -> bool:
        """Проверка, была ли запрошена отмена"""
        return self._stop_event.is_set()


class ProgressContext:
    """Контекст для отслеживания прогресса и отмены"""
    def __init__(self, total: int = 0):
        self.total = total
        self.current = 0
        self.cancelled = False
        self._lock = threading.Lock()
    
    def update(self, increment: int = 1) -> None:
        """Обновить прогресс"""
        with self._lock:
            self.current += increment
            if self.current > self.total:
                self.current = self.total
    
    def cancel(self) -> None:
        """Отменить операцию"""
        with self._lock:
            self.cancelled = True
    
    def is_cancelled(self) -> bool:
        """Проверка отмены"""
        with self._lock:
            return self.cancelled
    
    def get_progress(self) -> Tuple[int, int]:
        """Получить текущий прогресс"""
        with self._lock:
            return self.current, self.total
    
    @property
    def percentage(self) -> float:
        """Процент выполнения"""
        with self._lock:
            if self.total == 0:
                return 0
            return (self.current / self.total) * 100


# ==================== КЭШ МЕНЕДЖЕР ====================
class CacheManager:
    """
    Менеджер кэширования с поддержкой TTL и инвалидации
    """
    
    def __init__(self, ttl: int = 30):
        self.cache: Dict[str, Tuple[Any, float]] = {}
        self.ttl = ttl
        self._lock = threading.RLock()
        self._cleanup_thread = None
        self._stop_cleanup = threading.Event()
        self._start_cleanup()
    
    def _start_cleanup(self):
        """Запуск фоновой очистки кэша"""
        def cleanup():
            while not self._stop_cleanup.is_set():
                time.sleep(60)
                self._cleanup_expired()
        
        self._cleanup_thread = threading.Thread(target=cleanup, daemon=True)
        self._cleanup_thread.start()
    
    def _cleanup_expired(self):
        """Очистка истекших записей"""
        with self._lock:
            current_time = time.time()
            expired_keys = [
                key for key, (_, timestamp) in self.cache.items()
                if current_time - timestamp > self.ttl
            ]
            for key in expired_keys:
                del self.cache[key]
    
    def get(self, key: str) -> Optional[Any]:
        """Получить значение из кэша"""
        with self._lock:
            if key in self.cache:
                data, timestamp = self.cache[key]
                if time.time() - timestamp <= self.ttl:
                    return data
                del self.cache[key]
        return None
    
    def set(self, key: str, value: Any):
        """Сохранить значение в кэш"""
        with self._lock:
            self.cache[key] = (value, time.time())
    
    def invalidate(self, key: Optional[str] = None):
        """Инвалидировать кэш"""
        with self._lock:
            if key:
                if key in self.cache:
                    del self.cache[key]
            else:
                self.cache.clear()
    
    def clear(self):
        """Полная очистка кэша"""
        with self._lock:
            self.cache.clear()
    
    def __del__(self):
        self._stop_cleanup.set()
        if self._cleanup_thread and self._cleanup_thread.is_alive():
            self._cleanup_thread.join(timeout=2)


# ==================== БЕЗОПАСНЫЙ МЕНЕДЖЕР PIP ====================
class SecurePipManager:
    """
    Безопасный менеджер для операций с pip
    """
    
    def __init__(self, python_path: Optional[Path] = None):
        self.python_path = Path(python_path) if python_path else Path(sys.executable)
        self.cache = CacheManager(ttl=30)
        
        # Проверка существования Python
        if not self.python_path.exists():
            raise FileNotFoundError(f"Python not found: {self.python_path}")
    
    def _sanitize_args(self, args: List[str]) -> List[str]:
        """
        Санитизация списка аргументов
        
        Args:
            args: Список аргументов
        
        Returns:
            List[str]: Санитизированный список
        """
        sanitized = []
        for arg in args:
            if arg.startswith('-'):
                # Для опций разрешаем дефис и другие допустимые символы
                # Опции могут быть в формате: -f, --format, --format=json
                if arg.startswith('--'):
                    # Длинные опции: --option или --option=value
                    if '=' in arg:
                        opt, val = arg.split('=', 1)
                        if not re.match(r'^--[a-zA-Z0-9\-]+$', opt):
                            raise CommandInjectionError(f"Invalid option format: {arg}")
                        # Валидируем значение
                        if not re.match(r'^[a-zA-Z0-9\-_.]+$', val):
                            raise CommandInjectionError(f"Invalid option value: {arg}")
                    else:
                        if not re.match(r'^--[a-zA-Z0-9\-]+$', arg):
                            raise CommandInjectionError(f"Invalid option format: {arg}")
                else:
                    # Короткие опции: -f, -y
                    if not re.match(r'^-[a-zA-Z0-9]+$', arg):
                        raise CommandInjectionError(f"Invalid option format: {arg}")
                sanitized.append(arg)
            else:
                # Для аргументов применяем экранирование
                # Проверяем на опасные символы
                if re.search(r'[;&|`$(){}[\]<>!?*\\]', arg):
                    raise CommandInjectionError(f"Dangerous characters in argument: {arg}")
                sanitized.append(shlex.quote(arg))
        return sanitized
    
    def _run_pip(
        self,
        *args: str,
        timeout: int = 60,
        check: bool = False,
        **kwargs
    ) -> subprocess.CompletedProcess:
        """
        Безопасное выполнение команды pip
        
        Args:
            *args: Аргументы команды
            timeout: Таймаут в секундах
            check: Проверять возвращаемый код
            **kwargs: Дополнительные параметры
        
        Returns:
            CompletedProcess: Результат выполнения
        
        Raises:
            PipError: При ошибке выполнения
            CommandInjectionError: При попытке инъекции
        """
        # Санитизация аргументов
        try:
            sanitized_args = self._sanitize_args(list(args))
        except CommandInjectionError as e:
            logger.error(f"Command injection blocked: {e}")
            raise
        
        # Блокировка опасных команд (только для аргументов, не опций)
        dangerous_commands = ['rm', 'del', 'format', 'sudo', 'chmod', 'chown']
        for arg in sanitized_args:
            if not arg.startswith('-') and arg in dangerous_commands:
                raise CommandInjectionError(f"Dangerous command blocked: {arg}")
        
        cmd = [str(self.python_path), '-m', 'pip'] + sanitized_args
        logger.debug(f"Executing: {' '.join(cmd)}")
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
                **kwargs
            )
            
            if check and result.returncode != 0:
                raise PipError(f"Pip command failed: {result.stderr}")
            
            logger.debug(f"Return code: {result.returncode}")
            return result
            
        except subprocess.TimeoutExpired:
            logger.error(f"Timeout executing: {' '.join(cmd)}")
            raise PipError(f"Command timed out after {timeout} seconds")
        except Exception as e:
            logger.error(f"Error executing command: {e}")
            raise PipError(f"Failed to execute command: {str(e)}")
    
    def list_packages(self, format: str = 'json') -> Union[List[Dict], str]:
        """
        Получить список пакетов
        
        Args:
            format: Формат вывода ('json', 'freeze', 'columns')
        
        Returns:
            Список пакетов или строка
        """
        cache_key = f"list_{format}_{str(self.python_path)}"
        cached = self.cache.get(cache_key)
        if cached is not None:
            return cached
        
        try:
            if format == 'json':
                result = self._run_pip('list', '--format=json')
                if result.returncode == 0:
                    try:
                        data = json.loads(result.stdout)
                        self.cache.set(cache_key, data)
                        return data
                    except json.JSONDecodeError as e:
                        logger.error(f"Failed to parse JSON: {e}")
                        return []
            elif format == 'freeze':
                result = self._run_pip('freeze')
                if result.returncode == 0:
                    self.cache.set(cache_key, result.stdout)
                    return result.stdout
            else:
                result = self._run_pip('list', '--format=columns')
                if result.returncode == 0:
                    self.cache.set(cache_key, result.stdout)
                    return result.stdout
        except (PipError, CommandInjectionError) as e:
            logger.error(f"Failed to list packages: {e}")
            # Возвращаем пустой результат вместо ошибки
            return [] if format == 'json' else ""
        
        return [] if format == 'json' else ""
    
    def install_package(
        self,
        package: str,
        upgrade: bool = False,
        global_install: bool = False,
        progress_context: Optional[ProgressContext] = None
    ) -> Tuple[bool, str]:
        """
        Безопасная установка пакета
        
        Args:
            package: Имя пакета с версией
            upgrade: Обновить пакет
            global_install: Установка в глобальное окружение
            progress_context: Контекст прогресса для отмены
        
        Returns:
            Tuple[bool, str]: (успех, сообщение)
        """
        # Валидация имени пакета
        pkg_name = package.split('==')[0].split('>=')[0].split('<=')[0].split('~=')[0]
        pkg_name = pkg_name.strip()
        if not validate_package_name(pkg_name):
            raise CommandInjectionError(f"Invalid package name: {pkg_name}")
        
        # Проверка отмены
        if progress_context and progress_context.is_cancelled():
            raise OperationCancelledError("Operation cancelled by user")
        
        cmd = ['install']
        if upgrade:
            cmd.append('--upgrade')
        if global_install and str(self.python_path) == sys.executable:
            cmd.append('--break-system-packages')
        
        # Добавляем пакет
        cmd.append(package)
        
        try:
            result = self._run_pip(*cmd)
            if result.returncode == 0:
                self.cache.invalidate()
                return True, result.stdout
            return False, result.stderr
        except (PipError, CommandInjectionError) as e:
            logger.error(f"Failed to install {package}: {e}")
            return False, str(e)
    
    def uninstall_package(self, package: str) -> Tuple[bool, str]:
        """
        Безопасное удаление пакета
        
        Args:
            package: Имя пакета
        
        Returns:
            Tuple[bool, str]: (успех, сообщение)
        """
        if not validate_package_name(package):
            raise CommandInjectionError(f"Invalid package name: {package}")
        
        try:
            result = self._run_pip('uninstall', '-y', package)
            if result.returncode == 0:
                self.cache.invalidate()
                return True, result.stdout
            return False, result.stderr
        except (PipError, CommandInjectionError) as e:
            logger.error(f"Failed to uninstall {package}: {e}")
            return False, str(e)
    
    def show_package(self, package: str) -> Optional[Dict[str, str]]:
        """
        Получить информацию о пакете
        
        Args:
            package: Имя пакета
        
        Returns:
            Dict: Информация о пакете или None
        """
        if not validate_package_name(package):
            raise CommandInjectionError(f"Invalid package name: {package}")
        
        try:
            result = self._run_pip('show', package)
            if result.returncode == 0:
                info = {}
                for line in result.stdout.splitlines():
                    if ': ' in line:
                        key, value = line.split(': ', 1)
                        info[key.lower().strip()] = value.strip()
                return info
        except (PipError, CommandInjectionError) as e:
            logger.error(f"Failed to show package {package}: {e}")
        
        return None
    
    def get_outdated(self) -> List[Dict]:
        """Получить список устаревших пакетов"""
        cache_key = f"outdated_{str(self.python_path)}"
        cached = self.cache.get(cache_key)
        if cached is not None:
            return cached
        
        try:
            result = self._run_pip('list', '--outdated', '--format=json')
            if result.returncode == 0:
                data = json.loads(result.stdout)
                self.cache.set(cache_key, data)
                return data
        except (PipError, json.JSONDecodeError, CommandInjectionError) as e:
            logger.error(f"Failed to get outdated packages: {e}")
        
        return []
    
    def install_from_requirements(
        self,
        file_path: str,
        global_install: bool = False,
        progress_context: Optional[ProgressContext] = None
    ) -> Tuple[bool, str]:
        """
        Установка пакетов из requirements.txt
        
        Args:
            file_path: Путь к файлу
            global_install: Глобальная установка
            progress_context: Контекст прогресса
        
        Returns:
            Tuple[bool, str]: (успех, сообщение)
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Requirements file not found: {file_path}")
        
        # Проверка отмены
        if progress_context and progress_context.is_cancelled():
            raise OperationCancelledError("Operation cancelled by user")
        
        cmd = ['install', '-r', file_path]
        if global_install and str(self.python_path) == sys.executable:
            cmd.append('--break-system-packages')
        
        try:
            result = self._run_pip(*cmd)
            if result.returncode == 0:
                self.cache.invalidate()
                return True, result.stdout
            return False, result.stderr
        except (PipError, CommandInjectionError) as e:
            logger.error(f"Failed to install from requirements: {e}")
            return False, str(e)
    
    def get_version(self) -> str:
        """Получить версию Python"""
        try:
            result = subprocess.run(
                [str(self.python_path), '--version'],
                capture_output=True,
                text=True,
                timeout=5
            )
            return result.stdout.strip() or result.stderr.strip()
        except Exception:
            return "Unknown"


# ==================== БЕЗОПАСНЫЙ ПОИСК НА PYPI ====================
class PyPISearch:
    """Класс для безопасного поиска на PyPI"""
    
    @staticmethod
    def search_package(package: str, timeout: int = 10) -> Optional[Dict]:
        """
        Поиск пакета на PyPI
        
        Args:
            package: Имя пакета
            timeout: Таймаут запроса
        
        Returns:
            Optional[Dict]: Информация о пакете или None
        
        Raises:
            PackageNotFoundError: Пакет не найден
            requests.RequestException: Ошибка сети
        """
        if not validate_package_name(package):
            raise CommandInjectionError(f"Invalid package name: {package}")
        
        # Используем официальное API PyPI с проверкой SSL
        try:
            response = requests.get(
                f"https://pypi.org/pypi/{package}/json",
                timeout=timeout,
                verify=True,
                headers={'User-Agent': 'PythonPackageManager/2.2'}
            )
            
            if response.status_code == 404:
                raise PackageNotFoundError(f"Package '{package}' not found on PyPI")
            
            if response.status_code != 200:
                raise requests.RequestException(f"HTTP {response.status_code}: {response.reason}")
            
            return response.json()
            
        except requests.exceptions.SSLError as e:
            logger.error(f"SSL error when accessing PyPI: {e}")
            raise requests.RequestException(f"SSL certificate verification failed: {e}")
        except requests.exceptions.Timeout:
            raise requests.RequestException("Request timed out")
        except requests.exceptions.ConnectionError:
            raise requests.RequestException("Network connection error")
        except json.JSONDecodeError as e:
            logger.error(f"Invalid JSON response: {e}")
            raise requests.RequestException("Invalid response from PyPI")


# ==================== ОСНОВНОЙ КЛАСС ПРИЛОЖЕНИЯ ====================
class PythonPackageManager:
    """Главный класс приложения"""
    
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Python Package Manager v2.2")
        self.root.geometry("1000x700")
        self.root.resizable(True, True)
        self.root.minsize(800, 600)
        
        # Инициализация компонентов
        self.global_pip = SecurePipManager()
        self.cache = CacheManager(ttl=60)
        self.selected_venv_path: Optional[str] = None
        self._progress_dialogs: List[tk.Toplevel] = []
        self._current_thread: Optional[CancellableThread] = None
        self._progress_context: Optional[ProgressContext] = None
        
        # Настройка GUI
        self._setup_styles()
        self._create_main_layout()
        self._create_global_tab()
        self._create_venv_tab()
        self._create_status_bar()
        
        # Загрузка данных
        self.load_global_packages()
        self.scan_venv()
        
        # Обработка закрытия
        self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
        
        logger.info("Application started")
    
    # ==================== НАСТРОЙКА GUI ====================
    def _setup_styles(self):
        """Настройка стилей"""
        style = ttk.Style()
        style.theme_use('clam')
        
        # Цвета
        style.configure('TFrame', background='#f0f0f0')
        style.configure('TLabel', background='#f0f0f0')
        style.configure('TButton', padding=6, relief='flat', background='#e0e0e0')
        style.map('TButton', background=[('active', '#d0d0d0')])
        
        # Treeview
        style.configure('Treeview', rowheight=25, font=('Arial', 10))
        style.configure('Treeview.Heading', font=('Arial', 10, 'bold'))
        
        # Progressbar
        style.configure('TProgressbar', thickness=15)
    
    def _create_main_layout(self):
        """Создание основного макета"""
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        self.notebook = ttk.Notebook(main_frame)
        self.notebook.pack(fill=tk.BOTH, expand=True)
    
    def _create_status_bar(self):
        """Создание статусной строки"""
        self.status_var = tk.StringVar()
        self.status_var.set("Ready")
        
        self.status_bar = ttk.Label(
            self.root,
            textvariable=self.status_var,
            relief=tk.SUNKEN,
            anchor=tk.W,
            font=('Arial', 9)
        )
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
    
    def _on_closing(self):
        """Обработка закрытия"""
        # Отмена текущей операции
        if self._current_thread and self._current_thread.is_alive():
            if messagebox.askyesno("Confirm Exit", "Operation in progress. Exit anyway?"):
                self._current_thread.stop()
                self._current_thread.join(timeout=2)
                self.root.destroy()
        else:
            self.root.destroy()
        
        logger.info("Application closed")
        self.cache.clear()
    
    # ==================== ДИАЛОГИ ПРОГРЕССА ====================
    def _show_progress_dialog(
        self,
        title: str,
        max_value: int = 100
    ) -> Tuple[tk.Toplevel, ttk.Progressbar, Callable]:
        """
        Показать диалог с прогресс-баром и кнопкой отмены
        
        Args:
            title: Заголовок
            max_value: Максимальное значение
        
        Returns:
            Tuple[окно, прогресс-бар, функция отмены]
        """
        dialog = tk.Toplevel(self.root)
        dialog.title(title)
        dialog.geometry("450x150")
        dialog.resizable(False, False)
        dialog.transient(self.root)
        dialog.grab_set()
        
        # Центрирование
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        x = (dialog.winfo_screenwidth() // 2) - (width // 2)
        y = (dialog.winfo_screenheight() // 2) - (height // 2)
        dialog.geometry(f'{width}x{height}+{x}+{y}')
        
        # Метка
        label_var = tk.StringVar()
        label_var.set("Processing...")
        label = ttk.Label(dialog, textvariable=label_var, font=('Arial', 10))
        label.pack(pady=(15, 10))
        
        # Прогресс-бар
        progress_bar = ttk.Progressbar(
            dialog,
            maximum=max_value,
            length=350,
            mode='determinate'
        )
        progress_bar.pack(pady=10)
        
        # Кнопка отмены
        cancelled = False
        
        def cancel():
            nonlocal cancelled
            cancelled = True
            if self._progress_context:
                self._progress_context.cancel()
            dialog.destroy()
        
        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(pady=10)
        ttk.Button(btn_frame, text="Cancel", command=cancel).pack()
        
        self._progress_dialogs.append(dialog)
        
        def update_progress(value: int, text: Optional[str] = None):
            if cancelled:
                raise OperationCancelledError("Operation cancelled by user")
            progress_bar['value'] = min(value, max_value)
            if text:
                label_var.set(text)
            dialog.update()
        
        def get_cancelled():
            return cancelled
        
        return dialog, progress_bar, update_progress, get_cancelled
    
    def _close_progress_dialog(self, dialog: tk.Toplevel):
        """Закрыть диалог прогресса"""
        if dialog in self._progress_dialogs:
            self._progress_dialogs.remove(dialog)
        dialog.destroy()
    
    # ==================== ВКЛАДКА ГЛОБАЛЬНЫХ ПАКЕТОВ ====================
    def _create_global_tab(self):
        """Создание вкладки глобальных пакетов"""
        self.global_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.global_frame, text="🌍 Global Packages")
        
        # Верхняя панель
        top_frame = ttk.Frame(self.global_frame)
        top_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Label(
            top_frame,
            text="Global Packages (pip list)",
            font=('Arial', 11, 'bold')
        ).pack(side=tk.LEFT)
        
        # Кнопки
        btn_frame = ttk.Frame(top_frame)
        btn_frame.pack(side=tk.RIGHT)
        
        buttons = [
            ("🔄 Refresh", self.load_global_packages),
            ("📦 Install", self.install_package_dialog),
            ("🔍 Search", self.search_pypi),
            ("📤 Export", self.export_requirements),
            ("📥 Import", self.install_from_requirements),
            ("🔄 Updates", self.check_updates),
            ("📊 Stats", self.show_global_stats)
        ]
        
        for text, command in buttons:
            ttk.Button(btn_frame, text=text, command=command).pack(side=tk.LEFT, padx=2)
        
        # Поиск
        self._create_search_bar(self.global_frame, self._filter_global_packages)
        
        # Дерево пакетов
        self._create_global_tree()
    
    def _create_search_bar(self, parent: ttk.Frame, filter_func: Callable):
        """Создание строки поиска"""
        search_frame = ttk.Frame(parent)
        search_frame.pack(fill=tk.X, pady=(0, 5))
        
        ttk.Label(search_frame, text="🔍 Filter:").pack(side=tk.LEFT, padx=5)
        search_var = tk.StringVar()
        search_var.trace('w', lambda *args: filter_func())
        search_entry = ttk.Entry(search_frame, textvariable=search_var)
        search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        
        ttk.Button(
            search_frame,
            text="Clear",
            command=lambda: (search_var.set(""), filter_func())
        ).pack(side=tk.RIGHT, padx=5)
        
        return search_var
    
    def _create_global_tree(self):
        """Создание дерева глобальных пакетов"""
        tree_frame = ttk.Frame(self.global_frame)
        tree_frame.pack(fill=tk.BOTH, expand=True)
        
        columns = ('Package', 'Version')
        self.global_tree = ttk.Treeview(
            tree_frame,
            columns=columns,
            show='tree headings',
            height=20
        )
        
        self.global_tree.heading('#0', text='#')
        self.global_tree.heading('Package', text='Package Name')
        self.global_tree.heading('Version', text='Version')
        
        self.global_tree.column('#0', width=40)
        self.global_tree.column('Package', width=350)
        self.global_tree.column('Version', width=150)
        
        scrollbar = ttk.Scrollbar(
            tree_frame,
            orient=tk.VERTICAL,
            command=self.global_tree.yview
        )
        self.global_tree.configure(yscrollcommand=scrollbar.set)
        
        self.global_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Контекстное меню
        self.global_context_menu = tk.Menu(self.root, tearoff=0)
        self.global_context_menu.add_command(
            label="📦 Update Package",
            command=self.update_global_package
        )
        self.global_context_menu.add_command(
            label="🗑️ Uninstall Package",
            command=self.uninstall_global_package
        )
        self.global_context_menu.add_separator()
        self.global_context_menu.add_command(
            label="ℹ️ Package Info",
            command=self.package_info
        )
        self.global_context_menu.add_command(
            label="🔗 Open PyPI",
            command=self._open_pypi_page
        )
        self.global_context_menu.add_separator()
        self.global_context_menu.add_command(
            label="📋 Copy Name",
            command=self._copy_package_name
        )
        
        self.global_tree.bind("<Button-3>", self._show_global_context_menu)
        self.global_tree.bind("<Double-1>", lambda e: self.package_info())
        
        self.global_packages_data: List[Dict] = []
        self.global_search_var = tk.StringVar()
    
    def _filter_global_packages(self):
        """Фильтрация глобальных пакетов"""
        query = self.global_search_var.get().lower().strip()
        
        for item in self.global_tree.get_children():
            values = self.global_tree.item(item)['values']
            if not query:
                self.global_tree.reattach(item, '', 'end')
            elif values and query in values[0].lower():
                self.global_tree.reattach(item, '', 'end')
            else:
                self.global_tree.detach(item)
    
    def _show_global_context_menu(self, event):
        """Показать контекстное меню"""
        item = self.global_tree.identify_row(event.y)
        if item:
            self.global_tree.selection_set(item)
            self.global_context_menu.post(event.x_root, event.y_root)
    
    def _get_selected_global_package(self) -> Optional[Dict[str, str]]:
        """Получить выбранный глобальный пакет"""
        selection = self.global_tree.selection()
        if not selection:
            messagebox.showwarning("Warning", "Please select a package")
            return None
        
        values = self.global_tree.item(selection[0])['values']
        if not values:
            return None
        
        return {'name': values[0], 'version': values[1]}
    
    def _copy_package_name(self):
        """Копировать имя пакета в буфер обмена"""
        pkg = self._get_selected_global_package()
        if pkg:
            self.root.clipboard_clear()
            self.root.clipboard_append(pkg['name'])
            self.status_var.set(f"Copied: {pkg['name']}")
    
    def _open_pypi_page(self):
        """Открыть страницу пакета на PyPI"""
        pkg = self._get_selected_global_package()
        if pkg:
            import webbrowser
            url = f"https://pypi.org/project/{pkg['name']}/"
            webbrowser.open(url)
            self.status_var.set(f"Opened: {url}")
    
    @safe_execute
    def load_global_packages(self):
        """Загрузка глобальных пакетов"""
        self.status_var.set("Loading global packages...")
        self.root.config(cursor="watch")
        self.root.update()
        
        try:
            # Очистка дерева
            for item in self.global_tree.get_children():
                self.global_tree.delete(item)
            
            # Загрузка пакетов
            packages = self.global_pip.list_packages('json')
            
            if not packages:
                self.status_var.set("No packages found")
                return
            
            # Добавление в дерево
            for i, pkg in enumerate(packages, 1):
                self.global_tree.insert(
                    '',
                    'end',
                    values=(pkg['name'], pkg['version']),
                    text=str(i)
                )
            
            self.status_var.set(f"Loaded {len(packages)} global packages")
            logger.info(f"Loaded {len(packages)} global packages")
            
        except Exception as e:
            logger.error(f"Error loading packages: {e}")
            messagebox.showerror("Error", f"Failed to load packages:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def update_global_package(self):
        """Обновление глобального пакета"""
        pkg = self._get_selected_global_package()
        if not pkg:
            return
        
        if not messagebox.askyesno(
            "Update Package",
            f"Update {pkg['name']} to latest version?\n\nCurrent: {pkg['version']}"
        ):
            return
        
        self.status_var.set(f"Updating {pkg['name']}...")
        self.root.config(cursor="watch")
        
        try:
            success, message = self.global_pip.install_package(
                pkg['name'],
                upgrade=True,
                global_install=True
            )
            
            if success:
                messagebox.showinfo("Success", f"Package {pkg['name']} updated successfully")
                self.load_global_packages()
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to update {pkg['name']}: {e}")
            messagebox.showerror("Error", f"Failed to update:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def uninstall_global_package(self):
        """Удаление глобального пакета"""
        pkg = self._get_selected_global_package()
        if not pkg:
            return
        
        if not messagebox.askyesno(
            "Uninstall Package",
            f"Are you sure you want to uninstall {pkg['name']}?\n\nThis action cannot be undone!"
        ):
            return
        
        self.status_var.set(f"Uninstalling {pkg['name']}...")
        self.root.config(cursor="watch")
        
        try:
            success, message = self.global_pip.uninstall_package(pkg['name'])
            
            if success:
                messagebox.showinfo("Success", f"Package {pkg['name']} uninstalled successfully")
                self.load_global_packages()
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to uninstall {pkg['name']}: {e}")
            messagebox.showerror("Error", f"Failed to uninstall:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def package_info(self):
        """Показать информацию о пакете"""
        pkg = self._get_selected_global_package()
        if not pkg:
            return
        
        self.status_var.set(f"Getting info for {pkg['name']}...")
        
        try:
            info = self.global_pip.show_package(pkg['name'])
            
            if not info:
                messagebox.showinfo("Info", f"No information available for {pkg['name']}")
                return
            
            info_window = tk.Toplevel(self.root)
            info_window.title(f"Package Info: {pkg['name']}")
            info_window.geometry("600x500")
            info_window.resizable(True, True)
            info_window.minsize(500, 400)
            
            text = scrolledtext.ScrolledText(
                info_window,
                wrap=tk.WORD,
                font=('Courier', 10)
            )
            text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
            
            output = f"📦 Package: {pkg['name']}\n"
            output += f"{'='*50}\n\n"
            
            for key, value in info.items():
                output += f"{key.upper()}: {value}\n"
            
            text.insert('1.0', output)
            text.config(state=tk.DISABLED)
            
            self.status_var.set(f"Info displayed for {pkg['name']}")
            
        except Exception as e:
            logger.error(f"Failed to get info for {pkg['name']}: {e}")
            messagebox.showerror("Error", f"Failed to get info:\n\n{str(e)}")
            self.status_var.set("Error")
    
    @safe_execute
    def install_package_dialog(self):
        """Диалог установки пакета"""
        dialog = tk.Toplevel(self.root)
        dialog.title("Install Python Package")
        dialog.geometry("450x200")
        dialog.resizable(True, True)
        dialog.minsize(400, 180)
        dialog.transient(self.root)
        dialog.grab_set()
        
        main_frame = ttk.Frame(dialog)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
        
        # Имя пакета
        ttk.Label(main_frame, text="Package Name:", font=('Arial', 10)).pack(anchor=tk.W, pady=(0, 5))
        name_entry = ttk.Entry(main_frame, width=40)
        name_entry.pack(fill=tk.X, pady=(0, 10))
        
        # Версия
        ttk.Label(main_frame, text="Version (optional, e.g. ==1.2.3):", font=('Arial', 10)).pack(anchor=tk.W, pady=(0, 5))
        version_entry = ttk.Entry(main_frame, width=40)
        version_entry.pack(fill=tk.X, pady=(0, 10))
        
        # Предупреждение о PEP 668
        warning_label = ttk.Label(
            main_frame,
            text="⚠️ Global install will bypass system protection (PEP 668)",
            font=('Arial', 9),
            foreground='orange'
        )
        warning_label.pack(anchor=tk.W, pady=(5, 0))
        
        def install():
            pkg_name = name_entry.get().strip()
            if not pkg_name:
                messagebox.showwarning("Warning", "Package name is required")
                return
            
            if not validate_package_name(pkg_name):
                messagebox.showwarning(
                    "Warning",
                    "Invalid package name. Only letters, numbers, hyphens, underscores, and dots are allowed."
                )
                return
            
            version = version_entry.get().strip()
            if version and not validate_version(version):
                messagebox.showwarning(
                    "Warning",
                    "Invalid version format. Use: ==1.2.3, >=1.0, etc."
                )
                return
            
            full_pkg = f"{pkg_name}{version}" if version else pkg_name
            
            if not messagebox.askyesno(
                "Install Package",
                f"Install {full_pkg} globally?\n\n⚠️ This will bypass system protection (PEP 668)"
            ):
                return
            
            dialog.destroy()
            self.status_var.set(f"Installing {full_pkg}...")
            self.root.config(cursor="watch")
            
            try:
                success, message = self.global_pip.install_package(
                    full_pkg,
                    global_install=True
                )
                
                if success:
                    messagebox.showinfo("Success", f"Package {full_pkg} installed successfully")
                    self.load_global_packages()
                else:
                    raise PipError(message)
                    
            except Exception as e:
                logger.error(f"Failed to install {full_pkg}: {e}")
                messagebox.showerror("Error", f"Failed to install:\n\n{str(e)}")
                self.status_var.set("Error")
            finally:
                self.root.config(cursor="")
        
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=(15, 0))
        ttk.Button(btn_frame, text="Install", command=install).pack(side=tk.RIGHT, padx=5)
        ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
        
        name_entry.focus()
        name_entry.bind('<Return>', lambda e: install())
    
    @safe_execute
    def search_pypi(self):
        """Поиск на PyPI с безопасным запросом"""
        dialog = tk.Toplevel(self.root)
        dialog.title("Search PyPI")
        dialog.geometry("700x600")
        dialog.resizable(True, True)
        dialog.minsize(600, 500)
        dialog.transient(self.root)
        
        main_frame = ttk.Frame(dialog)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        ttk.Label(
            main_frame,
            text="Search for packages on PyPI:",
            font=('Arial', 10, 'bold')
        ).pack(anchor=tk.W, pady=(0, 5))
        
        search_frame = ttk.Frame(main_frame)
        search_frame.pack(fill=tk.X, pady=(0, 10))
        
        search_var = tk.StringVar()
        search_entry = ttk.Entry(search_frame, textvariable=search_var, width=40)
        search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        
        result_text = scrolledtext.ScrolledText(
            main_frame,
            wrap=tk.WORD,
            font=('Courier', 10),
            height=20
        )
        result_text.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
        
        def do_search():
            query = search_var.get().strip()
            if not query:
                messagebox.showwarning("Warning", "Enter search query")
                return
            
            if not validate_package_name(query):
                messagebox.showwarning(
                    "Warning",
                    "Invalid package name format"
                )
                return
            
            result_text.delete('1.0', tk.END)
            result_text.insert('1.0', f"🔍 Searching for '{query}'...\n\n")
            result_text.update()
            dialog.config(cursor="watch")
            
            try:
                data = PyPISearch.search_package(query)
                result_text.delete('1.0', tk.END)
                
                info = data.get('info', {})
                output = f"📦 Package: {info.get('name', 'N/A')}\n"
                output += f"{'='*50}\n\n"
                output += f"📌 Version: {info.get('version', 'N/A')}\n"
                output += f"👤 Author: {info.get('author', 'N/A')}\n"
                output += f"📋 License: {info.get('license', 'N/A')}\n"
                output += f"⭐ Downloads: {info.get('downloads', {}).get('last_month', 'N/A')}\n"
                output += f"🔗 Homepage: {info.get('home_page', 'N/A')}\n"
                output += f"📧 Email: {info.get('author_email', 'N/A')}\n"
                output += f"\n📝 Description:\n{'-'*50}\n"
                desc = info.get('description', 'No description available')
                output += f"{desc[:1500]}"
                if len(desc) > 1500:
                    output += "\n\n... (truncated)"
                output += f"\n\n🔗 PyPI URL: {info.get('package_url', 'N/A')}"
                
                result_text.insert('1.0', output)
                result_text.config(state=tk.DISABLED)
                
                # Кнопка установки
                btn_frame = ttk.Frame(main_frame)
                btn_frame.pack(fill=tk.X, pady=5)
                ttk.Button(
                    btn_frame,
                    text=f"📦 Install {info.get('name', 'Package')}",
                    command=lambda: self._install_from_search(dialog, info.get('name'))
                ).pack(side=tk.LEFT, padx=5)
                
            except PackageNotFoundError as e:
                result_text.delete('1.0', tk.END)
                result_text.insert('1.0', f"❌ {str(e)}")
            except requests.RequestException as e:
                result_text.delete('1.0', tk.END)
                result_text.insert('1.0', f"❌ Network error: {str(e)}")
            except Exception as e:
                logger.error(f"Search error: {e}")
                result_text.delete('1.0', tk.END)
                result_text.insert('1.0', f"❌ Error: {str(e)}")
            finally:
                dialog.config(cursor="")
        
        ttk.Button(main_frame, text="🔍 Search", command=do_search).pack(pady=(0, 10))
        search_entry.bind('<Return>', lambda e: do_search())
        search_entry.focus()
    
    def _install_from_search(self, dialog: tk.Toplevel, package_name: str):
        """Установить пакет из поиска"""
        dialog.destroy()
        
        if not package_name:
            return
        
        if not messagebox.askyesno("Install Package", f"Install {package_name} globally?"):
            return
        
        self.status_var.set(f"Installing {package_name}...")
        self.root.config(cursor="watch")
        
        try:
            success, message = self.global_pip.install_package(
                package_name,
                global_install=True
            )
            
            if success:
                messagebox.showinfo("Success", f"Package {package_name} installed successfully")
                self.load_global_packages()
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to install {package_name}: {e}")
            messagebox.showerror("Error", f"Failed to install:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def export_requirements(self):
        """Экспорт зависимостей с безопасной записью"""
        if not messagebox.askyesno("Export", "Export global packages to requirements.txt?"):
            return
        
        file_path = filedialog.asksaveasfilename(
            title="Save requirements.txt",
            defaultextension=".txt",
            filetypes=[("Requirements files", "*.txt"), ("All files", "*.*")],
            initialfile=f"requirements_{datetime.now().strftime('%Y%m%d')}.txt"
        )
        
        if not file_path:
            return
        
        self.status_var.set(f"Exporting...")
        self.root.config(cursor="watch")
        
        try:
            content = self.global_pip.list_packages('freeze')
            if not content:
                raise Exception("No packages to export")
            
            # Безопасная запись с проверкой пути
            file_path = Path(file_path)
            if not file_path.parent.exists():
                raise Exception(f"Directory does not exist: {file_path.parent}")
            
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            messagebox.showinfo("Success", f"Requirements exported to:\n\n{file_path}")
            self.status_var.set(f"Exported to {file_path}")
            logger.info(f"Exported requirements to {file_path}")
            
        except Exception as e:
            logger.error(f"Failed to export: {e}")
            messagebox.showerror("Error", f"Failed to export:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def install_from_requirements(self):
        """Установка из requirements.txt с прогрессом"""
        file_path = filedialog.askopenfilename(
            title="Select requirements.txt",
            filetypes=[("Requirements files", "*.txt"), ("All files", "*.*")]
        )
        
        if not file_path:
            return
        
        if not os.path.exists(file_path):
            messagebox.showerror("Error", "File not found")
            return
        
        if not messagebox.askyesno(
            "Install",
            f"Install packages from:\n\n{os.path.basename(file_path)}\n\n"
            f"⚠️ This will bypass system protection (PEP 668)"
        ):
            return
        
        self.status_var.set(f"Installing from {os.path.basename(file_path)}...")
        self.root.config(cursor="watch")
        
        try:
            # Создаем контекст прогресса
            self._progress_context = ProgressContext()
            
            success, message = self.global_pip.install_from_requirements(
                file_path,
                global_install=True,
                progress_context=self._progress_context
            )
            
            if success:
                messagebox.showinfo("Success", "All packages installed successfully")
                self.load_global_packages()
                self.status_var.set("Installation completed")
            else:
                raise PipError(message)
                
        except OperationCancelledError:
            self.status_var.set("Installation cancelled")
            messagebox.showinfo("Info", "Installation was cancelled")
        except Exception as e:
            logger.error(f"Failed to install from requirements: {e}")
            messagebox.showerror("Error", f"Installation failed:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
            self._progress_context = None
    
    @safe_execute
    def check_updates(self):
        """Проверка обновлений с возможностью массового обновления"""
        self.status_var.set("Checking for updates...")
        self.root.config(cursor="watch")
        
        try:
            outdated = self.global_pip.get_outdated()
            
            if not outdated:
                messagebox.showinfo("Info", "✅ All packages are up to date!")
                self.status_var.set("All packages up to date")
                return
            
            # Диалог с устаревшими пакетами
            update_dialog = tk.Toplevel(self.root)
            update_dialog.title("Outdated Packages")
            update_dialog.geometry("650x500")
            update_dialog.resizable(True, True)
            update_dialog.minsize(500, 400)
            update_dialog.transient(self.root)
            
            info_frame = ttk.Frame(update_dialog)
            info_frame.pack(fill=tk.X, padx=10, pady=5)
            ttk.Label(
                info_frame,
                text=f"📦 Found {len(outdated)} outdated packages",
                font=('Arial', 10, 'bold')
            ).pack(side=tk.LEFT)
            
            text = scrolledtext.ScrolledText(
                update_dialog,
                wrap=tk.WORD,
                font=('Courier', 10),
                height=15
            )
            text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
            
            text.insert('1.0', f"Outdated packages ({len(outdated)}):\n\n")
            for pkg in outdated:
                text.insert('end',
                    f"📦 {pkg['name']}\n"
                    f"   Current: {pkg['version']}\n"
                    f"   Latest:  {pkg['latest_version']}\n\n"
                )
            
            text.config(state=tk.DISABLED)
            
            # Прогресс
            progress_label = ttk.Label(update_dialog, text="")
            progress_label.pack(pady=5)
            
            progress_bar = ttk.Progressbar(
                update_dialog,
                maximum=len(outdated),
                length=400
            )
            progress_bar.pack(pady=5)
            
            # Флаг отмены
            cancelled = False
            
            def cancel_updates():
                nonlocal cancelled
                cancelled = True
                progress_label.config(text="Cancelling...")
            
            def update_all():
                nonlocal cancelled
                update_dialog.config(cursor="watch")
                text.config(state=tk.NORMAL)
                text.insert('end', "\n\n🔄 Updating all packages...\n")
                text.see(tk.END)
                text.update()
                
                updated = 0
                failed = 0
                
                for i, pkg in enumerate(outdated):
                    if cancelled:
                        text.insert('end', "\n⚠️ Update cancelled by user\n")
                        break
                    
                    progress_bar['value'] = i + 1
                    progress_label.config(text=f"Updating: {pkg['name']}")
                    update_dialog.update()
                    
                    try:
                        success, _ = self.global_pip.install_package(
                            pkg['name'],
                            upgrade=True,
                            global_install=True
                        )
                        
                        if success:
                            text.insert('end', f"✅ Updated {pkg['name']}\n")
                            updated += 1
                            logger.info(f"Updated {pkg['name']}")
                        else:
                            text.insert('end', f"❌ Failed to update {pkg['name']}\n")
                            failed += 1
                    except Exception as e:
                        text.insert('end', f"❌ Failed to update {pkg['name']}: {str(e)}\n")
                        failed += 1
                    
                    text.see(tk.END)
                    text.update()
                
                text.insert('end', f"\n{'='*50}\n")
                text.insert('end', f"✅ Updated: {updated}\n")
                text.insert('end', f"❌ Failed: {failed}\n")
                if cancelled:
                    text.insert('end', "⚠️ Cancelled by user\n")
                text.insert('end', "Update process completed!")
                text.config(state=tk.DISABLED)
                progress_label.config(text="Complete!")
                
                messagebox.showinfo(
                    "Update Complete",
                    f"Updated: {updated}\nFailed: {failed}\n\nClick OK to refresh"
                )
                update_dialog.destroy()
                self.load_global_packages()
                update_dialog.config(cursor="")
            
            btn_frame = ttk.Frame(update_dialog)
            btn_frame.pack(fill=tk.X, padx=10, pady=10)
            
            ttk.Button(
                btn_frame,
                text="🔄 Update All",
                command=update_all
            ).pack(side=tk.RIGHT, padx=5)
            
            ttk.Button(
                btn_frame,
                text="⏹ Cancel",
                command=cancel_updates
            ).pack(side=tk.RIGHT, padx=5)
            
            ttk.Button(
                btn_frame,
                text="Close",
                command=update_dialog.destroy
            ).pack(side=tk.RIGHT, padx=5)
            
            self.status_var.set(f"Found {len(outdated)} outdated packages")
            
        except Exception as e:
            logger.error(f"Failed to check updates: {e}")
            messagebox.showerror("Error", f"Failed to check updates:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def show_global_stats(self):
        """Показать статистику"""
        packages = self.global_pip.list_packages('json')
        if not packages:
            messagebox.showinfo("Info", "No packages data available")
            return
        
        stats_window = tk.Toplevel(self.root)
        stats_window.title("Global Packages Statistics")
        stats_window.geometry("500x400")
        stats_window.resizable(True, True)
        stats_window.minsize(400, 300)
        stats_window.transient(self.root)
        
        text = scrolledtext.ScrolledText(
            stats_window,
            wrap=tk.WORD,
            font=('Courier', 10)
        )
        text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        output = f"📊 Global Packages Statistics\n"
        output += f"{'='*50}\n\n"
        output += f"Total packages: {len(packages)}\n"
        output += f"Python version: {self.global_pip.get_version()}\n\n"
        
        # Дополнительная статистика
        text.insert('1.0', output)
        text.config(state=tk.DISABLED)
    
    # ==================== ВКЛАДКА ВИРТУАЛЬНЫХ ОКРУЖЕНИЙ ====================
    def _create_venv_tab(self):
        """Создание вкладки виртуальных окружений"""
        self.venv_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.venv_frame, text="🐍 Virtual Environments")
        
        # Верхняя панель
        top_frame = ttk.Frame(self.venv_frame)
        top_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Label(
            top_frame,
            text="Virtual Environments in ~/",
            font=('Arial', 11, 'bold')
        ).pack(side=tk.LEFT)
        
        btn_frame = ttk.Frame(top_frame)
        btn_frame.pack(side=tk.RIGHT)
        
        venv_buttons = [
            ("🔄 Rescan", self.scan_venv),
            ("➕ Create", self.create_venv_dialog),
            ("📋 Clone", self.clone_venv),
            ("📊 Stats", self._show_venv_stats)
        ]
        
        for text, command in venv_buttons:
            ttk.Button(btn_frame, text=text, command=command).pack(side=tk.LEFT, padx=2)
        
        # Поиск
        self.venv_search_var = self._create_search_bar(
            self.venv_frame,
            self._filter_venv
        )
        
        # Дерево
        self._create_venv_tree()
    
    def _create_venv_tree(self):
        """Создание дерева виртуальных окружений"""
        tree_frame = ttk.Frame(self.venv_frame)
        tree_frame.pack(fill=tk.BOTH, expand=True)
        
        columns = ('Path', 'Python', 'Packages')
        self.venv_tree = ttk.Treeview(
            tree_frame,
            columns=columns,
            show='tree headings',
            height=15
        )
        
        self.venv_tree.heading('#0', text='#')
        self.venv_tree.heading('Path', text='Environment Path')
        self.venv_tree.heading('Python', text='Python Version')
        self.venv_tree.heading('Packages', text='Packages')
        
        self.venv_tree.column('#0', width=40)
        self.venv_tree.column('Path', width=350)
        self.venv_tree.column('Python', width=150)
        self.venv_tree.column('Packages', width=80)
        
        scrollbar = ttk.Scrollbar(
            tree_frame,
            orient=tk.VERTICAL,
            command=self.venv_tree.yview
        )
        self.venv_tree.configure(yscrollcommand=scrollbar.set)
        
        self.venv_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Контекстное меню
        self.venv_context_menu = tk.Menu(self.root, tearoff=0)
        self.venv_context_menu.add_command(
            label="📦 Show Packages",
            command=self.show_venv_packages
        )
        self.venv_context_menu.add_command(
            label="🔄 Update All",
            command=self.update_venv_all
        )
        self.venv_context_menu.add_command(
            label="📤 Export Requirements",
            command=self.export_venv_requirements
        )
        self.venv_context_menu.add_command(
            label="📥 Install from file",
            command=self.install_venv_requirements
        )
        self.venv_context_menu.add_separator()
        self.venv_context_menu.add_command(
            label="🗑️ Delete Environment",
            command=self.delete_venv
        )
        self.venv_context_menu.add_command(
            label="🔍 Open in Explorer",
            command=self._open_venv_explorer
        )
        
        self.venv_tree.bind("<Button-3>", self._show_venv_context_menu)
        self.venv_tree.bind("<Double-1>", lambda e: self.show_venv_packages())
    
    def _filter_venv(self):
        """Фильтрация виртуальных окружений"""
        query = self.venv_search_var.get().lower().strip()
        
        for item in self.venv_tree.get_children():
            values = self.venv_tree.item(item)['values']
            if not query:
                self.venv_tree.reattach(item, '', 'end')
            elif values and query in values[0].lower():
                self.venv_tree.reattach(item, '', 'end')
            else:
                self.venv_tree.detach(item)
    
    def _show_venv_context_menu(self, event):
        """Показать контекстное меню для venv"""
        item = self.venv_tree.identify_row(event.y)
        if item:
            self.venv_tree.selection_set(item)
            values = self.venv_tree.item(item)['values']
            if values:
                self.selected_venv_path = values[0]
                self.venv_context_menu.post(event.x_root, event.y_root)
    
    def _open_venv_explorer(self):
        """Открыть папку venv в проводнике"""
        if self.selected_venv_path:
            import subprocess
            import platform
            
            path = self.selected_venv_path
            if platform.system() == 'Windows':
                subprocess.run(['explorer', path])
            elif platform.system() == 'Darwin':
                subprocess.run(['open', path])
            else:
                subprocess.run(['xdg-open', path])
            
            self.status_var.set(f"Opened: {path}")
    
    @safe_execute
    def scan_venv(self):
        """Сканирование виртуальных окружений"""
        self.status_var.set("Scanning for virtual environments...")
        self.root.config(cursor="watch")
        
        try:
            # Очистка
            for item in self.venv_tree.get_children():
                self.venv_tree.delete(item)
            
            home = Path.home()
            venv_names = ['venv', '.venv', 'env', '.env', 'virtualenv']
            found = 0
            
            # Общие пути
            common_paths = [
                home / '.virtualenvs',
                home / 'venvs',
                home / '.venv',
                home / 'envs',
                home / 'Envs',
                home / 'VirtualEnvs'
            ]
            
            for path in common_paths:
                if path.exists() and path.is_dir():
                    for venv in path.iterdir():
                        if venv.is_dir():
                            if self._add_venv_to_tree(venv):
                                found += 1
            
            # Поиск в домашней директории
            for root_dir in home.iterdir():
                if root_dir.is_dir() and not root_dir.name.startswith('.'):
                    for venv_name in venv_names:
                        venv_path = root_dir / venv_name
                        if venv_path.exists() and venv_path.is_dir():
                            if self._add_venv_to_tree(venv_path):
                                found += 1
            
            self.status_var.set(f"Found {found} virtual environments")
            logger.info(f"Found {found} virtual environments")
            
        except Exception as e:
            logger.error(f"Error scanning venvs: {e}")
            messagebox.showerror("Error", f"Failed to scan venvs:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    def _add_venv_to_tree(self, venv_path: Path) -> bool:
        """Добавить venv в дерево"""
        try:
            python_path = get_python_executable(venv_path)
            if not python_path:
                return False
            
            # Версия Python
            result = subprocess.run(
                [str(python_path), '--version'],
                capture_output=True,
                text=True,
                timeout=3
            )
            python_ver = result.stdout.strip() or result.stderr.strip()
            
            # Количество пакетов
            try:
                pip_manager = SecurePipManager(python_path)
                packages = pip_manager.list_packages('json')
                pkg_count = len(packages) if isinstance(packages, list) else 0
            except:
                pkg_count = 0
            
            # Проверка дубликатов
            for item in self.venv_tree.get_children():
                values = self.venv_tree.item(item)['values']
                if values and values[0] == str(venv_path):
                    return False
            
            self.venv_tree.insert(
                '',
                'end',
                values=(
                    str(venv_path),
                    python_ver,
                    str(pkg_count) if pkg_count > 0 else '0'
                ),
                text=str(len(self.venv_tree.get_children()) + 1)
            )
            return True
            
        except Exception as e:
            logger.debug(f"Failed to add venv {venv_path}: {e}")
            return False
    
    @safe_execute
    def show_venv_packages(self):
        """Показать пакеты в виртуальном окружении"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        venv_path = Path(self.selected_venv_path)
        python_path = get_python_executable(venv_path)
        
        if not python_path:
            messagebox.showerror("Error", f"Python not found in {self.selected_venv_path}")
            return
        
        self.status_var.set(f"Loading packages from {self.selected_venv_path}...")
        self.root.config(cursor="watch")
        
        try:
            pip_manager = SecurePipManager(python_path)
            packages = pip_manager.list_packages('json')
            
            if not packages:
                messagebox.showinfo("Info", "No packages found")
                return
            
            # Окно с пакетами
            pkg_window = tk.Toplevel(self.root)
            pkg_window.title(f"Packages in: {os.path.basename(self.selected_venv_path)}")
            pkg_window.geometry("750x550")
            pkg_window.resizable(True, True)
            pkg_window.minsize(600, 400)
            pkg_window.transient(self.root)
            
            # Верхняя панель
            top_frame = ttk.Frame(pkg_window)
            top_frame.pack(fill=tk.X, padx=5, pady=5)
            
            ttk.Label(
                top_frame,
                text=f"📦 Packages: {len(packages)}",
                font=('Arial', 10, 'bold')
            ).pack(side=tk.LEFT)
            
            btn_frame = ttk.Frame(top_frame)
            btn_frame.pack(side=tk.RIGHT)
            
            ttk.Button(
                btn_frame,
                text="🔄 Update All",
                command=lambda: self._update_venv_all(pkg_window, str(python_path))
            ).pack(side=tk.LEFT, padx=2)
            
            ttk.Button(
                btn_frame,
                text="📤 Export",
                command=lambda: self._export_venv_requirements(str(python_path))
            ).pack(side=tk.LEFT, padx=2)
            
            ttk.Button(
                btn_frame,
                text="📥 Import",
                command=lambda: self._install_venv_requirements(str(python_path))
            ).pack(side=tk.LEFT, padx=2)
            
            # Поиск
            search_frame = ttk.Frame(pkg_window)
            search_frame.pack(fill=tk.X, padx=5, pady=5)
            
            ttk.Label(search_frame, text="🔍 Search:").pack(side=tk.LEFT, padx=5)
            search_var = tk.StringVar()
            search_entry = ttk.Entry(search_frame, textvariable=search_var)
            search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
            
            # Дерево
            tree_frame = ttk.Frame(pkg_window)
            tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
            
            columns = ('Package', 'Version')
            tree = ttk.Treeview(
                tree_frame,
                columns=columns,
                show='tree headings',
                height=20
            )
            
            tree.heading('#0', text='#')
            tree.heading('Package', text='Package Name')
            tree.heading('Version', text='Version')
            
            tree.column('#0', width=40)
            tree.column('Package', width=350)
            tree.column('Version', width=150)
            
            scrollbar = ttk.Scrollbar(
                tree_frame,
                orient=tk.VERTICAL,
                command=tree.yview
            )
            tree.configure(yscrollcommand=scrollbar.set)
            
            tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
            scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
            
            for i, pkg in enumerate(packages, 1):
                tree.insert('', 'end', values=(pkg['name'], pkg['version']), text=str(i))
            
            # Контекстное меню
            context_menu = tk.Menu(pkg_window, tearoff=0)
            context_menu.add_command(
                label="🔄 Update",
                command=lambda: self._update_venv_package(tree, str(python_path))
            )
            context_menu.add_command(
                label="🗑️ Uninstall",
                command=lambda: self._uninstall_venv_package(tree, str(python_path))
            )
            
            tree.bind(
                "<Button-3>",
                lambda e, m=context_menu, t=tree: self._show_venv_pkg_menu(e, m, t)
            )
            
            # Фильтрация
            def filter_packages():
                query = search_var.get().lower().strip()
                for item in tree.get_children():
                    values = tree.item(item)['values']
                    if not query:
                        tree.reattach(item, '', 'end')
                    elif values and query in values[0].lower():
                        tree.reattach(item, '', 'end')
                    else:
                        tree.detach(item)
            
            search_var.trace('w', lambda *args: filter_packages())
            ttk.Button(
                search_frame,
                text="Clear",
                command=lambda: (search_var.set(""), filter_packages())
            ).pack(side=tk.RIGHT, padx=5)
            
            self.status_var.set(f"Loaded {len(packages)} packages from venv")
            
        except Exception as e:
            logger.error(f"Failed to load venv packages: {e}")
            messagebox.showerror("Error", f"Failed to load packages:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    def _show_venv_pkg_menu(self, event, menu, tree):
        """Показать контекстное меню для пакета в venv"""
        item = tree.identify_row(event.y)
        if item:
            tree.selection_set(item)
            menu.post(event.x_root, event.y_root)
    
    def _get_venv_package(self, tree) -> Optional[Dict[str, str]]:
        """Получить выбранный пакет из venv"""
        selection = tree.selection()
        if not selection:
            messagebox.showwarning("Warning", "Please select a package")
            return None
        
        values = tree.item(selection[0])['values']
        if not values:
            return None
        
        return {'name': values[0], 'version': values[1]}
    
    @safe_execute
    def _update_venv_package(self, tree, python_bin):
        """Обновить пакет в venv"""
        pkg = self._get_venv_package(tree)
        if not pkg:
            return
        
        if not messagebox.askyesno("Update Package", f"Update {pkg['name']} in venv?"):
            return
        
        self.status_var.set(f"Updating {pkg['name']} in venv...")
        
        try:
            pip_manager = SecurePipManager(python_bin)
            success, message = pip_manager.install_package(pkg['name'], upgrade=True)
            
            if success:
                messagebox.showinfo("Success", f"Package {pkg['name']} updated successfully")
                self.show_venv_packages()
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to update {pkg['name']}: {e}")
            messagebox.showerror("Error", f"Failed to update:\n\n{str(e)}")
            self.status_var.set("Error")
    
    @safe_execute
    def _uninstall_venv_package(self, tree, python_bin):
        """Удалить пакет из venv"""
        pkg = self._get_venv_package(tree)
        if not pkg:
            return
        
        if not messagebox.askyesno("Uninstall Package", f"Uninstall {pkg['name']} from venv?"):
            return
        
        self.status_var.set(f"Uninstalling {pkg['name']} from venv...")
        
        try:
            pip_manager = SecurePipManager(python_bin)
            success, message = pip_manager.uninstall_package(pkg['name'])
            
            if success:
                messagebox.showinfo("Success", f"Package {pkg['name']} uninstalled successfully")
                self.show_venv_packages()
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to uninstall {pkg['name']}: {e}")
            messagebox.showerror("Error", f"Failed to uninstall:\n\n{str(e)}")
            self.status_var.set("Error")
    
    @safe_execute
    def _update_venv_all(self, parent_window, python_bin):
        """Обновить все пакеты в venv"""
        if not messagebox.askyesno("Update All", "Update all packages in this virtual environment?"):
            return
        
        self.status_var.set("Updating all packages in venv...")
        
        try:
            pip_manager = SecurePipManager(python_bin)
            outdated = pip_manager.get_outdated()
            
            if not outdated:
                messagebox.showinfo("Info", "All packages are up to date")
                self.status_var.set("All packages up to date")
                return
            
            # Прогресс
            dialog, progress_bar, update_progress, get_cancelled = self._show_progress_dialog(
                "Updating Packages",
                len(outdated)
            )
            
            updated = 0
            failed = 0
            cancelled = False
            
            for i, pkg in enumerate(outdated):
                if get_cancelled():
                    cancelled = True
                    break
                
                update_progress(i + 1, f"Updating: {pkg['name']}")
                
                try:
                    success, _ = pip_manager.install_package(pkg['name'], upgrade=True)
                    if success:
                        updated += 1
                    else:
                        failed += 1
                except:
                    failed += 1
            
            self._close_progress_dialog(dialog)
            
            if cancelled:
                messagebox.showinfo("Info", "Update cancelled")
            else:
                messagebox.showinfo(
                    "Update Complete",
                    f"Updated: {updated}\nFailed: {failed}"
                )
            
            self.status_var.set(f"Updated {updated} packages")
            parent_window.destroy()
            self.show_venv_packages()
            
        except Exception as e:
            logger.error(f"Failed to update packages: {e}")
            messagebox.showerror("Error", f"Failed to update packages:\n\n{str(e)}")
            self.status_var.set("Error")
    
    @safe_execute
    def update_venv_all(self):
        """Обновить все пакеты в выбранном venv"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        venv_path = Path(self.selected_venv_path)
        python_path = get_python_executable(venv_path)
        
        if not python_path:
            messagebox.showerror("Error", f"Python not found in {self.selected_venv_path}")
            return
        
        self.show_venv_packages()
    
    @safe_execute
    def export_venv_requirements(self):
        """Экспорт зависимостей из venv"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        venv_path = Path(self.selected_venv_path)
        python_path = get_python_executable(venv_path)
        
        if not python_path:
            messagebox.showerror("Error", f"Python not found in {self.selected_venv_path}")
            return
        
        self._export_venv_requirements(str(python_path))
    
    @safe_execute
    def _export_venv_requirements(self, python_bin):
        """Внутренний метод экспорта"""
        file_path = filedialog.asksaveasfilename(
            title="Save requirements.txt",
            defaultextension=".txt",
            filetypes=[("Requirements files", "*.txt"), ("All files", "*.*")],
            initialfile=f"requirements_venv_{datetime.now().strftime('%Y%m%d')}.txt"
        )
        
        if not file_path:
            return
        
        try:
            pip_manager = SecurePipManager(python_bin)
            content = pip_manager.list_packages('freeze')
            
            if not content:
                raise Exception("No packages to export")
            
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            messagebox.showinfo("Success", f"Requirements exported to:\n\n{file_path}")
            self.status_var.set(f"Exported to {file_path}")
            logger.info(f"Exported venv requirements to {file_path}")
            
        except Exception as e:
            logger.error(f"Failed to export: {e}")
            messagebox.showerror("Error", f"Failed to export:\n\n{str(e)}")
            self.status_var.set("Error")
    
    @safe_execute
    def install_venv_requirements(self):
        """Установка зависимостей в venv"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        venv_path = Path(self.selected_venv_path)
        python_path = get_python_executable(venv_path)
        
        if not python_path:
            messagebox.showerror("Error", f"Python not found in {self.selected_venv_path}")
            return
        
        self._install_venv_requirements(str(python_path))
    
    @safe_execute
    def _install_venv_requirements(self, python_bin):
        """Внутренний метод установки зависимостей"""
        file_path = filedialog.askopenfilename(
            title="Select requirements.txt",
            filetypes=[("Requirements files", "*.txt"), ("All files", "*.*")]
        )
        
        if not file_path:
            return
        
        if not os.path.exists(file_path):
            messagebox.showerror("Error", "File not found")
            return
        
        if not messagebox.askyesno(
            "Install",
            f"Install packages from:\n\n{os.path.basename(file_path)}\ninto venv?"
        ):
            return
        
        self.status_var.set(f"Installing from {os.path.basename(file_path)}...")
        self.root.config(cursor="watch")
        
        try:
            pip_manager = SecurePipManager(python_bin)
            success, message = pip_manager.install_from_requirements(file_path)
            
            if success:
                messagebox.showinfo("Success", "Packages installed successfully")
                self.show_venv_packages()
                self.status_var.set("Installation completed")
            else:
                raise PipError(message)
                
        except Exception as e:
            logger.error(f"Failed to install from requirements: {e}")
            messagebox.showerror("Error", f"Installation failed:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def delete_venv(self):
        """Безопасное удаление виртуального окружения"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        if not messagebox.askyesno(
            "Delete Venv",
            f"Are you sure you want to delete:\n\n{self.selected_venv_path}\n\n"
            f"This action cannot be undone!"
        ):
            return
        
        self.status_var.set(f"Deleting {self.selected_venv_path}...")
        self.root.config(cursor="watch")
        
        try:
            # Проверка, что путь не корневой
            venv_path = Path(self.selected_venv_path)
            if str(venv_path) in ['/', 'C:\\', '/home', '/Users']:
                raise VenvError("Cannot delete system directory")
            
            shutil.rmtree(venv_path)
            messagebox.showinfo("Success", "Virtual environment deleted successfully")
            self.status_var.set("Deleted venv")
            self.scan_venv()
            logger.info(f"Deleted venv: {self.selected_venv_path}")
            
        except Exception as e:
            logger.error(f"Failed to delete venv: {e}")
            messagebox.showerror("Error", f"Failed to delete venv:\n\n{str(e)}")
            self.status_var.set("Error")
        finally:
            self.root.config(cursor="")
    
    @safe_execute
    def clone_venv(self):
        """Клонирование виртуального окружения"""
        if not self.selected_venv_path:
            messagebox.showwarning("Warning", "Please select a virtual environment")
            return
        
        src_venv = Path(self.selected_venv_path)
        src_python = get_python_executable(src_venv)
        
        if not src_python:
            messagebox.showerror("Error", "Source venv Python not found")
            return
        
        dialog = tk.Toplevel(self.root)
        dialog.title("Clone Virtual Environment")
        dialog.geometry("500x200")
        dialog.resizable(True, True)
        dialog.minsize(400, 180)
        dialog.transient(self.root)
        dialog.grab_set()
        
        main_frame = ttk.Frame(dialog)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
        
        ttk.Label(
            main_frame,
            text="New environment name:",
            font=('Arial', 10)
        ).pack(anchor=tk.W, pady=(0, 5))
        
        name_var = tk.StringVar(value=f"{src_venv.name}_clone")
        name_entry = ttk.Entry(main_frame, textvariable=name_var, width=40)
        name_entry.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Label(
            main_frame,
            text=f"Source: {src_venv.name}",
            font=('Arial', 9)
        ).pack(anchor=tk.W, pady=(0, 10))
        
        # Предупреждение
        ttk.Label(
            main_frame,
            text="⚠️ Cloning may take time depending on package count",
            font=('Arial', 9),
            foreground='orange'
        ).pack(anchor=tk.W)
        
        def do_clone():
            new_name = name_var.get().strip()
            if not new_name:
                messagebox.showwarning("Warning", "Name is required")
                return
            
            if not validate_package_name(new_name):
                messagebox.showwarning(
                    "Warning",
                    "Invalid name. Only letters, numbers, hyphens, underscores, and dots are allowed."
                )
                return
            
            new_path = src_venv.parent / new_name
            if new_path.exists():
                if not messagebox.askyesno("Warning", f"{new_path} already exists. Overwrite?"):
                    return
            
            dialog.destroy()
            self.status_var.set(f"Cloning venv to {new_path}...")
            self.root.config(cursor="watch")
            
            try:
                # Создание нового venv
                subprocess.run(
                    [sys.executable, '-m', 'venv', str(new_path)],
                    capture_output=True,
                    text=True,
                    check=True
                )
                
                # Получение пакетов из исходного
                pip_manager = SecurePipManager(src_python)
                packages = pip_manager.list_packages('freeze')
                
                if packages:
                    # Установка в новый venv
                    new_python = get_python_executable(new_path)
                    if new_python:
                        new_pip = SecurePipManager(new_python)
                        
                        # Использование временного файла с безопасным удалением
                        temp_file = None
                        try:
                            fd, temp_file = tempfile.mkstemp(suffix='.txt', text=True)
                            with os.fdopen(fd, 'w', encoding='utf-8') as f:
                                f.write(packages)
                            
                            success, _ = new_pip.install_from_requirements(temp_file)
                            if not success:
                                raise Exception("Failed to install packages")
                        finally:
                            if temp_file and os.path.exists(temp_file):
                                try:
                                    os.unlink(temp_file)
                                except Exception as e:
                                    logger.warning(f"Failed to delete temp file {temp_file}: {e}")
                
                messagebox.showinfo(
                    "Success",
                    f"Environment cloned to:\n\n{new_path}\n\n"
                    f"Packages installed: {len(packages.splitlines()) if packages else 0}"
                )
                self.status_var.set("Venv cloned")
                self.scan_venv()
                logger.info(f"Cloned venv to {new_path}")
                
            except Exception as e:
                logger.error(f"Failed to clone: {e}")
                messagebox.showerror("Error", f"Failed to clone:\n\n{str(e)}")
                self.status_var.set("Error")
            finally:
                self.root.config(cursor="")
        
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=(10, 0))
        ttk.Button(btn_frame, text="Clone", command=do_clone).pack(side=tk.RIGHT, padx=5)
        ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
        
        name_entry.focus()
        name_entry.select_range(0, tk.END)
    
    @safe_execute
    def create_venv_dialog(self):
        """Диалог создания виртуального окружения"""
        dialog = tk.Toplevel(self.root)
        dialog.title("Create Virtual Environment")
        dialog.geometry("500x300")
        dialog.resizable(True, True)
        dialog.minsize(400, 250)
        dialog.transient(self.root)
        dialog.grab_set()
        
        main_frame = ttk.Frame(dialog)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
        
        # Путь
        ttk.Label(
            main_frame,
            text="Location:",
            font=('Arial', 10)
        ).pack(anchor=tk.W, pady=(0, 5))
        
        path_frame = ttk.Frame(main_frame)
        path_frame.pack(fill=tk.X, pady=(0, 10))
        
        path_var = tk.StringVar(value=str(Path.home() / "venvs"))
        path_entry = ttk.Entry(path_frame, textvariable=path_var, width=40)
        path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        def browse_path():
            path = filedialog.askdirectory(
                title="Select parent directory",
                initialdir=str(Path.home())
            )
            if path:
                path_var.set(path)
        
        ttk.Button(path_frame, text="📁 Browse", command=browse_path).pack(side=tk.RIGHT, padx=5)
        
        # Имя
        ttk.Label(
            main_frame,
            text="Environment name:",
            font=('Arial', 10)
        ).pack(anchor=tk.W, pady=(0, 5))
        
        name_var = tk.StringVar(value="venv")
        name_entry = ttk.Entry(main_frame, textvariable=name_var, width=40)
        name_entry.pack(fill=tk.X, pady=(0, 10))
        
        # Python версия
        ttk.Label(
            main_frame,
            text="Python version (optional, e.g. python3.11):",
            font=('Arial', 10)
        ).pack(anchor=tk.W, pady=(0, 5))
        
        python_var = tk.StringVar()
        python_entry = ttk.Entry(main_frame, textvariable=python_var, width=40)
        python_entry.pack(fill=tk.X, pady=(0, 10))
        
        def create():
            parent_path = path_var.get().strip()
            env_name = name_var.get().strip()
            python_version = python_var.get().strip()
            
            if not env_name:
                messagebox.showwarning("Warning", "Environment name is required")
                return
            
            if not validate_package_name(env_name):
                messagebox.showwarning(
                    "Warning",
                    "Invalid name. Only letters, numbers, hyphens, underscores, and dots are allowed."
                )
                return
            
            venv_path = Path(parent_path) / env_name
            
            if venv_path.exists():
                if not messagebox.askyesno(
                    "Warning",
                    f"Path {venv_path} already exists. Overwrite?"
                ):
                    return
            
            dialog.destroy()
            self.status_var.set(f"Creating virtual environment at {venv_path}...")
            self.root.config(cursor="watch")
            
            try:
                # Создание venv
                cmd = [sys.executable, '-m', 'venv', str(venv_path)]
                if python_version:
                    python_cmd = [python_version, '-m', 'venv', str(venv_path)]
                    try:
                        subprocess.run(
                            python_cmd,
                            capture_output=True,
                            text=True,
                            check=True
                        )
                    except (subprocess.CalledProcessError, FileNotFoundError):
                        # Используем системный Python
                        subprocess.run(cmd, check=True)
                else:
                    subprocess.run(cmd, check=True)
                
                messagebox.showinfo(
                    "Success",
                    f"Virtual environment created at:\n\n{venv_path}"
                )
                self.status_var.set("Venv created")
                self.scan_venv()
                logger.info(f"Created venv at {venv_path}")
                
            except subprocess.CalledProcessError as e:
                logger.error(f"Failed to create venv: {e.stderr}")
                messagebox.showerror(
                    "Error",
                    f"Failed to create venv:\n\n{e.stderr}"
                )
                self.status_var.set("Error")
            except Exception as e:
                logger.error(f"Failed to create venv: {e}")
                messagebox.showerror("Error", f"Failed to create venv:\n\n{str(e)}")
                self.status_var.set("Error")
            finally:
                self.root.config(cursor="")
        
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=(10, 0))
        ttk.Button(btn_frame, text="Create", command=create).pack(side=tk.RIGHT, padx=5)
        ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
        
        name_entry.focus()
    
    @safe_execute
    def _show_venv_stats(self):
        """Показать статистику по виртуальным окружениям"""
        venv_items = self.venv_tree.get_children()
        if not venv_items:
            messagebox.showinfo("Info", "No virtual environments found")
            return
        
        stats_window = tk.Toplevel(self.root)
        stats_window.title("Virtual Environments Statistics")
        stats_window.geometry("600x400")
        stats_window.resizable(True, True)
        stats_window.minsize(500, 300)
        stats_window.transient(self.root)
        
        text = scrolledtext.ScrolledText(
            stats_window,
            wrap=tk.WORD,
            font=('Courier', 10)
        )
        text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        output = f"📊 Virtual Environments Statistics\n"
        output += f"{'='*50}\n\n"
        output += f"Total environments: {len(venv_items)}\n"
        
        for item in venv_items:
            values = self.venv_tree.item(item)['values']
            if values:
                path, python_ver, packages = values
                output += f"\n📁 {os.path.basename(path)}\n"
                output += f"   Path: {path}\n"
                output += f"   Python: {python_ver}\n"
                output += f"   Packages: {packages}\n"
        
        text.insert('1.0', output)
        text.config(state=tk.DISABLED)


# ==================== ЗАПУСК ====================
if __name__ == "__main__":
    try:
        root = tk.Tk()
        app = PythonPackageManager(root)
        root.mainloop()
    except Exception as e:
        logger.critical(f"Fatal error starting application: {e}", exc_info=True)
        messagebox.showerror(
            "Fatal Error",
            f"Failed to start application:\n\n{str(e)}\n\n"
            f"Check package_manager.log for details."
        )
        sys.exit(1)
