"""
项目瘦身
@author: xmqsvip
Created on 2024-11-30
"""

import os
import csv
import sys
import time
import shutil
import ctypes
import subprocess
from pathlib import Path

from .my_logger import my_logger


def is_admin():
    try:
        # 检查是否有管理员权限
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        return False


def check_dependency_files(main_run_path, project_dir, check_dir=None, pack_mode=0,
                           monitoring_time=18, except_packages=None):
    """
    检查依赖文件
    """

    current_dir = Path(__file__).parent.parent
    procmon_path = current_dir.joinpath('dep_exe/Procmon64.exe')
    pmc_base_path = current_dir.joinpath('dep_exe/ProcmonConfiguration.pmc')
    procmon_log_path = Path(project_dir).joinpath('procmon_log.pml')
    csv_log_path = Path(project_dir).joinpath('procmon_log.csv')
    dependency_file_csv = Path(project_dir).joinpath("dependency.csv")
    if pack_mode == 0:
        dependency_file_csv = Path(project_dir).joinpath("dependency_fast.csv")
    if dependency_file_csv.exists():
        my_logger.info('已存在依赖文件清单表，跳过依赖文件检查')
        dependency_files = get_dependency_list(dependency_file_csv, pack_mode=pack_mode)
        return dependency_files

    my_logger.info('当你的项目稍后自动运行后，请进行一些必要的功能操作：如点击运行按钮等,否则可能会造成依赖缺失')
    time.sleep(5)
    if os.path.exists(procmon_log_path):
        os.remove(procmon_log_path)
    my_logger.info(f'准备监控依赖文件，{monitoring_time}秒后自动结束监控')
    cmd = [
        procmon_path,
        "/Minimized",
        "/AcceptEula",
        "/Runtime", str(monitoring_time),
        "/Quiet",
        "/Backingfile", procmon_log_path,
        # "/LoadConfig", pmc_base_path,
    ]
    my_logger.info("开始启动监控工具,2秒后自动启动你的程序")
    procmon_process = subprocess.Popen(cmd)
    time.sleep(2)
    current_env_py = sys.executable.replace('\\', '/')
    image_path = str(sys.base_prefix).replace('\\', '/') + '/python.exe'

    # 启动你的脚本
    if 'py' in str(Path(main_run_path).suffix):
        if pack_mode == 0:
            main_run_cmd = [current_env_py, main_run_path]
            if not os.path.exists(image_path):
                my_logger.error(f'未找到{image_path}')
                sys.exit()
        else:
            python_exe_path = str(Path(project_dir).joinpath('rundep/python.exe'))
            pythonw_exe_path = str(Path(project_dir).joinpath('rundep/pythonw.exe'))
            if os.path.exists(python_exe_path):
                main_run_cmd = [python_exe_path, main_run_path]
                image_path = python_exe_path
            elif os.path.exists(pythonw_exe_path):
                main_run_cmd = [pythonw_exe_path, main_run_path]
                image_path = pythonw_exe_path
            else:
                my_logger.warning(f'未找到{project_dir}/rundep文件夹中的python.exe，使用当前环境的python运行脚本')
                main_run_cmd = [current_env_py, main_run_path]

    else:
        main_run_cmd = main_run_path
        image_path = main_run_path

    os.chdir(os.path.dirname(main_run_path))
    my_logger.info(f"启动你的程序:{main_run_path}")
    main_run_process = subprocess.Popen(main_run_cmd)
    time.sleep(monitoring_time)
    main_run_process.kill()

    procmon_process.wait()
    my_logger.info("开始保存日志")
    cmd = [
        procmon_path,
        "/Minimized",
        "/AcceptEula",
        "/Quiet",
        "/NoConnect",
        "/OpenLog", procmon_log_path,
        "/SaveAs", csv_log_path,
        "/LoadConfig", pmc_base_path,
    ]
    procmon_process = subprocess.Popen(cmd)
    procmon_process.wait()

    while True:
        if Path.exists(csv_log_path):
            break
        time.sleep(1)

    dependency_files = get_dependency_list(csv_log_path, image_path, check_dir, pack_mode)
    # # 排除用户指定的第三方依赖包
    if except_packages:
        ready_remove_list = []
        for i in dependency_files:
            for except_package in except_packages:
                if f"site-packages/{except_package}" in i:
                    ready_remove_list.append(i)
        for i in ready_remove_list:
            dependency_files.remove(i)

    if dependency_files:
        with open(dependency_file_csv, mode='w', newline='', encoding='utf-8') as fp:
            csv_writer = csv.writer(fp)
            for i in dependency_files:
                csv_writer.writerow([i])
    try:
        os.remove(procmon_log_path)
        os.remove(csv_log_path)
    except Exception as e:
        my_logger.error(f"删除日志文件失败:{e}")

    return dependency_files


