import datetime
import gzip
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time
import zipfile
from contextlib import contextmanager
from pathlib import Path
from urllib.parse import urlparse

import portalocker  # 跨平台文件锁
import requests
# from apps.upgrade.core import sys_backup_root
from config.config import SystemConfig
from django.conf import settings

sys_backup_root = os.path.join(settings.BASE_DIR, '.sys_backups')
backup_folder = os.path.join(sys_backup_root, 'backups')  # 备份存储
temp_folder = os.path.join(sys_backup_root, 'upgrade_temp')  # 下载缓存
UPGRADE_JSON_PATH = os.path.join(sys_backup_root, "upgrade.json")
# _execute_upgrade函数


def _execute_upgrade():
    """核心升级执行函数，包含完整生命周期管理"""
    os.makedirs(backup_folder, exist_ok=True)

    try:
        # 步骤1: 下载升级包
        _update_progress('downloading', 10, "开始下载升级包")
        package_path = _download_package(
            url=SystemConfig.UPGRADE_DOWNLOAD_URL,
            save_path=temp_folder,
            progress_callback=lambda pct: _update_progress(
                'downloading', 10 + int(pct * 0.6), f"下载进度 {pct}%"))

        # 步骤2: 验证签名
        _update_progress('verifying', 16, "验证文件完整性")
        if not _verify_package(package_path):
            raise ValueError("文件校验失败，可能存在篡改风险")
        _update_progress('verifying', 32,
                         "验证文件完整性通过,我们即将备份数据库与文件，服务器可能会出现连接失败")

        # 步骤3: 创建备份
        _update_progress('backup', 33, "创建系统备份")
        _create_backup(backup_folder)

        # 步骤4: 应用升级
        _update_progress('upgrading', 51, "替换系统文件")
        _apply_upgrade(package_path, temp_folder)

        # 步骤5: 数据库迁移
        _update_progress('migrating', 66, "执行数据库变更")
        _run_migrations()

        # 新增步骤6: 安装环境依赖
        _update_progress('installing', 70, "安装生产环境依赖")
        requirements_prod = os.path.join(os.path.dirname(package_path),
                                         'requirements_prod.txt')
        if os.path.exists(requirements_prod):
            if _install_requirements(requirements_prod):
                _update_progress('installing', 75,
                                 "依赖安装成功，生成新requirements.txt")
                _generate_requirements()
            else:
                _update_progress('installing', 75, "依赖安装失败，跳过requirements生成")
        else:
            _update_progress('installing', 75,
                             "未找到requirements_prod.txt，跳过依赖安装")

        # 步骤7: 清理陈旧文件
        _update_progress('cleaning', 76, f"清理陈旧文件：{temp_folder}")
        delect_old_status = _delect_old_files(temp_folder)
        if delect_old_status:
            _update_progress('cleaning', 82, "清理陈旧文件完成")
        else:
            _update_progress('cleaning', 82, "清理陈旧文件权限不足，跳过执行")

        # 步骤8: 重新收集静态目录
        _update_progress('cleaning', 83, f"重新收集静态：{temp_folder}")
        collected_static_status = _collected_static()
        if collected_static_status:
            _update_progress('cleaning', 95, "重新收集静态完成")
        else:
            _update_progress('cleaning', 96, "重新收集静态权限不足，跳过执行")

        # 步骤9: 清理临时文件
        remove_status = _retry_rmtree(temp_folder)
        if remove_status:
            _update_progress('cleaning', 97, "清理临时文件完成")
        else:
            _update_progress('cleaning', 97, "清理临时文件权限不足，跳过执行")

        # 最终状态
        _update_progress('success', 100, "升级完成")

    except Exception as e:
        _handle_failure(str(e), backup_folder)
        raise


