#!/usr/bin/env python3
"""
PySide6 Dependencies Analyzer with ldd support
Анализирует зависимости PySide6 модулей используя ldd для определения
реальных зависимостей скомпилированных библиотек.
"""

import ast
import os
import sys
import subprocess
import re
from pathlib import Path
from typing import Set, Dict, List
import argparse
import json


class PySide6DependencyAnalyzer:
    def __init__(self, project_root: Path = None):
        # Системные библиотеки, которые нужно всегда оставлять
        self.system_libs = {
            'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
            'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
            'libQt6Svg'
        }

        self.critical_modules = {
            'QtSvg',
        }

        self.real_dependencies = {}
        self.used_modules_code = set()
        self.used_modules_ldd = set()
        self.all_required_modules = set()
        # Определяем корень проекта
        if project_root is None:
            # Корень проекта - две директории выше от скрипта
            self.project_root = Path(__file__).parent.parent
        else:
            self.project_root = project_root

        self.venv_path = self.project_root / ".venv"
        self.build_path = self.project_root / "build-aux"

    def find_python_files(self, directory: Path) -> List[Path]:
        """Находит все Python файлы в директории"""
        python_files = []
        for root, dirs, files in os.walk(directory):
            dirs[:] = [d for d in dirs if d not in {'.venv', '__pycache__', '.git'}]

            for file in files:
                if file.endswith('.py'):
                    python_files.append(Path(root) / file)
        return python_files

    def find_pyside6_libs(self, base_path: Path) -> Dict[str, Path]:
        """Находит все PySide6 библиотеки (.so файлы)"""
        libs = {}

        # Ищем venv в корне проекта
        venv_candidates = [
            self.venv_path,  # .venv
            self.project_root / "venv",
            self.project_root / ".virtualenv",
        ]

        pyside6_path = None

        # Пробуем найти PySide6 в venv
        for venv in venv_candidates:
            if venv.exists():
                # Ищем Python версию
                lib_path = venv / "lib"
                if lib_path.exists():
                    for python_dir in lib_path.iterdir():
                        if python_dir.name.startswith('python'):
                            candidate = python_dir / "site-packages" / "PySide6"
                            if candidate.exists():
                                pyside6_path = candidate
                                print(f"Найден PySide6 в: {candidate}")
                                break
                    if pyside6_path:
                        break

        if not pyside6_path:
            print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
            # Если не нашли в venv, пробуем в AppDir
            if base_path:
                appdir_candidate = base_path / "AppDir/usr/local/lib"
                if appdir_candidate.exists():
                    for python_dir in appdir_candidate.iterdir():
                        if python_dir.name.startswith('python'):
                            candidate = python_dir / "dist-packages" / "PySide6"
                            if candidate.exists():
                                pyside6_path = candidate
                                print(f"Найден PySide6 в AppDir: {candidate}")
                                break

        if not pyside6_path:
            return libs

        # Ищем .so файлы модулей
        for so_file in pyside6_path.glob("Qt*.*.so"):
            module_name = so_file.stem.split('.')[0]  # QtCore.abi3.so -> QtCore
            if module_name.startswith('Qt'):
                libs[module_name] = so_file

        # Также ищем в подпапках
        for subdir in pyside6_path.iterdir():
            if subdir.is_dir() and subdir.name.startswith('Qt'):
                for so_file in subdir.glob("*.so*"):
                    if 'Qt' in so_file.name:
                        libs[subdir.name] = so_file
                        break

        return libs

    def analyze_ldd_dependencies(self, lib_path: Path) -> Set[str]:
        """Анализирует зависимости библиотеки с помощью ldd"""
        qt_deps = set()

        try:
            result = subprocess.run(['ldd', str(lib_path)],
                                  capture_output=True, text=True, check=True)

            # Парсим вывод ldd и ищем Qt библиотеки
            for line in result.stdout.split('\n'):
                # Ищем строки вида: libQt6Core.so.6 => /path/to/lib
                match = re.search(r'libQt6(\w+)\.so', line)
                if match:
                    qt_module = f"Qt{match.group(1)}"
                    qt_deps.add(qt_module)

        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            print(f"Предупреждение: не удалось выполнить ldd для {lib_path}: {e}")

        return qt_deps

    def build_real_dependency_graph(self, pyside_libs: Dict[str, Path]) -> Dict[str, Set[str]]:
        """Строит граф зависимостей на основе ldd анализа"""
        dependencies = {}

        print("Анализ реальных зависимостей с помощью ldd...")
        for module, lib_path in pyside_libs.items():
            print(f"  Анализируется {module}...")
            deps = self.analyze_ldd_dependencies(lib_path)
            dependencies[module] = deps

            if deps:
                print(f"    Зависимости: {', '.join(sorted(deps))}")

        return dependencies

    def analyze_file_imports(self, file_path: Path) -> Set[str]:
        """Анализирует один Python файл и возвращает используемые PySide6 модули"""
        modules = set()
        try:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()

            tree = ast.parse(content)

            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for alias in node.names:
                        if alias.name.startswith('PySide6.'):
                            module = alias.name.split('.', 2)[1]
                            if module.startswith('Qt'):
                                modules.add(module)

                elif isinstance(node, ast.ImportFrom):
                    if node.module and node.module.startswith('PySide6.'):
                        module = node.module.split('.', 2)[1]
                        if module.startswith('Qt'):
                            modules.add(module)

        except Exception as e:
            print(f"Ошибка при анализе {file_path}: {e}")

        return modules

    def get_all_dependencies(self, modules: Set[str], dependency_graph: Dict[str, Set[str]]) -> Set[str]:
        """Получает все зависимости для набора модулей, используя граф зависимостей из ldd"""
        all_deps = set(modules)

        if not dependency_graph:
            return all_deps

        # Повторяем до тех пор, пока не найдем все транзитивные зависимости
        changed = True
        iteration = 0
        while changed and iteration < 10:  # Защита от бесконечного цикла
            changed = False
            current_deps = set(all_deps)

            for module in current_deps:
                if module in dependency_graph:
                    new_deps = dependency_graph[module] - all_deps
                    if new_deps:
                        all_deps.update(new_deps)
                        changed = True

            iteration += 1

        return all_deps

    def analyze_project(self, project_path: Path, appdir_path: Path = None) -> Dict:
        """Анализирует весь проект"""
        python_files = self.find_python_files(project_path)
        print(f"Найдено {len(python_files)} Python файлов")

        # Анализ статических импортов
        used_modules_code = set()
        file_modules = {}

        for file_path in python_files:
            modules = self.analyze_file_imports(file_path)
            if modules:
                file_modules[str(file_path.relative_to(project_path))] = list(modules)
                used_modules_code.update(modules)

        print(f"Найдено {len(used_modules_code)} модулей в коде: {', '.join(sorted(used_modules_code))}")

        # Поиск PySide6 библиотек
        search_base = appdir_path if appdir_path else project_path
        pyside_libs = self.find_pyside6_libs(search_base)

        if not pyside_libs:
            print("ОШИБКА: PySide6 библиотеки не найдены! Анализ невозможен.")
            return {
                'error': 'PySide6 библиотеки не найдены',
                'analysis_method': 'failed',
                'found_libraries': 0,
                'directly_used_code': sorted(used_modules_code),
                'all_required': [],
                'removable': [],
                'available_modules': [],
                'file_usage': file_modules
            }

        print(f"Найдено {len(pyside_libs)} PySide6 библиотек")

        # Анализ реальных зависимостей с ldd
        real_dependencies = self.build_real_dependency_graph(pyside_libs)

        # Определяем модули, которые реально используются через ldd
        used_modules_ldd = set()
        for module in used_modules_code:
            if module in real_dependencies:
                used_modules_ldd.update(real_dependencies[module])
                used_modules_ldd.add(module)

        print(f"Реальные зависимости через ldd: {', '.join(sorted(used_modules_ldd))}")

        # Объединяем результаты анализа кода и ldd
        all_used_modules = used_modules_code | used_modules_ldd

        # Получаем все необходимые модули включая зависимости
        all_required = self.get_all_dependencies(all_used_modules, real_dependencies)

        # Все доступные PySide6 модули
        available_modules = set(pyside_libs.keys())

        # Модули, которые можно удалить
        removable = available_modules - all_required

        return {
            'analysis_method': 'ldd + static analysis',
            'found_libraries': len(pyside_libs),
            'directly_used_code': sorted(used_modules_code),
            'directly_used_ldd': sorted(used_modules_ldd),
            'all_required': sorted(all_required),
            'removable': sorted(removable),
            'available_modules': sorted(available_modules),
            'file_usage': file_modules,
            'real_dependencies': {k: sorted(v) for k, v in real_dependencies.items()},
            'library_paths': {k: str(v) for k, v in pyside_libs.items()},
            'analysis_summary': {
                'total_modules': len(available_modules),
                'required_modules': len(all_required),
                'removable_modules': len(removable),
                'space_saving_potential': f"{len(removable)/len(available_modules)*100:.1f}%" if available_modules else "0%"
            }
        }

    def generate_appimage_recipe(self, removable_modules: List[str], template_path: Path) -> str:
        """Генерирует обновленный AppImage рецепт с командами очистки"""

        # Читаем существующий рецепт
        try:
            with open(template_path, 'r', encoding='utf-8') as f:
                recipe_content = f.read()
        except FileNotFoundError:
            print(f"Шаблон рецепта не найден: {template_path}")
            return ""

        # Генерируем новые команды очистки
        cleanup_lines = []

        # QML удаляем только если не используется
        qml_modules = {'QtQml', 'QtQuick', 'QtQuickWidgets'}
        if qml_modules.issubset(set(removable_modules)):
            cleanup_lines.append("  - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/")

        # Инструменты разработки (всегда удаляем)
        cleanup_lines.append("  - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}")

        # Модули для удаления
        if removable_modules:
            removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
            if removable_filtered:
                modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
                cleanup_lines.append(f"  - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")

        # Генерируем команду для удаления нативных библиотек с сохранением нужных
        required_libs = set()
        for module in sorted(set(self.all_required_modules)):
            required_libs.add(f"libQt6{module.replace('Qt', '')}*")

        # Добавляем системные библиотеки
        for lib in self.system_libs:
            required_libs.add(f"{lib}*")

        keep_pattern = '|'.join(sorted(required_libs))

        cleanup_lines.extend([
            "  - shopt -s extglob",
            f"  - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
        ])

        import re

        # Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
        # Паттерн: после "  - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
        pattern = r'(  - cp -r lib AppDir/usr\n)((?:  - (?:rm|shopt).*\n)*?)(?=AppDir:)'

        match = re.search(pattern, recipe_content)

        if not match:
            print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
            print("Добавляем команды очистки перед блоком AppDir:")

            # Просто вставим команды перед AppDir:
            appdir_pos = recipe_content.find('AppDir:')
            if appdir_pos != -1:
                new_content = (
                    recipe_content[:appdir_pos] +
                    '\n'.join(cleanup_lines) + '\n' +
                    recipe_content[appdir_pos:]
                )
                return new_content
            else:
                print("ОШИБКА: Не найден блок AppDir: в рецепте")
                return ""

        # Создаем замену - группа 1 (cp -r lib) + новые команды очистки
        replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'

        updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)

        return updated_recipe

