"""Filetreeviewer 模块 - 文件树查看器对话框."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, ClassVar, Protocol

from PySide2.QtCore import QModelIndex, QPoint, Qt, QTimer, QUrl
from PySide2.QtGui import QCloseEvent, QDesktopServices, QFont, QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QApplication, QDialog, QMenu, QMessageBox

from ..models.config import conf
from ..models.database import ScanDatabase
from ..models.logger import logger
from .filetreeviewer_ui import Ui_FileTreeViewer

if TYPE_CHECKING:
    from ..gui import FileScanner

# ruff: noqa: ERA001


class FileInfoProtocol(Protocol):
    """文件信息协议,匹配 DirectoryFileInfo 数据类的接口."""

    @property
    def file_path(self) -> str:
        """获取文件路径."""
        ...

    @property
    def file_size(self) -> int:
        """获取文件大小(字节)."""
        ...

    @property
    def scan_status(self) -> str:
        """获取扫描状态 (pending, scanning, completed, error)."""
        ...


class FileTreeViewer(QDialog, Ui_FileTreeViewer):
    """文件树查看器对话框."""

    # 文件类型图标映射(使用 Unicode 字符)
    FILE_ICONS: ClassVar[dict[str, str]] = {
        ".py": "🐍",
        ".js": "📜",
        ".html": "🌐",
        ".css": "🎨",
        ".json": "📋",
        ".xml": "📄",
        ".md": "📝",
        ".txt": "📃",
        ".pdf": "📕",
        ".doc": "📘",
        ".docx": "📘",
        ".xls": "📊",
        ".xlsx": "📊",
        ".ppt": "📽️",
        ".pptx": "📽️",
        ".jpg": "🖼️",
        ".jpeg": "🖼️",
        ".png": "🖼️",
        ".gif": "🖼️",
        ".bmp": "🖼️",
        ".mp3": "🎵",
        ".wav": "🎵",
        ".mp4": "🎬",
        ".avi": "🎬",
        ".mkv": "🎬",
        ".zip": "📦",
        ".rar": "📦",
        ".7z": "📦",
        ".tar": "📦",
        ".gz": "📦",
        ".exe": "⚙️",
        ".bat": "⚙️",
        ".sh": "⚙️",
        ".cpp": "💻",
        ".c": "💻",
        ".h": "💻",
        ".java": "☕",
        ".cs": "#️⃣",
        ".rb": "💎",
        ".php": "🐘",
        ".sql": "🗄️",
        ".db": "🗄️",
        ".sqlite": "🗄️",
    }

    # 文件类别定义(按用途分类)
    FILE_CATEGORIES: ClassVar[dict[str, list[str]]] = {
        "🖼️ 图片文件": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".webp", ".svg", ".tiff", ".psd", ".ai"],
        "📄 文档文件": [
            ".pdf",
            ".doc",
            ".docx",
            ".xls",
            ".xlsx",
            ".ppt",
            ".pptx",
            ".txt",
            ".rtf",
            ".odt",
            ".ods",
            ".odp",
        ],
        "💻 代码文件": [
            ".py",
            ".js",
            ".ts",
            ".jsx",
            ".tsx",
            ".java",
            ".cpp",
            ".c",
            ".h",
            ".cs",
            ".rb",
            ".php",
            ".go",
            ".rs",
            ".swift",
            ".kt",
        ],
        "🌐 网页文件": [".html", ".htm", ".xhtml", ".css", ".scss", ".sass", ".less", ".vue", ".svelte"],
        "📦 压缩文件": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz", ".iso", ".dmg", ".pkg"],
        "🎵 音频文件": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a", ".midi"],
        "🎬 视频文件": [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v", ".mpeg", ".mpg"],
        "📊 数据文件": [".json", ".xml", ".csv", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"],
        "🗄️ 数据库文件": [".db", ".sqlite", ".sqlitedb", ".sql", ".mdb", ".accdb"],
        "📝 文本文件": [".txt", ".md", ".rst", ".tex", ".log"],
        "⚙️ 可执行文件": [".exe", ".msi", ".app", ".deb", ".rpm", ".apk", ".jar"],
        "🔧 脚本文件": [".bat", ".cmd", ".sh", ".bash", ".ps1", ".vbs", ".ahk"],
        "📁 项目文件": [".gitignore", ".dockerignore", "makefile", ".mk", ".pro", ".cmake", ".gradle", ".pom"],
        "🎨 设计文件": [".psd", ".ai", ".eps", ".indd", ".fig", ".sketch", ".xd", ".dwg", ".dxf", ".stl", ".obj"],
        "📋 配置文件": [".ini", ".cfg", ".conf", ".config", ".properties", ".env", ".toml"],
        "🧪 测试文件": [".test", ".spec", ".feature"],
    }

    def __init__(self, parent: FileScanner | None = None) -> None:
        super().__init__(parent=parent)
        self.setupUi(self)
        self.progress_bar.setFormat("正在加载:%p%")

        # 设置字体为微软雅黑粗体(与 FileScanner 保持一致)
        font = QFont(conf.DEFAULT_FONT_FAMILY, conf.DEFAULT_FONT_SIZE)
        if conf.FONT_BOLD:
            font.setBold(True)
        self.setFont(font)

        self.model = None
        self._current_database_path = None
        self._file_count = 0
        self._directory_count = 0
        self._is_loaded = False  # 标记是否已加载数据
        self._view_mode = "category"  # 视图模式:"category"(按类别) 或 "directory"(按目录)

        self.resetModel()

        # 连接按钮信号
        self.open_button.clicked.connect(self.open_file)
        self.locate_button.clicked.connect(self.locate_file)

        # 双击展开/折叠目录
        self.directory_tree.doubleClicked.connect(self.on_tree_double_clicked)

        # 添加右键菜单
        self.directory_tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self.directory_tree.customContextMenuRequested.connect(self._show_context_menu)

        # 添加视图模式切换(使用 QStackedWidget)
        self._setup_stacked_view()

    def _setup_stacked_view(self) -> None:
        """设置堆叠视图,使用不同的 QTreeView 显示目录和分类视图."""
        # 为分类视图设置模型
        self.category_model = QStandardItemModel()
        self.category_model.setHorizontalHeaderLabels(["文件结构"])
        self.category_tree.setModel(self.category_model)
        self.category_tree.setHeaderHidden(True)

        # 确保 stacked_widget 包含两个页面
        if self.stacked_widget.count() == 0:
            self.stacked_widget.addWidget(self.directory_tree)  # 索引 0: 目录视图
            self.stacked_widget.addWidget(self.category_tree)  # 索引 1: 分类视图

        # 连接分类视图的信号
        self.category_tree.doubleClicked.connect(self.on_category_tree_double_clicked)
        self.category_tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self.category_tree.customContextMenuRequested.connect(self._show_category_context_menu)

        # 当前激活的视图索引 (0=目录,1=分类)
        self._current_view_index = 1  # 默认使用分类视图
        self.stacked_widget.setCurrentIndex(1)  # 默认显示分类视图

        # 创建视图切换按钮
        self.toggle_view_button.clicked.connect(self._on_toggle_view_mode)
        self._update_view_mode_button_text()

    def _update_view_mode_button_text(self) -> None:
        """更新视图模式按钮文本."""
        if self._current_view_index == 0:
            self.toggle_view_button.setText("🗂️ 按类别显示")
        else:
            self.toggle_view_button.setText("📁 按目录显示")  # 默认是分类视图,所以按钮显示"按目录显示"

    def _on_toggle_view_mode(self) -> None:
        """切换视图模式."""
        if self._current_view_index == 0:
            # 从目录视图切换到分类视图
            self._current_view_index = 1
            self.stacked_widget.setCurrentIndex(1)
            logger.info("切换到分类视图")

            # 如果已加载数据但分类视图未构建,则构建它
            if self._is_loaded and self.category_model.rowCount() == 0:
                self._build_category_view_if_needed()
        else:
            # 从分类视图切换到目录视图
            self._current_view_index = 0
            self.stacked_widget.setCurrentIndex(0)
            logger.info("切换到目录视图")

        self._update_view_mode_button_text()

    def _build_category_view_if_needed(self) -> None:
        """在需要时构建分类视图."""
        if not self._current_database_path:
            return

        logger.info("首次显示分类视图,正在构建...")
        db_path = ScanDatabase(self._current_database_path)
        try:
            # 数据库已按 file_path 去重,直接使用
            files_info = db_path.get_all_directory_files()
            logger.info(f"从数据库加载了 {len(files_info)} 个唯一文件")

            if files_info:
                self._build_category_view(files_info)
        finally:
            db_path.close()

    def closeEvent(self, event: QCloseEvent) -> None:  # type: ignore[override]
        """关闭窗口事件处理 - 隐藏而非销毁.

        Args:
            event: 关闭事件
        """
        logger.debug("文件树查看器关闭,隐藏窗口")
        event.ignore()  # 忽略关闭事件,阻止窗口被销毁
        self.hide()  # 隐藏窗口
        # 注意:不销毁窗口,保留已构建的树形结构以加快下次打开速度

    def resetModel(self) -> None:
        """重置文件树模型."""
        # 重置目录视图模型
        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(["文件结构"])
        self.directory_tree.setModel(self.model)
        self.directory_tree.setHeaderHidden(True)

        # 重置分类视图模型
        self.category_model = QStandardItemModel()
        self.category_model.setHorizontalHeaderLabels(["文件结构"])
        self.category_tree.setModel(self.category_model)
        self.category_tree.setHeaderHidden(True)

        self._file_count = 0
        self._directory_count = 0
        self._is_loaded = False  # 重置模型时清除加载标记
        self._update_statistics()

    def _update_progress(self, value: int) -> None:
        """更新进度条.

        Args:
            value: 进度值 (0-100)
        """
        # 使用 UI 文件中定义的进度条
        self.progress_bar.setValue(value)
        QApplication.processEvents()  # 处理 UI 事件,防止卡死

    def _remove_progress_bar(self) -> None:
        """隐藏进度条."""
        # 隐藏 UI 文件中定义的进度条
        self.progress_bar.hide()

    def add_files(self, keyword: str, files: list[str]) -> None:
        """添加文件到文件树模型.

        Args:
            keyword: 搜索关键词
            files: 文件路径列表
        """
        if self.model is None:
            msg = "模型未初始化"
            raise RuntimeError(msg)
        root = self.model.invisibleRootItem()
        keyword_item = QStandardItem(f"关键字:[{keyword}]")
        keyword_item.appendRows([QStandardItem(file) for file in files])
        root.appendRow(keyword_item)
        self.directory_tree.setExpanded(root.index(), True)  # noqa: FBT003

    def load_from_database(self, db_path: Path | str) -> bool:
        """从数据库加载文件信息并构建树形结构.

        Args:
            db_path: 数据库文件路径

        Returns
        -------
            是否加载成功
        """
        try:
            db_path = Path(db_path)
            if not db_path.exists():
                QMessageBox.warning(self, "错误", f"数据库文件不存在:{db_path}")
                return False

            # 优化:如果已经加载过同一个数据库,直接显示窗口即可
            if self._is_loaded and self._current_database_path == db_path:
                logger.info(f"数据库已加载过,直接显示:{db_path}")
                self.show()
                self.activateWindow()
                self.raise_()
                return True

            logger.info(f"正在加载数据库:{db_path}")
            self.resetModel()

            # 读取数据库(数据库已按 file_path 去重)
            try:
                db = ScanDatabase(db_path)
                # 获取数据库中的所有文件(不限制 root_path)
                files_info = db.get_all_directory_files()

                logger.info(f"从数据库加载了 {len(files_info)} 个唯一文件")
            finally:
                # 确保关闭数据库连接
                if "db" in locals():
                    db.close()

            if not files_info:
                QMessageBox.information(self, "提示", "数据库中没有文件信息")
                return False

            logger.info(f"从数据库加载了 {len(files_info)} 个文件")

            # 先显示对话框,让 UI 有机会渲染
            self.show()
            QApplication.processEvents()

            # 处理 UI 事件,让进度条有机会显示出来
            QApplication.processEvents()

            # 先设置数据库路径,这样构建分类视图时可以使用
            self._current_database_path = db_path
            self._is_loaded = True  # 标记已加载
            self.setWindowTitle(f"文件浏览器 - {db_path.parent.name}")

            # 构建树形结构- 目录视图
            logger.info("正在按目录结构构建树形结构...")
            self._build_tree_from_files_batch(files_info, db_path.parent)

            # 立即构建分类视图
            logger.info("正在构建分类视图...")
            self._build_category_view(files_info)

            # 延迟移除进度条,让用户看到 100% 的完成状态
            QApplication.processEvents()
            QTimer.singleShot(500, self._remove_progress_bar)  # 500ms 后移除
        except (ValueError, OSError, RuntimeError) as e:
            logger.error(f"加载数据库失败:{e}")
            QMessageBox.critical(self, "错误", f"加载数据库失败:{e}")
            return False
        else:
            return True

    def _get_file_category(self, file_ext: str) -> str:
        """获取文件所属类别.

        Args:
            file_ext: 文件扩展名(小写,带点)

        Returns
        -------
            类别名称
        """
        for category, extensions in self.FILE_CATEGORIES.items():
            if file_ext in extensions:
                return category
        return "📁 其他文件"  # 默认类别

    def _build_tree_from_files_batch(self, files_info: list, root_path: Path) -> None:
        """根据文件信息构建树形结构(分批处理,防止卡死).

        Args:
            files_info: 文件信息列表
            root_path: 根目录路径
        """
        if self.model is None:
            msg = "模型未初始化"
            raise RuntimeError(msg)

        root_item = self.model.invisibleRootItem()

        # 按路径排序
        sorted_files = sorted(files_info, key=lambda x: x.file_path)
        total_files = len(sorted_files)
        batch_size = 1000  # 每批处理 1000 个文件

        # 按目录结构构建(原有逻辑)
        logger.info("正在按目录结构构建树形结构...")
        directories = {}  # 用于缓存目录节点
        # 先构建目录结构(不添加文件)
        self._build_directory_structure(sorted_files, root_path, root_item, directories, batch_size, total_files)
        logger.info(f"目录结构构建完成,共 {self._directory_count} 个目录")
        # 再添加文件到对应的目录
        self._add_files_to_tree(sorted_files, root_path, root_item, directories, batch_size, total_files)
        logger.info(f"文件添加完成,共 {self._file_count} 个文件")

        # 展开第一层目录
        for i in range(root_item.rowCount()):
            self.directory_tree.setExpanded(root_item.child(i).index(), False)  # noqa: FBT003

        self._update_statistics()
        # 注意:不在这里设置 100% 进度,因为可能还要构建分类视图

    def _get_relative_path(self, file_path: Path, root_path: Path) -> Path:
        """获取文件相对路径.

        Args:
            file_path: 文件路径
            root_path: 根路径

        Returns
        -------
            相对路径
        """
        try:
            return file_path.relative_to(root_path)
        except ValueError:
            rel_path_parts = file_path.parts
            return Path(*rel_path_parts) if rel_path_parts else file_path

    def _build_directory_structure(
        self,
        sorted_files: list,
        root_path: Path,
        root_item: QStandardItem,
        directories: dict,
        batch_size: int,
        total_files: int,
    ) -> None:
        """构建目录结构.

        Args:
            sorted_files: 排序后的文件信息列表
            root_path: 根路径
            root_item: 根项目
            directories: 目录缓存字典
            batch_size: 批次大小
            total_files: 总文件数
        """
        for i, file_info in enumerate(sorted_files):
            file_path = Path(file_info.file_path)
            rel_path = self._get_relative_path(file_path, root_path)

            # 构建目录树(只处理目录部分)
            current_path = root_path

            # 处理目录层级
            for part in rel_path.parts[:-1]:
                current_path /= part
                cache_key = str(current_path)

                if cache_key not in directories:
                    dir_icon = "📁"
                    dir_name = f"{dir_icon} {part}"
                    dir_item = QStandardItem(dir_name)
                    dir_item.setData(str(current_path), int(Qt.UserRole))  # type: ignore[arg-type]
                    dir_item.setEditable(False)

                    root_item.appendRow(dir_item)
                    directories[cache_key] = dir_item
                    self._directory_count += 1

            # 每批处理更新进度
            if (i + 1) % batch_size == 0:
                progress = int((i + 1) / total_files * 50)  # 前 50% 用于构建目录
                self._update_progress(progress)
                logger.debug(f"构建目录进度:{progress}% ({i + 1}/{total_files})")

    def _build_category_view(self, files_info: list) -> None:
        """构建分类视图.

        Args:
            files_info: 文件信息列表
        """
        if self.category_model is None:
            msg = "分类模型未初始化"
            raise RuntimeError(msg)

        root_item = self.category_model.invisibleRootItem()
        sorted_files = sorted(files_info, key=lambda x: x.file_path)
        total_files = len(sorted_files)
        batch_size = 1000

        # 按文件类别构建
        logger.info("正在按文件类别构建分类视图...")
        if self._current_database_path:
            self._build_tree_by_category(
                sorted_files,
                self._current_database_path.parent,
                root_item,
                batch_size,
                total_files,
            )

        # 展开第一层类别
        for i in range(root_item.rowCount()):
            self.category_tree.setExpanded(root_item.child(i).index(), False)  # noqa: FBT003

        self._update_statistics()
        # 构建完成后设置进度为 100%
        self._update_progress(100)

    def _build_tree_by_category(
        self,
        sorted_files: list,
        root_path: Path,
        root_item: QStandardItem,
        batch_size: int,
        total_files: int,
    ) -> None:
        """按文件类别构建树形结构.

        Args:
            sorted_files: 排序后的文件信息列表
            root_path: 根路径
            root_item: 根项目
            batch_size: 批次大小
            total_files: 总文件数
        """
        # 按类别分组文件
        categorized_files: dict[str, list] = {}
        for file_info in sorted_files:
            file_path = Path(file_info.file_path)
            file_ext = file_path.suffix.lower()
            category = self._get_file_category(file_ext)

            if category not in categorized_files:
                categorized_files[category] = []
            categorized_files[category].append(file_info)

        # 创建类别节点并添加文件
        logger.info(f"文件分类完成,共 {len(categorized_files)} 个类别")
        processed_count = 0

        for category_name in sorted(categorized_files.keys()):
            category_files = categorized_files[category_name]

            # 创建类别节点
            category_item = QStandardItem(f"{category_name} ({len(category_files)})")
            category_item.setEditable(False)
            category_item.setForeground(Qt.darkBlue)

            # 添加该类别下的所有文件
            for file_info in category_files:
                file_path = Path(file_info.file_path)
                rel_path = self._get_relative_path(file_path, root_path)

                # 创建文件项
                file_item = self._create_file_item(file_path, rel_path, file_info)
                category_item.appendRow(file_item)
                self._file_count += 1

                processed_count += 1
                # 更新进度
                if processed_count % batch_size == 0:
                    progress = int(processed_count / total_files * 100)
                    self._update_progress(progress)
                    logger.debug(f"分类加载进度:{progress}% ({processed_count}/{total_files})")

            root_item.appendRow(category_item)

    def _add_files_to_tree(
        self,
        sorted_files: list,
        root_path: Path,
        root_item: QStandardItem,
        directories: dict,
        batch_size: int,
        total_files: int,
    ) -> None:
        """添加文件到树形结构.

        Args:
            sorted_files: 排序后的文件信息列表
            root_path: 根路径
            root_item: 根项目
            directories: 目录缓存字典
            batch_size: 批次大小
            total_files: 总文件数
        """
        for i, file_info in enumerate(sorted_files):
            file_path = Path(file_info.file_path)
            rel_path = self._get_relative_path(file_path, root_path)

            # 找到父目录
            current_path = root_path
            parent_item = root_item

            for part in rel_path.parts[:-1]:
                current_path /= part
                cache_key = str(current_path)
                if cache_key in directories:
                    parent_item = directories[cache_key]

            # 创建并添加文件项
            file_item = self._create_file_item(file_path, rel_path, file_info)
            parent_item.appendRow(file_item)
            self._file_count += 1

            # 每批处理更新进度
            if (i + 1) % batch_size == 0:
                progress = 50 + int(((i + 1) / total_files) * 50)  # 后 50% 用于添加文件
                self._update_progress(progress)
                logger.debug(f"添加文件进度:{progress}% ({i + 1}/{total_files})")

    def _create_file_item(self, file_path: Path, rel_path: Path, file_info: FileInfoProtocol) -> QStandardItem:
        """创建文件项目.

        Args:
            file_path: 文件路径
            rel_path: 相对路径
            file_info: 文件信息对象

        Returns
        -------
            文件项目
        """
        file_name = rel_path.parts[-1]
        file_ext = Path(file_name).suffix.lower()
        file_icon = self.FILE_ICONS.get(file_ext, "📄")

        # 显示文件大小
        file_size = getattr(file_info, "file_size", 0)
        size_str = self._format_file_size(file_size)

        # 获取分析相关信息
        analyse_status = getattr(file_info, "analyse_status", "pending")
        match_count = getattr(file_info, "match_count", 0)
        processing_time = getattr(file_info, "processing_time_seconds", 0.0)

        # 构建显示名称 - 突出显示匹配信息
        display_name = f"{file_icon} {file_name}"

        # 优先显示匹配数量(更醒目)
        if match_count > 0:
            display_name += f" 🔴 +{match_count}"

        # 添加文件大小
        display_name += f" ({size_str})"

        # 添加分析状态和匹配信息
        if analyse_status == "completed":
            if match_count > 0:
                display_name += f" ✓ {processing_time:.2f}s"
            else:
                display_name += f" ✓ 无匹配 {processing_time:.2f}s"
        elif analyse_status == "analysing":
            display_name += " [⏳ 分析中...]"
        elif analyse_status == "error":
            display_name += " [❌ 错误]"
        # pending 状态不显示额外信息

        file_item = QStandardItem(display_name)
        file_item.setData(str(file_path), int(Qt.UserRole))  # type: ignore[arg-type]
        file_item.setData(file_info, int(Qt.UserRole) + 1)  # type: ignore[arg-type]
        file_item.setEditable(False)

        # 设置工具提示(鼠标悬停时显示详细信息)
        tooltip = self._create_file_tooltip(file_path, file_info)
        file_item.setToolTip(tooltip)

        # 根据分析状态设置颜色
        if analyse_status == "completed":
            if match_count > 0:
                # 有匹配 - 红色(更醒目)
                file_item.setForeground(Qt.red)
                font = file_item.font()
                font.setBold(True)
                file_item.setFont(font)
            else:
                # 无匹配 - 蓝色
                file_item.setForeground(Qt.blue)
        elif analyse_status == "analysing":
            # 分析中 - 黄色
            file_item.setForeground(Qt.yellow)
        elif analyse_status == "error":
            # 错误 - 红色
            file_item.setForeground(Qt.red)
        else:
            # pending - 灰色
            file_item.setForeground(Qt.gray)

        return file_item

    def _create_file_tooltip(self, file_path: Path, file_info: FileInfoProtocol) -> str:
        """创建文件的工具提示(鼠标悬停时显示详细信息).

        Args:
            file_path: 文件路径
            file_info: 文件信息对象

        Returns
        -------
            工具提示字符串
        """
        # 获取分析相关信息
        analyse_status = getattr(file_info, "analyse_status", "pending")
        match_count = getattr(file_info, "match_count", 0)
        processing_time = getattr(file_info, "processing_time_seconds", 0.0)
        file_size = getattr(file_info, "file_size", 0)

        # 构建工具提示内容
        tooltip_parts = [
            f"📄 <b>文件:</b>{file_path.name}",
            f"📍 <b>路径:</b>{file_path.parent}",
            f"📊 <b>大小:</b>{self._format_file_size(file_size)}",
            f"⏱️ <b>处理时间:</b>{processing_time:.3f}秒",
            f"🎯 <b>匹配数:</b><span style='color: red; font-weight: bold;'>{match_count}</span>",
            f"📈 <b>状态:</b>{self._get_status_display(analyse_status)}",
        ]

        # 如果有匹配，显示匹配规则名称
        matches_attr = getattr(file_info, "matches", None)
        if match_count > 0 and matches_attr is not None and matches_attr:
            matches = matches_attr
            if isinstance(matches, list) and len(matches) > 0:
                tooltip_parts.append("\n<b>匹配规则:</b>")
                # 只显示前 5 个规则,避免提示太长
                for i, match in enumerate(matches[:5], 1):
                    if isinstance(match, dict):
                        rule_name = match.get("rule_name", "未知规则")
                    else:
                        rule_name = getattr(match, "rule_name", "未知规则")
                    tooltip_parts.append(f"  {i}. {rule_name}")
                if len(matches) > 5:
                    tooltip_parts.append(f"  ... 还有 {len(matches) - 5} 个匹配")

        return "<br>".join(tooltip_parts)

    def _get_status_display(self, analyse_status: str) -> str:
        """获取状态的显示文本.

        Args:
            analyse_status: 分析状态

        Returns
        -------
            状态显示文本
        """
        status_map = {"pending": "⏳ 待分析", "analysing": "🔄 分析中", "completed": "✅ 已完成", "error": "❌ 错误"}
        return status_map.get(analyse_status, f"❓ {analyse_status}")

    def _format_file_size(self, size: int) -> str:
        """格式化文件大小显示.

        Args:
            size: 字节数

        Returns
        -------
            格式化后的大小字符串
        """
        if size < 1024:
            return f"{size} B"
        if size < 1024 * 1024:
            return f"{size / 1024:.1f} KB"
        if size < 1024 * 1024 * 1024:
            return f"{size / (1024 * 1024):.1f} MB"
        return f"{size / (1024 * 1024 * 1024):.1f} GB"

    def _update_statistics(self) -> None:
        """更新统计信息显示."""
        stats_text = f"目录:{self._directory_count} | 文件:{self._file_count}"
        self.operation_groupbox.setTitle(f"操作 ({stats_text})")

    def open_file(self) -> None:
        """打开选中的文件."""
        # 根据当前激活的视图获取文件路径
        file_name = self.get_current_file_path(from_category=(self._current_view_index == 1))
        if file_name:
            logger.info(f"打开文件:{file_name}")
            QDesktopServices.openUrl(QUrl(f"file:///{file_name}"))

    def locate_file(self) -> None:
        """定位选中文件所在的目录."""
        # 根据当前激活的视图获取文件路径
        file_name = self.get_current_file_path(from_category=(self._current_view_index == 1))
        if file_name:
            folder_path = Path(file_name).parent
            if not folder_path.is_dir():
                QMessageBox.warning(self, "警告", "文件所在目录无效!")
                return

            logger.info(f"定位文件夹:{folder_path}")
            QDesktopServices.openUrl(QUrl(f"file:///{folder_path.as_posix()}"))

    def _get_file_path_from_item(self, item: QStandardItem, view_name: str) -> str:
        """从项目项获取文件路径.

        Args:
            item: 标准项目项
            view_name: 视图名称(用于日志)

        Returns
        -------
            文件路径字符串
        """
        if not item or not item.parent():
            QMessageBox.warning(self, "警告", "请选择文件路径项!")
            return ""

        file_path = item.data(int(Qt.UserRole))  # type: ignore[arg-type]
        if file_path:
            logger.debug(f"从{view_name}视图获取文件路径:{file_path}")
            return str(file_path)

        logger.warning(f"{view_name}视图中选中项没有文件路径数据")
        return ""

    def get_current_file_path(self, *, from_category: bool = False) -> str:
        """获取当前文件路径.

        Args:
            from_category: 是否从分类视图获取

        Returns
        -------
            文件路径字符串
        """
        if from_category:
            if self.category_model is None:
                msg = "分类模型未初始化"
                raise RuntimeError(msg)
            # 使用 category_tree 的 currentIndex 获取选中项
            index = self.category_tree.currentIndex()
            if not index.isValid():
                QMessageBox.warning(self, "警告", "请选择项目!")
                return ""

            item = self.category_model.itemFromIndex(index)
            return self._get_file_path_from_item(item, "分类")
        # 目录视图
        if self.model is None:
            msg = "模型未初始化"
            raise RuntimeError(msg)
        index = self.directory_tree.currentIndex()
        if not index.isValid():
            QMessageBox.warning(self, "警告", "请选择项目!")
            return ""

        item = self.model.itemFromIndex(index)
        return self._get_file_path_from_item(item, "目录")

    def on_tree_double_clicked(self, index: QModelIndex) -> None:
        """双击树形节点处理(目录视图).

        Args:
            index: 点击的索引
        """
        if self.model is None:
            return

        item = self.model.itemFromIndex(index)
        if item and item.parent():  # 如果是文件节点
            # 双击文件节点时打开文件
            file_path = self.get_current_file_path()
            if file_path:
                # 尝试显示该文件的匹配详情
                file_info = item.data(int(Qt.UserRole) + 1)  # type: ignore[arg-type]
                if file_info and hasattr(file_info, "match_count") and getattr(file_info, "match_count", 0) > 0:
                    # 有匹配时显示匹配详情
                    self._show_file_match_details(file_info)
                else:
                    # 没有匹配时直接打开文件
                    self.open_file()

    def _show_file_match_details(self, file_info: FileInfoProtocol) -> None:
        """显示文件的匹配详情.

        Args:
            file_info: 文件信息对象
        """
        from ..models.database import ScanDatabase
        from .matched_previewer import MatchPreviewDialog

        # 获取数据库连接
        db_path = self._current_database_path
        if not db_path:
            QMessageBox.warning(self, "警告", "未找到数据库连接")
            return

        try:
            db = ScanDatabase(db_path)

            # 获取该文件的所有匹配记录
            # 需要先找到 file_id
            file_record = db.get_file_by_path(getattr(file_info, "root_path", ""), getattr(file_info, "file_path", ""))

            if not file_record:
                QMessageBox.warning(self, "警告", "未找到文件记录")
                return

            file_id = file_record.get("id", 0)
            matches = db.get_file_matches(file_id)

            if not matches:
                QMessageBox.information(self, "匹配详情", "该文件没有匹配结果")
                return

            # 创建预览对话框
            dialog = MatchPreviewDialog(self)

            # 显示匹配信息
            dialog.show_matches(
                file_path=getattr(file_info, "file_path", ""),
                file_size=getattr(file_info, "file_size", 0),
                processing_time=getattr(file_info, "processing_time_seconds", 0.0),
                matches=matches,
            )

            dialog.exec_()

        finally:
            if "db" in locals():
                db.close()

    def on_category_tree_double_clicked(self, index: QModelIndex) -> None:
        """双击分类树形节点处理.

        Args:
            index: 点击的索引
        """
        if self.category_model is None:
            return

        item = self.category_model.itemFromIndex(index)
        if item and item.parent():  # 如果是文件节点
            # 双击文件节点时打开文件
            file_path = self.get_current_file_path(from_category=True)
            if file_path:
                # 尝试显示该文件的匹配详情
                file_info = item.data(int(Qt.UserRole) + 1)  # type: ignore[arg-type]
                if file_info and hasattr(file_info, "match_count") and getattr(file_info, "match_count", 0) > 0:
                    # 有匹配时显示匹配详情
                    self._show_file_match_details(file_info)
                else:
                    # 没有匹配时直接打开文件
                    self.open_file()
        # 类别节点的双击由 Qt 自动处理展开/折叠

    def _show_context_menu(self, position: QPoint) -> None:
        """显示右键菜单(目录视图).

        Args:
            position: 鼠标位置
        """
        # 获取当前选中的项
        index = self.directory_tree.currentIndex()
        if not index.isValid():
            return

        if self.model is None:
            return

        item = self.model.itemFromIndex(index)
        if not item or not item.parent():
            # 只有文件节点才显示匹配相关菜单
            return

        context_menu = QMenu(self)

        # 获取文件信息
        file_info = item.data(int(Qt.UserRole) + 1)  # type: ignore[arg-type]
        match_count = getattr(file_info, "match_count", 0) if file_info else 0

        # 添加展开选项
        expand_action = context_menu.addAction("📂 展开所有")
        collapse_action = context_menu.addAction("📁 折叠所有")
        context_menu.addSeparator()

        # 添加匹配相关的操作
        if match_count > 0:
            view_matches_action = context_menu.addAction(f"🔍 查看匹配详情 (🔴 {match_count} 个)")
        else:
            view_matches_action = context_menu.addAction("🔍 查看匹配详情 (无匹配)")
            view_matches_action.setEnabled(False)

        copy_path_action = context_menu.addAction("📋 复制文件路径")

        # 显示菜单并获取用户选择
        viewport = self.directory_tree.viewport()
        global_position = viewport.mapToGlobal(position)
        action = context_menu.exec_(global_position)  # type: ignore[arg-type]

        if action == expand_action:
            self._expand_all()
        elif action == collapse_action:
            self._collapse_all()
        elif action == view_matches_action and file_info:
            self._show_file_match_details(file_info)
        elif action == copy_path_action:
            file_path = item.data(int(Qt.UserRole))  # type: ignore[arg-type]
            if file_path:
                QApplication.clipboard().setText(file_path)
                logger.info(f"已复制文件路径:{file_path}")

    def _show_category_context_menu(self, position: QPoint) -> None:
        """显示分类视图的右键菜单.

        Args:
            position: 鼠标位置
        """
        # 获取当前选中的项
        index = self.category_tree.currentIndex()
        if not index.isValid():
            return

        if self.category_model is None:
            return

        item = self.category_model.itemFromIndex(index)
        if not item or not item.parent():
            # 只有文件节点才显示匹配相关菜单
            return

        context_menu = QMenu(self)

        # 获取文件信息
        file_info = item.data(int(Qt.UserRole) + 1)  # type: ignore[arg-type]
        match_count = getattr(file_info, "match_count", 0) if file_info else 0

        # 添加展开选项
        expand_action = context_menu.addAction("📂 展开所有")
        collapse_action = context_menu.addAction("📁 折叠所有")
        context_menu.addSeparator()

        # 添加匹配相关的操作
        if match_count > 0:
            view_matches_action = context_menu.addAction(f"🔍 查看匹配详情 (🔴 {match_count} 个)")
        else:
            view_matches_action = context_menu.addAction("🔍 查看匹配详情 (无匹配)")
            view_matches_action.setEnabled(False)

        copy_path_action = context_menu.addAction("📋 复制文件路径")

        # 显示菜单并获取用户选择
        viewport = self.category_tree.viewport()
        global_position = viewport.mapToGlobal(position)
        action = context_menu.exec_(global_position)  # type: ignore[arg-type]

        if action == expand_action:
            self._expand_category_all()
        elif action == collapse_action:
            self._collapse_category_all()
        elif action == view_matches_action and file_info:
            self._show_file_match_details(file_info)
        elif action == copy_path_action:
            file_path = item.data(int(Qt.UserRole))  # type: ignore[arg-type]
            if file_path:
                QApplication.clipboard().setText(file_path)
                logger.info(f"已复制文件路径:{file_path}")

    def _expand_all(self) -> None:
        """展开所有节点(目录视图)."""
        if self.model is None:
            return

        root_item = self.model.invisibleRootItem()
        # 只展开第一层目录,避免展开太多导致卡死
        for i in range(root_item.rowCount()):
            child = root_item.child(i)
            if child:
                self.directory_tree.setExpanded(child.index(), True)  # noqa: FBT003
        logger.debug("已展开第一层树节点(目录视图)")

    def _expand_category_all(self) -> None:
        """展开所有节点(分类视图)."""
        if self.category_model is None:
            return

        root_item = self.category_model.invisibleRootItem()
        # 只展开第一层类别,避免展开太多导致卡死
        for i in range(root_item.rowCount()):
            child = root_item.child(i)
            if child:
                self.category_tree.setExpanded(child.index(), True)  # noqa: FBT003
        logger.debug("已展开第一层树节点(分类视图)")

    def _expand_item_recursive(self, item: QStandardItem) -> None:
        """递归展开项目及其子项.

        Args:
            item: 要展开的项目
        """
        self._expand_collapse_item_recursive(item, expand=True)

    def _collapse_all(self) -> None:
        """折叠所有节点(目录视图)."""
        if self.model is None:
            return

        root_item = self.model.invisibleRootItem()
        # 只折叠第一层目录,避免操作太多导致卡死
        for i in range(root_item.rowCount()):
            child = root_item.child(i)
            if child:
                self.directory_tree.setExpanded(child.index(), False)  # noqa: FBT003
        logger.debug("已折叠第一层树节点(目录视图)")

    def _collapse_category_all(self) -> None:
        """折叠所有节点(分类视图)."""
        if self.category_model is None:
            return

        root_item = self.category_model.invisibleRootItem()
        # 只折叠第一层类别,避免操作太多导致卡死
        for i in range(root_item.rowCount()):
            child = root_item.child(i)
            if child:
                self.category_tree.setExpanded(child.index(), False)  # noqa: FBT003
        logger.debug("已折叠第一层树节点(分类视图)")

    def _collapse_item_recursive(self, item: QStandardItem) -> None:
        """递归折叠项目及其子项.

        Args:
            item: 要折叠的项目
        """
        self._expand_collapse_item_recursive(item, expand=False)

    def _expand_collapse_item_recursive(self, item: QStandardItem, *, expand: bool) -> None:
        """递归展开或折叠项目及其子项.

        Args:
            item: 要操作的项目
            expand: True 为展开,False 为折叠
        """
        for i in range(item.rowCount()):
            child = item.child(i)
            if child:
                # 展开或折叠当前项目
                self.directory_tree.setExpanded(child.index(), expand)
                # 递归操作子项目
                self._expand_collapse_item_recursive(child, expand=expand)