def _install_requirements(requirements_path):
    """
    安装指定requirements文件中的依赖包
    :param requirements_path: requirements文件路径
    :return: bool 安装是否成功
    """

    if not Path(requirements_path).exists():
        print(f"警告：依赖文件 {requirements_path} 不存在")
        return False

    error_modules = []
    success_count = 0

    with open(requirements_path) as f:
        requirements = [
            line.strip() for line in f
            if line.strip() and not line.startswith('#')
        ]

    for req in requirements:
        try:
            # 使用--no-cache-dir避免缓存问题，清华镜像加速下载
            cmd = [
                sys.executable, '-m', 'pip', 'install', '--no-cache-dir', '-i',
                'https://pypi.tuna.tsinghua.edu.cn/simple', req
            ]
            subprocess.check_call(cmd)
            success_count += 1
        except subprocess.CalledProcessError as e:
            print(f"安装失败: {req} - 错误: {e}")
            error_modules.append(req)

    if error_modules:
        error_file = Path(requirements_path).parent / 'install_errors.log'
        with open(error_file, 'a') as f:
            f.write(f"{'='*20} {datetime.now()} {'='*20}\n")
            f.write("\n".join(error_modules))
        print(f"警告：{len(error_modules)}个依赖安装失败，已记录到 {error_file}")

    return len(error_modules) == 0


def _generate_requirements(output_file='requirements.txt'):
    """
    生成当前环境的依赖列表文件
    :param output_file: 输出文件名
    :return: bool 是否生成成功
    """

    try:
        # 使用pip freeze获取精确版本
        result = subprocess.run([sys.executable, '-m', 'pip', 'freeze'],
                                capture_output=True,
                                text=True,
                                check=True)

        # 过滤掉非PyPI安装的包（如本地开发包）
        packages = [
            line for line in result.stdout.split('\n')
            if line and not line.startswith(('@', '-e', 'git+'))
        ]

        # 写入文件（UTF-8编码确保兼容性）
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write("# Auto-generated requirements file\n")
            f.write("# Date: {}\n".format(
                datetime.now().strftime("%Y-%m-%d %H:%M")))
            f.write("\n".join(sorted(packages)))

        print(f"成功生成依赖文件: {Path(output_file).absolute()}")
        return True

    except subprocess.CalledProcessError as e:
        print(f"生成失败: {e.stderr}")
        return False
    except Exception as e:
        print(f"意外错误: {str(e)}")
        return False


def _collected_static():
    """校验STATIC_ROOT目录下是否已存在有效静态文件"""
    static_root = settings.STATIC_ROOT

    # 检查目录存在性
    if not os.path.exists(static_root):
        return False

    # 验证目录内容有效性
    try:
        # 检测是否包含至少一个非空文件（如admin组件）
        has_valid_files = any(
            os.path.getsize(os.path.join(root, f)) > 0
            for root, dirs, files in os.walk(static_root) for f in files
            if not f.startswith('.'))
        from django.core.management import call_command
        call_command('collectstatic', '--noinput')
        return has_valid_files
    except PermissionError:
        return False


def _delete_parent_folders(file_path, stop_at=settings.BASE_DIR):
    """递归删除空父文件夹"""
    deleted_folders = set()

    # 获取所有可能的父文件夹路径
    parent_dir = os.path.dirname(file_path)
    folders_to_check = []

    # 收集直到stop_at的父目录
    while len(parent_dir) > len(stop_at):
        folders_to_check.append(parent_dir)
        parent_dir = os.path.dirname(parent_dir)

    # 从最深层开始检查
    for folder in reversed(folders_to_check):
        try:
            # 仅当文件夹存在且为空时删除
            if os.path.exists(folder) and not os.listdir(folder):
                os.rmdir(folder)
                deleted_folders.add(folder)
        except OSError as e:
            print(f"删除空文件夹失败：{folder} - {str(e)}")
            break  # 遇到错误停止向上删除

    return len(deleted_folders) > 0


