"""
自动热更新模块

支持在不重新下载整个exe/容器的情况下，自动更新少量修改的文件。

更新机制：
1. 从远程服务器获取更新清单（包含文件列表、版本、MD5哈希）
2. 比较本地文件与远程文件的哈希值
3. 只下载有变化的文件
4. 备份旧文件，下载新文件
5. 需要时重启应用

支持更新的文件类型：
- Python 源文件 (.py)
- 前端文件 (.js, .ts, .tsx, .jsx, .vue, .css, .html, .txt, .json)
- 配置文件 (.yml, .json)
- 静态资源（图片、字体等）
"""

import os
import sys
import json
import hashlib
import shutil
import tempfile
import asyncio
import aiohttp
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Any
from loguru import logger
from dataclasses import dataclass, asdict
from enum import Enum


class UpdateStatus(Enum):
    """更新状态"""
    IDLE = "idle"                      # 空闲
    CHECKING = "checking"              # 检查中
    DOWNLOADING = "downloading"        # 下载中
    INSTALLING = "installing"          # 安装中
    COMPLETED = "completed"            # 完成
    FAILED = "failed"                  # 失败
    RESTART_REQUIRED = "restart_required"  # 需要重启


@dataclass
class FileUpdate:
    """文件更新信息"""
    path: str                 # 相对路径
    md5: str                  # MD5哈希
    size: int                 # 文件大小
    download_url: str         # 下载URL
    version: str              # 文件版本
    requires_restart: bool    # 是否需要重启
    description: str = ""     # 更新说明


@dataclass
class DeletedFile:
    """待删除文件信息"""
    path: str
    requires_restart: bool
    description: str = ""


@dataclass
class UpdateManifest:
    """更新清单"""
    version: str                      # 版本号
    release_date: str                 # 发布日期
    description: str                  # 版本说明
    files: List[FileUpdate]           # 文件列表
    deleted_files: List[DeletedFile] = None  # 待删除文件列表
    min_version: str = ""             # 最低兼容版本
    changelog: List[str] = None       # 更新日志


@dataclass
class UpdateProgress:
    """更新进度"""
    status: UpdateStatus
    current_file: str = ""
    current_index: int = 0
    total_files: int = 0
    downloaded_bytes: int = 0
    total_bytes: int = 0
    message: str = ""
    error: str = ""