def get_dependency_list(csv_log_path, image_path=None, check_dir=None, pack_mode=0):
    my_logger.info('开始分析依赖文件...')
    if pack_mode == 0:
        base_env_dir = sys.base_prefix.lower().replace('\\', '/')
        current_env_dir = sys.prefix.lower().replace('\\', '/')
        if base_env_dir == current_env_dir:
            check_dir = base_env_dir
        else:
            check_dir = [base_env_dir, current_env_dir]

    dependency_files = set()
    with open(csv_log_path, encoding='utf-8') as fp:
        csvreader = csv.reader(fp)
        if check_dir and image_path:
            image_path = image_path.lower().replace('\\', '/')
            if isinstance(check_dir, list):
                for row in csvreader:
                    cell_value = row[0].replace('\\', '/')
                    cell_value_ = cell_value.lower()
                    cell_value_2 = row[1].replace('\\', '/').lower()
                    if cell_value_2 == image_path and cell_value_ and os.path.isfile(cell_value_) and (
                            '__pycache__' not in cell_value_):
                        if (check_dir[0] in cell_value_) or (check_dir[1] in cell_value_):
                            dependency_files.add(cell_value)
            else:
                check_dir = check_dir.lower().replace('\\', '/')
                for row in csvreader:
                    cell_value = row[0].replace('\\', '/')
                    cell_value_ = cell_value.lower()
                    cell_value_2 = row[1].replace('\\', '/').lower()
                    if cell_value_2 == image_path and cell_value_ and os.path.isfile(cell_value_) and (
                            check_dir in cell_value_) and ('__pycache__' not in cell_value_):
                        dependency_files.add(cell_value)
        else:
            # 读取的是依赖文件清单表，跳过查找依赖文件
            for row in csvreader:
                dependency_files.add(row[0])

    return dependency_files


def move_files(check_dir, project_dir, dependency_files):
    my_logger.info('开始瘦身...')
    removed_file_dir = os.path.join(project_dir, "removed_file")

    moved_file_num = 0
    removed_size = 0

    for root, dirs, files in os.walk(check_dir):
        if "rundep/AppData" in root:
            continue
        for filename in files:
            src_file = str(os.path.join(root, filename)).replace('\\', '/')
            if src_file in dependency_files:
                continue
            else:
                try:
                    # 保持目录结构
                    relative_path = str(os.path.relpath(root, check_dir))
                    dest_folder = os.path.join(removed_file_dir, relative_path)
                    os.makedirs(dest_folder, exist_ok=True)
                    removed_size += os.path.getsize(src_file)
                    # 移动文件
                    shutil.move(src_file, os.path.join(dest_folder, filename))
                    moved_file_num += 1
                except Exception as e:
                    my_logger.error(f"无法移动文件: {src_file}: {e}")

    # 移除空文件夹
    for root, dirs, files in os.walk(project_dir, topdown=False):
        for name in dirs:
            full_path = os.path.join(root, name)
            if not os.listdir(str(full_path)):
                try:
                    os.rmdir(full_path)
                except Exception:
                    pass

    removed_size = f"{removed_size / (1024 * 1024):.2f}"
    my_logger.info(f"瘦身完成，移除了{moved_file_num}个文件, 减小了{removed_size}M")
    if moved_file_num > 0:
        my_logger.info(f"移除的文件保存到了:{removed_file_dir}")


def to_slim_file(main_run_path: str, check_dir: str, project_dir: str = None,
                 monitoring_time: int = 18, pack_mode=1) -> None:
    """
    项目瘦身
    :param main_run_path: 项目主运行文件路径,py或其它
    :param check_dir: 需要瘦身的目录
    :param project_dir:  项目目录
    :param monitoring_time:  监控工具监控时长（秒）
    :param pack_mode: 若单独使用瘦身功能不必填写
    :return:
    """
    if not is_admin():
        my_logger.error('请以管理员身份运行本程序(或以管理员身份打开的编辑器中执行此程序)')
        return

    if project_dir is None:
        project_dir = check_dir

    main_run_path = main_run_path.replace("\\", "/")
    check_dir = check_dir.replace("\\", "/")
    project_dir = project_dir.replace("\\", "/")
    dependency_files = check_dependency_files(main_run_path, project_dir, check_dir, monitoring_time=monitoring_time,
                                              pack_mode=pack_mode)
    move_files(check_dir, project_dir, dependency_files)