def _delect_old_files(temp_folder, retries=3, delay=1):
    extract_path = os.path.join(temp_folder, 'extracted')
    deleted_list = os.path.join(extract_path, 'patch', 'deleted_files.txt')
    _update_progress('cleaning', 66, f"清理{deleted_list}内文件")

    # 读取待删除文件列表
    try:
        with open(deleted_list, 'r', encoding='utf-8') as f:
            files_to_delete = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"未找到文件列表：{deleted_list}")
        return False
    except Exception as e:
        print(f"读取文件列表失败：{e}")
        return False

    all_success = True

    for file_rel_path in files_to_delete:

        full_path = os.path.join(settings.BASE_DIR, file_rel_path)

        # 检查路径是否存在
        if not os.path.exists(full_path):
            _delete_parent_folders(full_path)
            continue  # 若文件不存在，视为已删除

        # 重试删除操作
        success = False
        for attempt in range(retries):
            try:
                if os.path.isfile(full_path):
                    os.remove(full_path)
                else:
                    shutil.rmtree(full_path)  # 处理目录
                _delete_parent_folders(full_path)
                success = True
                break
            except PermissionError:
                if attempt < retries - 1:
                    time.sleep(delay)
                else:
                    _update_progress('cleaning', 70, f"权限不足，无法删除：{full_path}")
            except Exception as e:
                _update_progress('cleaning', 70, f"删除失败：{full_path}，错误：{e}")
                if attempt < retries - 1:
                    time.sleep(delay)
                else:
                    break

        # 确认文件是否已被删除
        if os.path.exists(full_path):
            all_success = False
            _update_progress('cleaning', 70, f"文件仍存在，删除失败：{full_path}")

    return all_success


def _retry_rmtree(temp_folder, retries=3, delay=1):
    """
    增强版目录删除函数，包含：
    - 历史遗留文件清理
    - 跨平台只读文件处理
    - 重试机制和错误日志
    """

    # 移除临时目录（含重试机制）
    for i in range(retries):
        try:
            if not os.path.exists(temp_folder):
                return True
            shutil.rmtree(temp_folder, onerror=_on_rm_error)
            return True
        except FileNotFoundError:
            return True
        except Exception as e:
            print(f"删除临时目录失败，重试 {i+1}/{retries}: {e}")
            if i < retries - 1:
                time.sleep(delay)
    return False


def _on_rm_error(func, path, exc_info):
    """统一处理删除失败：修改权限后重试"""
    try:
        os.chmod(path, os.stat.S_IWUSR)  # 确保写权限
        os.unlink(path)  # 直接尝试删除文件，针对某些残留文件
    except Exception as e:
        print(f"处理删除错误 {path} 失败: {e}")
    try:
        func(path)  # 再次调用原删除函数
    except Exception as e:
        print(f"再次删除失败: {path}, 错误: {e}")







@contextmanager
def lock_upgrade_file(mode='r+'):
    """文件锁上下文管理器"""
    with open(UPGRADE_JSON_PATH, mode) as f:
        portalocker.lock(f, portalocker.LOCK_EX)  # 独占锁
        try:
            yield f
        finally:
            portalocker.unlock(f)


def read_upgrade_status():
    """读取升级状态"""
    if not os.path.exists(UPGRADE_JSON_PATH):
        return None
    with lock_upgrade_file('r') as f:
        return json.load(f)


def write_upgrade_status(data):
    """写入升级状态（原子操作）"""
    with lock_upgrade_file('w') as f:
        json.dump(data, f, indent=2)


def _update_progress(status, percentage, log_message):
    """原子化更新JSON进度"""
    current_data = read_upgrade_status() or {
        "status": "pending",
        "progress": 0,
        "log": "",
        "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "version": SystemConfig.VERSION
    }

    current_data["status"] = status
    current_data["progress"] = percentage
    current_data["log"] += f"\n[{status.upper()}] {log_message}"

    write_upgrade_status(current_data)