def main():
    parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
    parser.add_argument('project_path', nargs='?', default='.',
                        help='Путь к проекту для анализа (по умолчанию: текущая директория)')
    parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
    parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
    parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
    parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')

    args = parser.parse_args()

    project_path = Path(args.project_path).resolve()
    if not project_path.exists():
        print(f"Ошибка: путь {project_path} не существует")
        sys.exit(1)

    appdir_path = Path(args.appdir).resolve() if args.appdir else None
    if appdir_path and not appdir_path.exists():
        print(f"Предупреждение: AppDir путь {appdir_path} не существует")
        appdir_path = None

    # Определяем корень проекта
    # Если запущен из подпапки проекта, ищем корень
    project_root = project_path
    if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
        project_root = project_path
    else:
        # Пытаемся найти корень проекта
        current = project_path
        while current != current.parent:
            if (current / ".git").exists() or (current / "pyproject.toml").exists():
                project_root = current
                break
            current = current.parent
    
    print(f"Корень проекта: {project_root}")

    analyzer = PySide6DependencyAnalyzer(project_root=project_root)
    
    # Если указан custom venv путь
    if args.venv:
        analyzer.venv_path = Path(args.venv).resolve()
        print(f"Использую указанный venv: {analyzer.venv_path}")
    
    results = analyzer.analyze_project(project_path, appdir_path)

    # Сохраняем в анализатор для генерации команд
    analyzer.all_required_modules = set(results.get('all_required', []))

    # Выводим результаты
    print("\n" + "="*60)
    print("АНАЛИЗ ЗАВИСИМОСТЕЙ PYSIDE6 (ldd analysis)")
    print("="*60)

    if 'error' in results:
        print(f"\nОШИБКА: {results['error']}")
        sys.exit(1)

    print(f"\nМетод анализа: {results['analysis_method']}")
    print(f"Найдено библиотек: {results['found_libraries']}")

    if results['directly_used_code']:
        print(f"\nИспользуемые модули в коде ({len(results['directly_used_code'])}):")
        for module in results['directly_used_code']:
            print(f"  • {module}")

    if results['directly_used_ldd']:
        print(f"\nРеальные зависимости через ldd ({len(results['directly_used_ldd'])}):")
        for module in results['directly_used_ldd']:
            print(f"  • {module}")

    print(f"\nВсе необходимые модули ({len(results['all_required'])}):")
    for module in results['all_required']:
        print(f"  • {module}")

    print(f"\nМодули, которые можно удалить ({len(results['removable'])}):")
    for module in results['removable']:
        print(f"  • {module}")

    print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")

    if args.verbose and results['real_dependencies']:
        print(f"\nРеальные зависимости (ldd):")
        for module, deps in results['real_dependencies'].items():
            if deps:
                print(f"  {module} → {', '.join(deps)}")

    # Обновляем AppImage рецепт
    recipe_path = analyzer.build_path / "AppImageBuilder.yml"
    if recipe_path.exists():
        updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
        if updated_recipe:
            with open(recipe_path, 'w', encoding='utf-8') as f:
                f.write(updated_recipe)
            print(f"\nAppImage рецепт обновлен: {recipe_path}")
        else:
            print(f"\nОШИБКА: не удалось обновить рецепт")
    else:
        print(f"\nПредупреждение: рецепт AppImage не найден в {recipe_path}")

    # Сохраняем результаты в JSON
    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        print(f"Результаты сохранены в: {args.output}")

    print("\n" + "="*60)


if __name__ == "__main__":
    main()