class AutoUpdater:
    """自动更新器"""

    # 默认 GitHub 更新源配置
    DEFAULT_GITHUB_API_BASE = "https://api.github.com"
    DEFAULT_GITHUB_RAW_BASE = "https://raw.githubusercontent.com"
    DEFAULT_GITHUB_OWNER = "GuDong2003"
    DEFAULT_GITHUB_REPO = "xianyu-auto-reply-fix"
    
    # 可热更新的静态文件类型（通常不需要重启）
    HOT_UPDATABLE_EXTENSIONS = {
        '.css', '.eot', '.gif', '.html', '.ico', '.jpeg', '.jpg',
        '.js', '.json', '.jsx', '.map', '.otf', '.png', '.svg',
        '.ts', '.tsx', '.ttf', '.vue',
        '.txt', '.webp', '.woff', '.woff2', '.yml', '.yaml',
    }
    
    # 需要重启的文件类型
    RESTART_REQUIRED_EXTENSIONS = {'.py', '.pyd', '.so', '.dll', '.exe'}
    
    # 不允许更新的文件/目录
    EXCLUDED_PATHS = {
        'data/',
        'logs/',
        'browser_data/',
        'uploads/',
        '__pycache__/',
        '.git/',
        '.github/',
        '.claude/',
        '.ace-tool/',
        '.pytest_cache/',
        'build/',
        'nginx/',
        'dist/',
        'node_modules/',
        'qr_screenshots/',
        'trajectory_history/',
        'update_backup/',
        'venv/',
        'global_config.yml',  # 用户配置文件不更新
        'update_files.json',
        'Dockerfile',
        'Dockerfile-cn',
        'docker-compose.yml',
        'docker-compose-cn.yml',
    }
    
    def __init__(self, 
                 app_dir: Optional[str] = None,
                 update_server: Optional[str] = None,
                 github_owner: Optional[str] = None,
                 github_repo: Optional[str] = None,
                 github_token: Optional[str] = None,
                 current_version: str = "1.0.0"):
        """
        初始化更新器
        
        Args:
            app_dir: 应用目录，默认为当前工作目录
            update_server: 兼容旧参数，保留但不再作为默认更新源
            github_owner: GitHub 仓库 owner
            github_repo: GitHub 仓库 repo
            github_token: GitHub API Token（可选，用于提升限额）
            current_version: 当前版本号
        """
        self.app_dir = Path(app_dir) if app_dir else Path.cwd()
        self.github_owner = github_owner or os.getenv("UPDATE_GITHUB_OWNER", self.DEFAULT_GITHUB_OWNER)
        self.github_repo = github_repo or os.getenv("UPDATE_GITHUB_REPO", self.DEFAULT_GITHUB_REPO)
        self.github_token = (github_token or os.getenv("UPDATE_GITHUB_TOKEN", "")).strip()
        self.update_server = update_server or f"{self.github_owner}/{self.github_repo}"
        self.current_version = current_version
        self.backup_dir = self.app_dir / "update_backup"
        
        # 更新状态
        self.progress = UpdateProgress(status=UpdateStatus.IDLE)
        self._update_callbacks: List[callable] = []
        
        # 确保备份目录存在
        self.backup_dir.mkdir(parents=True, exist_ok=True)
        
        logger.info(
            "自动更新器初始化: "
            f"app_dir={self.app_dir}, repo={self.github_owner}/{self.github_repo}, version={self.current_version}"
        )
    
    def add_progress_callback(self, callback: callable):
        """添加进度回调"""
        self._update_callbacks.append(callback)
    
    def _notify_progress(self):
        """通知进度更新"""
        for callback in self._update_callbacks:
            try:
                callback(self.progress)
            except Exception as e:
                logger.error(f"进度回调执行失败: {e}")
    
    def _update_progress(self, **kwargs):
        """更新进度"""
        for key, value in kwargs.items():
            if hasattr(self.progress, key):
                setattr(self.progress, key, value)
        self._notify_progress()
    
    def _calculate_file_md5(self, file_path: Path) -> str:
        """计算文件MD5"""
        if not file_path.exists():
            return ""
        
        md5_hash = hashlib.md5()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                md5_hash.update(chunk)
        return md5_hash.hexdigest()
    
    def _is_excluded(self, path: str) -> bool:
        """检查路径是否被排除"""
        path_lower = path.lower().replace('\\', '/')
        for excluded in self.EXCLUDED_PATHS:
            if path_lower.startswith(excluded.lower()) or excluded.lower() in path_lower:
                return True
        return False
    
    def _needs_restart(self, file_path: str) -> bool:
        """检查文件更新是否需要重启"""
        ext = Path(file_path).suffix.lower()
        return ext in self.RESTART_REQUIRED_EXTENSIONS

    def refresh_current_version(self) -> str:
        """从本地版本文件刷新当前版本号，避免长生命周期进程读到旧版本"""
        version = self.current_version or "1.0.0"
        version_file = self.app_dir / "static" / "version.txt"

        try:
            if version_file.exists():
                file_version = version_file.read_text(encoding="utf-8").strip()
                if file_version:
                    version = file_version
        except Exception as e:
            logger.warning(f"读取本地版本文件失败，继续使用缓存版本: {e}")
            return self.current_version

        if version != self.current_version:
            logger.info(f"检测到本地版本变更: {self.current_version} -> {version}")
            self.current_version = version

        return self.current_version

    def _build_request_headers(self, accept_json: bool = True) -> Dict[str, str]:
        """构建 GitHub 请求头"""
        headers = {
            "User-Agent": f"XianyuAutoReplyUpdater/{self.current_version}",
        }
        if accept_json:
            headers["Accept"] = "application/vnd.github+json"
        if self.github_token:
            headers["Authorization"] = f"Bearer {self.github_token}"
        return headers

    def _build_latest_release_url(self) -> str:
        """构建 GitHub 最新 release API 地址"""
        return (
            f"{self.DEFAULT_GITHUB_API_BASE}/repos/"
            f"{self.github_owner}/{self.github_repo}/releases/latest"
        )

    def _build_raw_file_url(self, tag: str, relative_path: str) -> str:
        """构建 GitHub raw 文件地址"""
        relative_path = relative_path.replace("\\", "/").lstrip("/")
        return (
            f"{self.DEFAULT_GITHUB_RAW_BASE}/"
            f"{self.github_owner}/{self.github_repo}/{tag}/{relative_path}"
        )

    def _extract_changelog(self, release_data: Dict[str, Any]) -> List[str]:
        """从 release body 中提取简易更新日志"""
        body = (release_data.get("body") or "").strip()
        if not body:
            return []

        changelog = []
        for raw_line in body.splitlines():
            line = raw_line.strip()
            if not line:
                continue
            if line.startswith(("- ", "* ", "+ ")):
                line = line[2:].strip()
            changelog.append(line)
        return changelog

    def _find_asset_download_url(self, release_data: Dict[str, Any], asset_name: str) -> Optional[str]:
        """查找指定 release asset 的浏览器下载地址"""
        for asset in release_data.get("assets", []) or []:
            if asset.get("name") == asset_name:
                return asset.get("browser_download_url")
        return None
    
    async def check_for_updates(self) -> Optional[UpdateManifest]:
        """
        检查是否有可用更新
        
        Returns:
            UpdateManifest: 更新清单，如果没有更新则返回None
        """
        self.refresh_current_version()
        self._update_progress(status=UpdateStatus.CHECKING, message="正在检查更新...")
        
        try:
            async with aiohttp.ClientSession() as session:
                release_url = self._build_latest_release_url()
                async with session.get(
                    release_url,
                    headers=self._build_request_headers(),
                    timeout=aiohttp.ClientTimeout(total=30)
                ) as response:
                    if response.status != 200:
                        logger.warning(f"获取 GitHub Release 失败: HTTP {response.status}")
                        self._update_progress(status=UpdateStatus.IDLE, message="检查更新失败")
                        return None

                    release_data = await response.json()

                release_tag = (release_data.get("tag_name") or "").strip()
                if not release_tag:
                    logger.warning("GitHub Release 缺少 tag_name，无法检查更新")
                    self._update_progress(status=UpdateStatus.IDLE, message="检查更新失败")
                    return None

                manifest_url = self._find_asset_download_url(release_data, "update_files.json")
                if not manifest_url:
                    manifest_url = self._build_raw_file_url(release_tag, "update_files.json")

                async with session.get(
                    manifest_url,
                    headers=self._build_request_headers(accept_json=False),
                    timeout=aiohttp.ClientTimeout(total=30)
                ) as response:
                    if response.status != 200:
                        logger.warning(f"获取更新清单失败: HTTP {response.status}")
                        self._update_progress(status=UpdateStatus.IDLE, message="检查更新失败")
                        return None

                    manifest_text = await response.text()
                    try:
                        manifest_data = json.loads(manifest_text)
                    except json.JSONDecodeError as exc:
                        logger.warning(f"解析更新清单失败: {exc}")
                        self._update_progress(status=UpdateStatus.IDLE, message="检查更新失败")
                        return None

                manifest_version = manifest_data.get("version") or release_tag
                changelog = manifest_data.get("changelog") or self._extract_changelog(release_data)

                files = []
                for file_info in manifest_data.get("files", []):
                    file_path = file_info["path"].replace("\\", "/")
                    files.append(FileUpdate(
                        path=file_path,
                        md5=file_info.get("md5", ""),
                        size=file_info.get("size", 0),
                        download_url=self._build_raw_file_url(release_tag, file_path),
                        version=file_info.get("version", manifest_version),
                        requires_restart=file_info.get("requires_restart", self._needs_restart(file_path)),
                        description=file_info.get("description", "")
                    ))

                deleted_files = []
                for file_info in manifest_data.get("deleted_files", []):
                    file_path = file_info["path"].replace("\\", "/")
                    deleted_files.append(DeletedFile(
                        path=file_path,
                        requires_restart=file_info.get("requires_restart", self._needs_restart(file_path)),
                        description=file_info.get("description", "")
                    ))

                manifest = UpdateManifest(
                    version=manifest_version,
                    release_date=manifest_data.get("release_date") or release_data.get("published_at", ""),
                    description=manifest_data.get("description") or release_data.get("name") or f"版本 {manifest_version} 更新",
                    files=files,
                    deleted_files=deleted_files,
                    min_version=manifest_data.get("min_version", ""),
                    changelog=changelog
                )

                logger.info(
                    f"发现发布版本: {manifest.version}, 共 {len(files)} 个文件可用于比对更新, "
                    f"{len(deleted_files)} 个文件待删除"
                )
                self._update_progress(status=UpdateStatus.IDLE, message=f"已获取版本 {manifest.version} 的更新清单")

                return manifest
                    
        except asyncio.TimeoutError:
            logger.error("检查更新超时")
            self._update_progress(status=UpdateStatus.FAILED, error="检查更新超时")
            return None
        except Exception as e:
            logger.error(f"检查更新失败: {e}")
            self._update_progress(status=UpdateStatus.FAILED, error=str(e))
            return None
    
    async def get_files_to_update(self, manifest: UpdateManifest) -> List[FileUpdate]:
        """
        获取需要更新的文件列表（排除已是最新的文件）
        
        Args:
            manifest: 更新清单
            
        Returns:
            需要更新的文件列表
        """
        files_to_update = []
        excluded_count = 0
        up_to_date_count = 0
        
        for file_update in manifest.files:
            # 跳过被排除的文件
            if self._is_excluded(file_update.path):
                logger.debug(f"跳过排除的文件: {file_update.path}")
                excluded_count += 1
                continue
            
            local_path = self.app_dir / file_update.path
            
            # 如果服务端没有提供MD5，则始终更新该文件
            if not file_update.md5 or not file_update.md5.strip():
                files_to_update.append(file_update)
                logger.debug(f"需要更新（无MD5校验）: {file_update.path}")
                continue
            
            local_md5 = self._calculate_file_md5(local_path)
            
            # 如果本地文件不存在或MD5不匹配，则需要更新
            if local_md5 != file_update.md5:
                files_to_update.append(file_update)
                logger.debug(f"需要更新: {file_update.path} (本地MD5: {local_md5}, 远程MD5: {file_update.md5})")
            else:
                up_to_date_count += 1

        logger.info(
            f"更新检查完成: 共 {len(manifest.files)} 个文件, 需要更新 {len(files_to_update)} 个, "
            f"已是最新 {up_to_date_count} 个, 排除 {excluded_count} 个"
        )
        
        return files_to_update

    async def get_files_to_delete(self, manifest: UpdateManifest) -> List[DeletedFile]:
        """获取本地实际存在且允许删除的旧文件列表"""
        files_to_delete = []

        for deleted_file in manifest.deleted_files or []:
            if self._is_excluded(deleted_file.path):
                logger.debug(f"跳过排除的删除路径: {deleted_file.path}")
                continue

            local_path = self.app_dir / deleted_file.path
            if local_path.exists() and local_path.is_file():
                files_to_delete.append(deleted_file)
                logger.debug(f"需要删除旧文件: {deleted_file.path}")

        return files_to_delete
    
    # 非关键文件，MD5校验失败时可以继续更新（仅警告不报错）
    NON_CRITICAL_FILES = {'version.txt', 'update_log.txt', 'changelog.txt'}
    
    async def download_file(self, file_update: FileUpdate, session: aiohttp.ClientSession) -> Optional[bytes]:
        """
        下载单个文件
        
        Args:
            file_update: 文件更新信息
            session: aiohttp会话
            
        Returns:
            文件内容，失败返回None
        """
        try:
            async with session.get(file_update.download_url, timeout=aiohttp.ClientTimeout(total=60)) as response:
                if response.status != 200:
                    logger.error(f"下载文件失败: {file_update.path}, HTTP {response.status}")
                    return None
                
                content = await response.read()
                
                # 验证MD5（如果服务端提供了MD5值）
                if file_update.md5 and file_update.md5.strip():
                    downloaded_md5 = hashlib.md5(content).hexdigest()
                    if downloaded_md5 != file_update.md5:
                        # 检查是否为非关键文件
                        file_name = Path(file_update.path).name
                        if file_name in self.NON_CRITICAL_FILES:
                            logger.warning(f"非关键文件MD5不匹配（忽略）: {file_update.path}, 期望: {file_update.md5}, 实际: {downloaded_md5}")
                            # 非关键文件，继续更新
                        else:
                            logger.error(f"文件MD5校验失败: {file_update.path}, 期望: {file_update.md5}, 实际: {downloaded_md5}")
                            return None
                    else:
                        logger.debug(f"文件MD5校验通过: {file_update.path}")
                else:
                    logger.debug(f"跳过MD5校验（服务端未提供）: {file_update.path}")
                
                return content
                
        except Exception as e:
            logger.error(f"下载文件异常: {file_update.path}, {e}")
            return None
    
    def _backup_file(self, file_path: Path) -> bool:
        """
        备份文件
        
        Args:
            file_path: 要备份的文件路径
            
        Returns:
            是否成功
        """
        if not file_path.exists():
            return True
        
        try:
            relative_path = file_path.relative_to(self.app_dir)
            backup_path = self.backup_dir / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}" / relative_path
            backup_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(file_path, backup_path)
            logger.debug(f"备份文件: {file_path} -> {backup_path}")
            return True
        except Exception as e:
            logger.error(f"备份文件失败: {file_path}, {e}")
            return False

    def _cleanup_empty_parent_dirs(self, directory: Path):
        """清理因删除文件产生的空目录"""
        current = directory

        while current != self.app_dir and current != current.parent:
            try:
                current.rmdir()
            except OSError:
                break

            current = current.parent
    
    async def apply_updates(self, files_to_update: List[FileUpdate]) -> Tuple[bool, List[str], bool]:
        """
        应用更新
        
        Args:
            files_to_update: 需要更新的文件列表
            
        Returns:
            (是否成功, 更新的文件列表, 是否需要重启)
        """
        if not files_to_update:
            return True, [], False
        
        updated_files = []
        needs_restart = False
        total_size = sum(f.size for f in files_to_update)
        downloaded_size = 0
        
        self._update_progress(
            status=UpdateStatus.DOWNLOADING,
            total_files=len(files_to_update),
            total_bytes=total_size,
            message=f"正在下载 {len(files_to_update)} 个文件..."
        )
        
        async with aiohttp.ClientSession() as session:
            for index, file_update in enumerate(files_to_update):
                self._update_progress(
                    current_file=file_update.path,
                    current_index=index + 1,
                    message=f"正在下载: {file_update.path}"
                )
                
                # 下载文件
                content = await self.download_file(file_update, session)
                if content is None:
                    self._update_progress(
                        status=UpdateStatus.FAILED,
                        error=f"下载文件失败: {file_update.path}"
                    )
                    return False, updated_files, needs_restart
                
                downloaded_size += len(content)
                self._update_progress(downloaded_bytes=downloaded_size)
                
                # 备份并安装
                local_path = self.app_dir / file_update.path
                
                # 备份旧文件
                if not self._backup_file(local_path):
                    logger.warning(f"备份失败，继续更新: {file_update.path}")
                
                # 确保目录存在
                local_path.parent.mkdir(parents=True, exist_ok=True)
                
                # 写入新文件
                try:
                    self._update_progress(
                        status=UpdateStatus.INSTALLING,
                        message=f"正在安装: {file_update.path}"
                    )
                    
                    with open(local_path, 'wb') as f:
                        f.write(content)
                    
                    updated_files.append(file_update.path)
                    
                    if file_update.requires_restart:
                        needs_restart = True
                    
                    logger.info(f"更新文件成功: {file_update.path}")
                    
                except Exception as e:
                    logger.error(f"写入文件失败: {file_update.path}, {e}")
                    self._update_progress(
                        status=UpdateStatus.FAILED,
                        error=f"安装文件失败: {file_update.path}"
                    )
                    return False, updated_files, needs_restart
        
        # 更新完成
        if needs_restart:
            self._update_progress(
                status=UpdateStatus.RESTART_REQUIRED,
                message=f"更新完成，共更新 {len(updated_files)} 个文件，需要重启应用"
            )
        else:
            self._update_progress(
                status=UpdateStatus.COMPLETED,
                message=f"更新完成，共更新 {len(updated_files)} 个文件"
            )
        
        return True, updated_files, needs_restart

    async def apply_deletions(self, files_to_delete: List[DeletedFile]) -> Tuple[bool, List[str], bool]:
        """
        删除 manifest 中声明的旧文件

        Args:
            files_to_delete: 待删除文件列表

        Returns:
            (是否成功, 删除的文件列表, 是否需要重启)
        """
        if not files_to_delete:
            return True, [], False

        deleted_paths = []
        needs_restart = False

        self._update_progress(
            status=UpdateStatus.INSTALLING,
            total_files=len(files_to_delete),
            message=f"正在清理 {len(files_to_delete)} 个旧文件..."
        )

        for index, deleted_file in enumerate(files_to_delete):
            self._update_progress(
                current_file=deleted_file.path,
                current_index=index + 1,
                message=f"正在删除旧文件: {deleted_file.path}"
            )

            local_path = self.app_dir / deleted_file.path
            if not local_path.exists():
                continue

            if not self._backup_file(local_path):
                logger.warning(f"删除前备份失败，继续删除: {deleted_file.path}")

            try:
                local_path.unlink()
                deleted_paths.append(deleted_file.path)

                if deleted_file.requires_restart:
                    needs_restart = True

                self._cleanup_empty_parent_dirs(local_path.parent)
                logger.info(f"删除旧文件成功: {deleted_file.path}")
            except Exception as e:
                logger.error(f"删除旧文件失败: {deleted_file.path}, {e}")
                self._update_progress(
                    status=UpdateStatus.FAILED,
                    error=f"删除旧文件失败: {deleted_file.path}"
                )
                return False, deleted_paths, needs_restart

        return True, deleted_paths, needs_restart
    
    async def perform_update(self, manifest: Optional[UpdateManifest] = None) -> Dict[str, Any]:
        """
        执行完整的更新流程
        
        Args:
            manifest: 更新清单，如果为None则自动检查
            
        Returns:
            更新结果
        """
        result = {
            "success": False,
            "message": "",
            "updated_files": [],
            "deleted_files": [],
            "needs_restart": False,
            "new_version": ""
        }
        
        try:
            self.refresh_current_version()
            # 检查更新
            if manifest is None:
                manifest = await self.check_for_updates()
            
            if manifest is None:
                result["message"] = "没有可用更新"
                result["success"] = True
                return result
            
            result["new_version"] = manifest.version
            
            # 获取需要更新的文件
            files_to_update = await self.get_files_to_update(manifest)
            files_to_delete = await self.get_files_to_delete(manifest)
            
            if not files_to_update and not files_to_delete:
                result["message"] = "所有文件已是最新"
                result["success"] = True
                return result
            
            logger.info(
                f"开始更新到版本 {manifest.version}: {len(files_to_update)} 个文件更新, "
                f"{len(files_to_delete)} 个文件待删除"
            )
            
            # 应用更新
            success, updated_files, needs_restart = await self.apply_updates(files_to_update)
            deleted_files: List[str] = []

            if success:
                success, deleted_files, delete_restart = await self.apply_deletions(files_to_delete)
                needs_restart = needs_restart or delete_restart
            
            result["success"] = success
            result["updated_files"] = updated_files
            result["deleted_files"] = deleted_files
            result["needs_restart"] = needs_restart
            
            if success:
                message_parts = []
                if updated_files:
                    message_parts.append(f"更新 {len(updated_files)} 个文件")
                if deleted_files:
                    message_parts.append(f"删除 {len(deleted_files)} 个旧文件")

                result["message"] = f"成功{'，'.join(message_parts) if message_parts else '处理变更'}到版本 {manifest.version}"
                if needs_restart:
                    result["message"] += "，需要重启应用生效"
                
                # 更新成功后，保存文件哈希清单（用于以后对比）
                self.save_file_hashes(manifest.version, updated_files, deleted_files)
            else:
                result["message"] = "更新过程中出现错误"
            
            return result
            
        except Exception as e:
            logger.error(f"更新失败: {e}")
            result["message"] = f"更新失败: {str(e)}"
            self._update_progress(status=UpdateStatus.FAILED, error=str(e))
            return result
    
    def get_local_file_hashes(self, file_patterns: List[str] = None) -> Dict[str, str]:
        """
        获取本地文件的MD5哈希值
        
        Args:
            file_patterns: 文件模式列表，默认为常见的可更新文件
            
        Returns:
            {文件路径: MD5哈希}
        """
        if file_patterns is None:
            try:
                from generate_update_manifest import collect_updatable_files

                file_hashes = {}
                for relative_path in collect_updatable_files(self.app_dir):
                    if self._is_excluded(relative_path):
                        continue

                    file_path = self.app_dir / relative_path
                    file_hashes[relative_path] = self._calculate_file_md5(file_path)

                return file_hashes
            except Exception as e:
                logger.debug(f"使用 manifest 扫描规则获取本地哈希失败，回退到后缀模式: {e}")

            file_patterns = [
                '*.py', '*.js', '*.ts', '*.tsx', '*.jsx', '*.vue',
                '*.css', '*.html', '*.txt', '*.json', '*.map',
                '*.yml', '*.yaml', '*.png', '*.jpg', '*.jpeg',
                '*.gif', '*.svg', '*.ico', '*.webp', '*.woff',
                '*.woff2', '*.eot', '*.otf', '*.ttf',
            ]
        
        file_hashes = {}
        
        for pattern in file_patterns:
            for file_path in self.app_dir.rglob(pattern):
                try:
                    relative_path = str(file_path.relative_to(self.app_dir)).replace('\\', '/')
                    
                    # 跳过排除的路径
                    if self._is_excluded(relative_path):
                        continue
                    
                    file_hashes[relative_path] = self._calculate_file_md5(file_path)
                except Exception as e:
                    logger.debug(f"计算文件哈希失败: {file_path}, {e}")
        
        return file_hashes
    
    def cleanup_old_backups(self, keep_days: int = 7):
        """
        清理旧的备份文件
        
        Args:
            keep_days: 保留天数
        """
        try:
            cutoff_time = datetime.now().timestamp() - (keep_days * 24 * 60 * 60)
            
            for backup_dir in self.backup_dir.iterdir():
                if backup_dir.is_dir():
                    if backup_dir.stat().st_mtime < cutoff_time:
                        shutil.rmtree(backup_dir)
                        logger.info(f"清理旧备份: {backup_dir}")
                        
        except Exception as e:
            logger.error(f"清理备份失败: {e}")
    
    def save_file_hashes(
        self,
        version: str,
        updated_files: List[str] = None,
        deleted_files: List[str] = None,
    ):
        """
        保存文件哈希清单到本地
        
        更新完成后调用此方法，记录所有文件的MD5哈希值，
        方便以后对比哪些文件发生了变化。
        
        Args:
            version: 当前版本号
            updated_files: 本次更新的文件列表（可选）
        """
        try:
            hash_file = self.app_dir / "data" / "file_hashes.json"
            hash_file.parent.mkdir(parents=True, exist_ok=True)
            
            # 获取所有可更新文件的哈希
            all_hashes = self.get_local_file_hashes()
            
            # 构建哈希清单
            manifest = {
                "version": version,
                "updated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                "total_files": len(all_hashes),
                "files": all_hashes
            }
            
            # 如果有本次更新的文件列表，单独记录
            if updated_files:
                manifest["last_updated_files"] = updated_files
                manifest["last_updated_count"] = len(updated_files)
            if deleted_files:
                manifest["last_deleted_files"] = deleted_files
                manifest["last_deleted_count"] = len(deleted_files)
            
            # 保存到文件
            with open(hash_file, 'w', encoding='utf-8') as f:
                json.dump(manifest, f, ensure_ascii=False, indent=2)
            
            logger.info(f"已保存文件哈希清单: {hash_file}, 共 {len(all_hashes)} 个文件")
            
        except Exception as e:
            logger.error(f"保存文件哈希清单失败: {e}")
    
    def load_file_hashes(self) -> Optional[Dict[str, Any]]:
        """
        加载本地保存的文件哈希清单
        
        Returns:
            哈希清单字典，如果不存在则返回None
        """
        try:
            hash_file = self.app_dir / "data" / "file_hashes.json"
            
            if not hash_file.exists():
                return None
            
            with open(hash_file, 'r', encoding='utf-8') as f:
                return json.load(f)
                
        except Exception as e:
            logger.error(f"加载文件哈希清单失败: {e}")
            return None
    
    def compare_file_hashes(self) -> Dict[str, Any]:
        """
        比较当前文件与保存的哈希清单
        
        Returns:
            比较结果，包含变化的文件列表
        """
        result = {
            "has_changes": False,
            "saved_version": None,
            "changed_files": [],
            "new_files": [],
            "deleted_files": [],
            "unchanged_files": []
        }
        
        try:
            saved_manifest = self.load_file_hashes()
            
            if saved_manifest is None:
                result["message"] = "没有保存的哈希清单，无法比较"
                return result
            
            result["saved_version"] = saved_manifest.get("version")
            saved_hashes = saved_manifest.get("files", {})
            
            # 获取当前文件哈希
            current_hashes = self.get_local_file_hashes()
            
            # 比较文件
            all_files = set(saved_hashes.keys()) | set(current_hashes.keys())
            
            for file_path in all_files:
                saved_md5 = saved_hashes.get(file_path)
                current_md5 = current_hashes.get(file_path)
                
                if saved_md5 is None:
                    # 新增的文件
                    result["new_files"].append(file_path)
                elif current_md5 is None:
                    # 删除的文件
                    result["deleted_files"].append(file_path)
                elif saved_md5 != current_md5:
                    # 修改的文件
                    result["changed_files"].append({
                        "path": file_path,
                        "old_md5": saved_md5,
                        "new_md5": current_md5
                    })
                else:
                    # 未变化的文件
                    result["unchanged_files"].append(file_path)
            
            result["has_changes"] = bool(result["changed_files"] or result["new_files"] or result["deleted_files"])
            result["message"] = f"比较完成: {len(result['changed_files'])} 个文件修改, {len(result['new_files'])} 个新增, {len(result['deleted_files'])} 个删除"
            
        except Exception as e:
            logger.error(f"比较文件哈希失败: {e}")
            result["message"] = f"比较失败: {str(e)}"
        
        return result


# 全局更新器实例
_updater: Optional[AutoUpdater] = None


def get_updater() -> AutoUpdater:
    """获取全局更新器实例"""
    global _updater
    if _updater is None:
        # 尝试从版本文件读取当前版本
        version = "1.0.0"
        try:
            version_file = Path(__file__).parent / "static" / "version.txt"
            if version_file.exists():
                version = version_file.read_text().strip()
        except:
            pass
        
        _updater = AutoUpdater(current_version=version)
    
    return _updater


def init_updater(app_dir: str = None, update_server: str = None, current_version: str = None) -> AutoUpdater:
    """
    初始化全局更新器
    
    Args:
        app_dir: 应用目录
        update_server: 更新服务器地址
        current_version: 当前版本号
    """
    global _updater
    
    if current_version is None:
        try:
            version_file = Path(app_dir or ".") / "static" / "version.txt"
            if version_file.exists():
                current_version = version_file.read_text().strip()
            else:
                current_version = "1.0.0"
        except:
            current_version = "1.0.0"
    
    _updater = AutoUpdater(
        app_dir=app_dir,
        update_server=update_server,
        current_version=current_version
    )
    
    return _updater