def _download_package(url,
                      save_path,
                      progress_callback,
                      chunk_size=1024 * 1024):
    """下载实现，动态获取文件名（兼容Windows）"""
    os.makedirs(save_path, exist_ok=True)

    # 发送请求并获取响应
    response = requests.get(url,
                            stream=True,
                            params={'software_id': SystemConfig.SOFTWARE_ID})
    response.raise_for_status()

    # 解析真实文件名
    def get_safe_filename(response):
        # 从 Content-Disposition 提取
        content_disp = response.headers.get('Content-Disposition', '')
        if 'filename=' in content_disp:
            filename = re.findall('filename=(?:"([^"]+)"|([^;]+))',
                                  content_disp)[0]
            filename = filename[0] or filename[1]
        else:  # 从URL路径提取
            filename = os.path.basename(urlparse(response.url).path)

        # 处理无文件名情况（如路径以/结尾）
        if not filename:
            filename = f"package_{SystemConfig.SOFTWARE_ID}.zip"

        # 去除非法字符并防止路径注入
        filename = os.path.basename(
            filename.replace('/', '_').replace('\\', '_')).strip()
        if not filename:
            filename = "unknown_package"
        return filename

    filename = get_safe_filename(response)
    temp_file = os.path.join(save_path, f"{filename}.tmp")  # 临时文件加.tmp后缀
    final_file = os.path.join(save_path, filename)

    # 清理残留临时文件
    if os.path.exists(temp_file):
        os.remove(temp_file)

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

        with open(temp_file, 'wb') as f:
            downloaded = 0
            for chunk in response.iter_content(chunk_size):
                if chunk:
                    f.write(chunk)
                    downloaded += len(chunk)
                    if total_size > 0:
                        pct = downloaded / total_size * 100
                        progress_callback(min(pct, 100))

        # 验证文件大小（服务器提供content-length时）
        if total_size > 0 and os.path.getsize(temp_file) != total_size:
            raise IOError("下载文件不完整")

        os.replace(temp_file, final_file)  # 原子性重命名
        return final_file

    except Exception as e:
        if os.path.exists(temp_file):
            os.remove(temp_file)
        raise RuntimeError(f"下载失败: {e}") from e


def _verify_package(file_path):
    response = requests.get(
        SystemConfig.HASH_CHECK_URL,
        timeout=3,
        params={'software_id': SystemConfig.SOFTWARE_ID}  # 假设需要传递软件ID
    )
    response.raise_for_status()  # 检查4xx/5xx状态码

    # 严格解析版本号格式
    expected_sha256 = response.json().get('hash', '')
    """文件完整性校验"""
    sha256 = hashlib.sha256()
    with open(file_path, 'rb') as f:
        while chunk := f.read(128 * 1024):
            sha256.update(chunk)
    return sha256.hexdigest() == expected_sha256.lower()


def _check_db_tool(engine):
    """根据数据库引擎检测对应备份工具是否存在"""
    db_type = engine.lower()
    tool_name = ""
    install_guide = ""

    # 检测操作系统类型
    is_windows = sys.platform.startswith('win32')
    linux_distro = _get_linux_distro() if not is_windows else ""

    if 'postgresql' in db_type:
        tool_name = 'pg_dump.exe' if is_windows else 'pg_dump'
        install_guide = ("Windows：安装PostgreSQL客户端并添加PATH\n"
                         "CentOS：sudo yum install postgresql-client\n"
                         "Debian：sudo apt-get install postgresql-client-15"
                         ) if not is_windows else "安装PostgreSQL客户端并添加PATH环境变量"

    elif 'mysql' in db_type:
        tool_name = 'mysqldump.exe' if is_windows else 'mysqldump'
        install_guide = ("Windows：安装MySQL Server并配置PATH\n"
                         "CentOS：sudo yum install mysql-client\n"
                         "Debian：sudo apt-get install mysql-client")

    elif 'sqlite3' in db_type:
        return 'sqlite3'  # SQLite3默认随Python安装

    # 执行工具检测
    try:
        if is_windows:
            subprocess.run(f'where {tool_name}',
                           check=True,
                           shell=True,
                           capture_output=True)
        else:
            subprocess.run(['which', tool_name],
                           check=True,
                           capture_output=True)
        return tool_name
    except subprocess.CalledProcessError:
        return False


def _get_linux_distro():
    """识别Linux发行版"""
    try:
        with open('/etc/os-release') as f:
            return next(
                line.split('=')[1].strip('"\n') for line in f
                if line.startswith('ID=')).lower()
    except FileNotFoundError:
        return "unknown"


def _get_backup_command(engine, db_config):
    """生成跨平台数据库备份命令"""
    backup_tool = _check_db_tool(engine)
    if not backup_tool:
        return False
    db_type = engine.lower()

    if 'postgresql' in db_type:
        return (f'"{backup_tool}" -h {db_config["HOST"]} '
                f'-p {db_config["PORT"]} '
                f'-U {db_config["USER"]} '
                f'-d {db_config["NAME"]}')

    elif 'mysql' in db_type:
        return (f'"{backup_tool}" --host={db_config["HOST"]} '
                f'--port={db_config["PORT"]} '
                f'--user={db_config["USER"]} '
                f'--password={db_config["PASSWORD"]} '
                f'{db_config["NAME"]}')

    elif 'sqlite3' in db_type:
        return f'sqlite3 {db_config["NAME"]} ".dump"'

    else:
        raise Exception(f'不支持的数据库引擎: {engine}')


def _create_backup(backup_folder):
    """系统备份策略升级版"""
    _update_progress('backup', 35, "准备备份路径")
    backup_name = f"backup_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
    os.makedirs(backup_folder, exist_ok=True)
    full_path = os.path.join(backup_folder, backup_name)

    # 获取数据库配置
    db_config = settings.DATABASES['default']
    engine = db_config['ENGINE']
    _update_progress('backup', 40, "备份路径准备就绪")
    try:
        # 数据库备份
        _update_progress('backup', 41, "开始备份数据库")
        backup_cmd = _get_backup_command(engine, db_config)
        if not backup_cmd:
            _update_progress('backup', 0, f"数据库备份失败: {str(e)}")
            return False
        # 执行备份命令
        with open(f"{full_path}.sql", 'w') as f:
            proc = subprocess.run(backup_cmd,
                                  shell=True,
                                  stdout=f,
                                  stderr=subprocess.PIPE,
                                  text=True)
            if proc.returncode != 0:
                raise Exception(f'数据库备份失败: {proc.stderr}')

        _update_progress('backup', 45, "数据库备份完成，马上开始建立文件备份")
    except Exception as e:
        _update_progress('backup', 0, f"备份失败: {str(e)}")
        return False
    try:
        # 代码目录备份
        _update_progress('backup', 46,
                         f"开始备份文件: {os.path.join(full_path, 'code')}")
        shutil.copytree(
            settings.BASE_DIR,
            os.path.join(full_path, 'code'),
            ignore=shutil.ignore_patterns('.sys_backups*')  # 关键过滤
        )
        _update_progress('backup', 50,
                         f"文件备份完成，路径: {os.path.join(full_path, 'code')}")
    except Exception as e:
        _update_progress('backup', 0,
                         f"备份完成失败，路径: {os.path.join(full_path, 'code')}")

    return full_path


def _is_valid_gzip(file_path):
    """通过文件头校验是否为合法gzip文件"""
    try:
        with gzip.open(file_path, 'rb') as f:
            f.read(1)  # 尝试读取1字节触发格式校验
        return True
    except (gzip.BadGzipFile, OSError):
        return False


def file_sha256(file_path):
    """计算文件SHA256哈希（兼容大文件）"""
    sha = hashlib.sha256()
    with open(file_path, 'rb') as f:
        while chunk := f.read(8192):
            sha.update(chunk)
    return sha.hexdigest()


def _apply_upgrade(package_path, temp_folder):
    """应用升级的原子操作"""
    extract_path = os.path.join(temp_folder, 'extracted')
    print('extract_path------------>', extract_path)
    os.makedirs(extract_path, exist_ok=True)

    # 格式校验
    if package_path.endswith('.tar.gz') or package_path.endswith('.tgz'):
        if not _is_valid_gzip(package_path):
            raise ValueError(f"File is not a valid gzip: {package_path}")
        mode = 'r:gz'
    elif package_path.endswith('.zip'):
        if not zipfile.is_zipfile(package_path):
            raise ValueError(f"File is not a valid ZIP: {package_path}")
        mode = 'zip'
    else:
        raise ValueError(
            f"Unsupported format: {os.path.basename(package_path)}")
    _update_progress('upgrading', 55, f"升级包格式校验完成")
    # 解压操作
    if package_path.endswith('.tar.gz') or package_path.endswith('.tgz'):
        with tarfile.open(package_path, mode) as tar_ref:
            tar_ref.extractall(extract_path)
    elif package_path.endswith('.zip'):
        with zipfile.ZipFile(package_path) as zip_ref:
            zip_ref.extractall(extract_path)
    _update_progress('upgrading', 60, f"升级包解压完成")
    # 替换文件（使用原子操作）
    # 替换文件（增强版原子操作）
    # 仅遍历 src 目录
    src_path = os.path.join(extract_path, 'src')
    for root, dirs, files in os.walk(src_path):
        relative_path = os.path.relpath(root, src_path)  # 计算相对于 src_path 的路径
        dest_path = os.path.join(settings.BASE_DIR, relative_path)
        # 智能创建目标目录（含多级嵌套）
        os.makedirs(dest_path, exist_ok=True)

        for file in files:
            src_file = os.path.normpath(os.path.join(root, file))
            dest_file = os.path.normpath(os.path.join(dest_path, file))
            temp_dest = dest_file + '.upgrading'  # 临时文件后缀
            _update_progress('upgrading', 65, f"替换文件：{dest_file}")
            try:
                # 阶段1：哈希校验跳过未变更文件
                if os.path.exists(dest_file):
                    if file_sha256(src_file) == file_sha256(dest_file):
                        _update_progress('skipped', 0, f"文件未变化: {dest_file}")
                        continue

                # 阶段2：原子写入临时文件
                shutil.copy2(src_file, temp_dest)

                # 阶段3：跨平台安全替换
                if os.path.exists(dest_file):
                    os.replace(temp_dest, dest_file)  # 原子替换现有文件
                else:
                    os.rename(temp_dest, dest_file)  # 直接重命名新文件

                _update_progress('upgrading', 65, f"原子替换成功: {dest_file}")

            except PermissionError as pe:
                # 处理文件被占用场景
                error_msg = f"文件被占用: {dest_file} | 建议: 停止服务后重试 ({pe})"
                _update_progress('upgrading', 0, error_msg)

            except Exception as e:
                # 详细错误日志
                error_detail = f"替换失败: {dest_file} | 错误类型: {type(e).__name__} - {str(e)}"
                _update_progress('upgrading', 0, error_detail)

            finally:
                # 确保临时文件清理
                if os.path.exists(temp_dest):
                    try:
                        os.remove(temp_dest)
                    except Exception as cleanup_error:
                        pass
    _update_progress('upgrading', 90, f"文件替换完成")


def _run_migrations():
    """执行数据库迁移（包含makemigrations）"""
    from django.core.management import execute_from_command_line
    execute_from_command_line(["manage.py", "makemigrations", "--noinput"])
    execute_from_command_line(["manage.py", "migrate", "--noinput"])


def _handle_failure(error_msg, backup_folder):
    """失败处理（含自动回滚）"""
    _update_progress('failed', 0, f"\n[ERROR] {error_msg}")

    # 自动回滚到最新备份
    latest_backup = max(os.listdir(backup_folder))
    _restore_backup(os.path.join(backup_folder, latest_backup))


def _restore_backup(backup_path):
    """从备份恢复系统"""
    # 恢复数据库
    # os.system(f"psql {settings.DATABASE_NAME} < {backup_path}.sql")

    # 步骤1：加载备份内容到临时目录
    temp_dir = tempfile.mkdtemp()  # 创建系统级临时目录
    code_backup = os.path.join(backup_path, 'code')

    try:
        # 复制备份内容到临时空间
        shutil.copytree(src=code_backup, dst=os.path.join(temp_dir, 'code'))

        # 步骤2：安全删除旧目录（排除备份目录）
        def custom_ignore(path, names):
            return ['.sys_backups'] if path == settings.BASE_DIR else []

        shutil.rmtree(
            settings.BASE_DIR,
            ignore_errors=True,
            ignore=custom_ignore  # 关键保护逻辑
        )

        # 步骤3：从临时目录恢复代码
        shutil.copytree(os.path.join(temp_dir, 'code'), settings.BASE_DIR)
    finally:
        # 清理临时文件
        shutil.rmtree(temp_dir, ignore_errors=True)
