"""Filescanner module for filescan - 支持简单搜索和规则扫描."""

from __future__ import annotations

import json
import typing
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path

from PySide2.QtCore import QCoreApplication, QSize, QStringListModel
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (
    QApplication,
    QDialog,
    QFileDialog,
    QHBoxLayout,
    QLabel,
    QMessageBox,
    QPushButton,
    QTableWidget,
    QTableWidgetItem,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)
from pytola_core import StateConfig, StateMachine
from pytola_qtstyles import apply_theme

from .core.config import conf
from .core.database import ScanDatabase
from .core.logger import logger
from .scanners import DocumentScanner, Rule
from .threads.count_thread import CountThread
from .threads.scan_worker import ScanWorker
from .widgets.configdialog import ConfigDialog
from .widgets.filescanner_ui import Ui_FileScanner
from .widgets.filetreeviewer import FileTreeViewer


@dataclass
class FileSearcherMode:
    """FileSearcherMode 数据类.

    Args:
        mode (str): 模式名称.
        enables (typing.List[QWidget]): 要启用的组件.
        disables (typing.List[QWidget]): 要禁用的组件.
    """

    mode: str
    enables: typing.List[QWidget]
    disables: typing.List[QWidget]


class ScannerState(str, Enum):
    """文件扫描器状态枚举.

    Attributes
    ----------
        INIT: 初始化状态
        COUNTING: 文件计数中
        FINISH_COUNTING: 计数完成
        SEARCHING: 搜索中
    """

    INIT = "init"
    COUNTING = "counting"
    FINISH_COUNTING = "finish_counting"
    SEARCHING = "searching"


class FileScanner(QWidget, Ui_FileScanner):
    """文件扫描器主界面 - 支持简单搜索和规则扫描."""

    # 组件名称映射字典（类级别常量）
    WIDGET_NAMES: typing.ClassVar[dict[str, str]] = {
        "open_button": "open_button",
        "scan_button": "scan_button",
        "stop_button": "stop_button",
        "config_button": "config_button",
        "result_button": "result_button",
    }

    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)

        self.config_dialog = ConfigDialog(parent=self)
        self.tree_view = FileTreeViewer(parent=self)
        self.threads = []
        self.folders = conf.SEARCH_FOLDERS
        self.foldersModel = QStringListModel()
        self.foldersModel.setStringList(self.folders)
        self.directory_combobox.setModel(self.foldersModel)
        self.message_list_widget.setAutoScroll(True)

        self.count_thread: typing.Optional[CountThread] = None

        # 规则扫描相关
        self.scan_worker: typing.Optional[ScanWorker] = None
        self.is_scanning = False
        self.scan_results = None

        # 数据库路径
        self.db_path: Path | None = None

        # 连接按钮信号
        self.open_button.clicked.connect(self.open_directory)
        self.scan_button.clicked.connect(self.start_search)
        self.stop_button.clicked.connect(self.stop)
        self.config_button.clicked.connect(self.show_config)
        self.result_button.clicked.connect(self._show_scan_history)

        # 监听目录切换
        self.directory_combobox.currentTextChanged.connect(self._on_directory_changed)

        # 初始化状态机
        self.state_machine = StateMachine(initial_state=ScannerState.INIT, on_state_change=self._on_state_changed)
        self._register_states()

        # 应用初始状态
        self.state_machine.force_set_state(ScannerState.INIT)

        # 初始化时检查数据库
        self._check_database_exists()

        apply_theme(self, conf.DEFAULT_STYLE)

    def _register_states(self) -> None:
        """注册所有状态配置到状态机."""
        # 创建组件名称到实际组件的映射
        widget_map = {
            "open_button": self.open_button,
            "scan_button": self.scan_button,
            "stop_button": self.stop_button,
            "config_button": self.config_button,
            "result_button": self.result_button,
        }

        states_config: typing.Dict[ScannerState, StateConfig] = {
            ScannerState.INIT: StateConfig(
                enables={"open_button", "config_button"},
                disables={"scan_button", "stop_button", "result_button"},
                transitions={"start_counting": ScannerState.COUNTING},
            ),
            ScannerState.COUNTING: StateConfig(
                enables={"stop_button", "config_button"},
                disables={"open_button", "scan_button", "result_button"},
                transitions={
                    "finish_counting": ScannerState.FINISH_COUNTING,
                    "start_searching": ScannerState.SEARCHING,
                },
            ),
            ScannerState.FINISH_COUNTING: StateConfig(
                enables={"open_button", "scan_button"},
                disables={"stop_button"},
                transitions={"start_counting": ScannerState.COUNTING, "start_searching": ScannerState.SEARCHING},
            ),
            ScannerState.SEARCHING: StateConfig(
                enables={"stop_button"},
                disables={"open_button", "scan_button", "config_button", "result_button"},
                transitions={"finish_searching": ScannerState.FINISH_COUNTING},
            ),
        }

        # 批量注册状态
        self.state_machine.register_states(states_config)  # type: ignore[arg-type]

        # 保存组件映射以便后续使用
        self._widget_map = widget_map

    def _on_state_changed(self, old_state: str, new_state: str) -> None:
        """状态变更回调.

        Args:
            old_state: 旧状态
            new_state: 新状态
        """
        state_config = self.state_machine.state_config
        if not state_config:
            return

        # 应用新状态的 UI 配置
        for widget_name in state_config.enables:
            if widget_name in self._widget_map:
                self._widget_map[widget_name].setEnabled(True)

        for widget_name in state_config.disables:
            if widget_name in self._widget_map:
                self._widget_map[widget_name].setEnabled(False)

        logger.debug(f"UI 状态已更新：{old_state} -> {new_state}")

    def load_style(self, style_name: str) -> None:
        """加载并应用主题样式.

        Args:
            style_name: 主题样式名称
        """
        apply_theme(self, style_name)

    def open_directory(self) -> None:
        """获取待搜索文件夹."""
        folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
        if folder:
            if folder in self.folders:
                self.directory_combobox.setCurrentText(folder)
                # 切换目录后检查数据库
                self._check_database_exists()
                return

            self.folders.insert(0, Path(folder).as_posix())
            max_folders = conf.MAX_FOLDERS
            if len(self.folders) > max_folders:
                self.folders = self.folders[:max_folders]
                conf.SEARCH_FOLDERS = self.folders
            self.foldersModel.setStringList(self.folders)
            conf.save_to_json()

        if self.count_thread is None:
            self.count_thread = CountThread(parent=self, widget=self, directory=self.folders[0])
            self.count_thread.start()
            self.state_machine.transition("start_counting")

        # 打开新目录后检查数据库
        self._check_database_exists()

    def start_search(self) -> None:
        """开始搜索 - 支持简单搜索和规则扫描."""
        # 前置检查
        if not self.check_search_prerequisites():
            return

        # 直接使用规则扫描（配置中已包含规则）
        self.start_rule_based_scan()

    def start_rule_based_scan(self) -> None:
        """开始基于规则的文档扫描."""
        input_dir = Path(self.directory_combobox.currentText())

        if not input_dir.exists() or not input_dir.is_dir():
            QMessageBox.warning(self, "错误", "无效的输入目录")
            return

        rules_data = conf.RULES.copy()

        try:
            # 解析规则
            rules = []
            if isinstance(rules_data, list):
                rules = [Rule(rule) for rule in rules_data]
            elif isinstance(rules_data, dict) and "rules" in rules_data:
                rules = [Rule(rule) for rule in rules_data["rules"]]

            if not rules:
                QMessageBox.warning(self, "错误", "没有找到有效的规则")
                return

            # 解析文件类型
            file_types = [ft.strip() for ft in conf.DOC_FILE_TYPES.split(",")]

            # 设置扫描状态
            self.is_scanning = True

            # 更新数据库路径
            input_dir = Path(self.directory_combobox.currentText())
            self.db_path = input_dir / conf.DB_FILENAME

            # 切换到搜索状态
            if not self.state_machine.transition("start_searching"):
                QMessageBox.warning(self, "错误", "无法切换到搜索状态")
                return

            # 创建扫描器
            scanner = DocumentScanner(
                input_dir=input_dir,
                rules=rules,
                file_types=file_types,
                use_pdf_ocr=conf.USE_PDF_OCR,
                use_process_pool=conf.USE_PROCESS_POOL,
                batch_size=conf.BATCH_SIZE,
                save_to_db=conf.ENABLE_DB_PERSISTENCE,
                db_path=self.db_path,
            )

            # 创建工作线程
            self.scan_worker = ScanWorker(scanner, conf.THREADS)
            self.scan_worker.signals.progress_message.connect(self._log_message)
            self.scan_worker.signals.progress_update.connect(self._update_progress)
            self.scan_worker.signals.scan_finished.connect(self._scan_finished)
            self.scan_worker.signals.scan_error.connect(self._scan_error)
            self.scan_worker.start()

            self.update_msg(f"开始规则扫描（使用 {len(rules)} 条规则）...")

        except (ValueError, OSError) as error:
            QMessageBox.critical(self, "错误", f"规则解析失败：{error}")

    def stop(self) -> None:
        """停止搜索."""
        if self.count_thread:
            self.count_thread.flag_quit = True
            self.count_thread = None

        # 停止规则扫描
        if self.scan_worker and self.scan_worker.scanner:
            self.scan_worker.scanner.stop()
            self.is_scanning = False
            self.update_msg("已停止规则扫描")

    def show_config(self) -> None:
        """显示配置对话框."""
        self.config_dialog.show()

    def check_search_prerequisites(self) -> bool:
        """检查搜索前的必要条件."""
        search_path = self.directory_combobox.currentText()

        if not search_path:
            QMessageBox.warning(self, "警告", "请先选择搜索目录!")
            return False

        return True

    def _log_message(self, message: str) -> None:
        """记录日志消息."""
        timestamp = datetime.now(tz=timezone.utc).strftime("%H:%M:%S")
        self.update_msg(f"[{timestamp}] {message}")

    def _update_progress(self, current: int, total: int) -> None:
        """更新进度."""
        if total > 0:
            percentage = int((current / total) * 100)
            self.progress_bar.setValue(percentage)
            self.display_count(current)
            QCoreApplication.processEvents()

    def _scan_finished(self, results: dict) -> None:
        """扫描完成回调."""
        self.scan_results = results
        self.is_scanning = False

        # 切换回计数完成状态
        self.state_machine.transition("finish_searching")

        scan_info = results.get("scan_info", {})
        files_with_matches = scan_info.get("files_with_matches", 0)

        self.update_msg(f"扫描完成！在 {files_with_matches} 个文件中找到匹配项")

        # 扫描完成后重新检查数据库状态（因为可能刚创建了数据库）
        self._check_database_exists()

        self._show_scan_results(results)

    def _scan_error(self, error_msg: str) -> None:
        """扫描错误回调."""
        self.is_scanning = False

        # 扫描失败时也切换回计数完成状态
        self.state_machine.transition("finish_searching")

        self.update_msg(f"扫描错误：{error_msg}")
        QMessageBox.critical(self, "错误", f"扫描失败：{error_msg}")

    def _show_scan_results(self, results: dict) -> None:
        """显示扫描结果."""
        matches = results.get("matches", [])

        if not matches:
            QMessageBox.information(self, "完成", "未找到任何匹配项")
            return

        # 创建结果对话框
        result_dialog = QDialog(self)
        result_dialog.setWindowTitle("扫描结果")
        result_dialog.setMinimumSize(800, 600)

        layout = QVBoxLayout(result_dialog)

        # 统计信息
        scan_info = results.get("scan_info", {})
        info_label = QLabel(
            f"总文件数：{scan_info.get('total_files', 0)}\n"
            f"匹配文件：{len(matches)}\n"
            f"扫描时间：{scan_info.get('scan_time', '')}"
        )
        layout.addWidget(info_label)

        # 结果表格
        table = QTableWidget()
        table.setColumnCount(4)
        table.setHorizontalHeaderLabels(["文件名", "类型", "匹配数", "处理时间 (s)"])
        table.horizontalHeader().setStretchLastSection(True)

        for row, match_data in enumerate(matches):
            file_name = Path(match_data.get("file_path", "")).name
            file_type = match_data.get("file_type", "")
            match_count = len(match_data.get("matches", []))
            proc_time = match_data.get("metadata", {}).get("processing_time_seconds", 0)

            table.setItem(row, 0, QTableWidgetItem(file_name))
            table.setItem(row, 1, QTableWidgetItem(file_type))
            table.setItem(row, 2, QTableWidgetItem(str(match_count)))
            table.setItem(row, 3, QTableWidgetItem(f"{proc_time:.3f}"))

        layout.addWidget(table)

        # 详情文本
        details_label = QLabel("选中文件的匹配详情:")
        layout.addWidget(details_label)

        details_text = QTextEdit()
        details_text.setReadOnly(True)
        details_text.setMaximumHeight(200)
        layout.addWidget(details_text)

        def show_details() -> None:
            selected_rows = table.selectionModel().selectedRows()
            if not selected_rows:
                return

            row = selected_rows[0].row()
            match_data = matches[row]

            details = [
                f"文件：{match_data.get('file_path', '')}",
                f"类型：{match_data.get('file_type', '')}",
                f"大小：{match_data.get('file_size', 0)} bytes\n",
            ]

            for match in match_data.get("matches", []):
                details.extend([
                    f"规则：{match.get('rule_name', '')}",
                    f"描述：{match.get('rule_description', '')}",
                    f"行 {match.get('line_number', 0)}: {match.get('match', '')}",
                    "\n上下文:",
                ])
                details.extend(f"  {ctx_line}" for ctx_line in match.get("context", []))
                details.append("-" * 50)

            details_text.setText("\n".join(details))

        table.itemSelectionChanged.connect(show_details)

        # 保存按钮
        save_btn = QPushButton("保存结果到 JSON")
        save_btn.clicked.connect(lambda: self._save_scan_results(results))
        layout.addWidget(save_btn)

        # 查看历史扫描记录按钮
        history_btn = QPushButton("查看历史扫描记录")
        history_btn.clicked.connect(self._show_scan_history)
        layout.addWidget(history_btn)

        result_dialog.exec_()

    def _save_scan_results(self, results: dict) -> None:
        """保存扫描结果到 JSON 文件."""
        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "保存扫描结果",
            f"scan_results_{datetime.now(tz=timezone.utc).strftime('%Y%m%d_%H%M%S')}.json",
            "JSON Files (*.json)",
        )

        if file_path:
            try:
                with Path(file_path).open("w", encoding="utf-8") as f:
                    json.dump(results, f, indent=2, ensure_ascii=False)
                QMessageBox.information(self, "成功", f"结果已保存到：{file_path}")
            except OSError as error:
                QMessageBox.critical(self, "错误", f"保存失败：{error}")

    def _show_scan_history(self) -> None:
        """显示历史扫描记录."""
        if not self.db_path or not self.db_path.exists():
            QMessageBox.information(self, "提示", "当前目录下没有历史扫描记录")
            return

        try:
            db = ScanDatabase(self.db_path)
            sessions = db.get_scan_sessions()

            if not sessions:
                QMessageBox.information(self, "提示", "没有找到历史扫描记录")
                return

            # 创建历史记录对话框
            history_dialog = QDialog(self)
            history_dialog.setWindowTitle("历史扫描记录")
            history_dialog.setMinimumSize(800, 600)

            layout = QVBoxLayout(history_dialog)

            # 统计信息
            stats = db.get_statistics()
            stats_label = QLabel(
                f"总扫描次数：{stats['total_sessions']}\n"
                f"总匹配文件数：{stats['total_matched_files']}\n"
                f"总匹配项数：{stats['total_matches']}"
            )
            layout.addWidget(stats_label)

            # 历史记录表格
            table = QTableWidget()
            table.setColumnCount(6)
            table.setHorizontalHeaderLabels(["会话 ID", "扫描名称", "扫描时间", "输入目录", "文件数", "匹配文件数"])
            table.horizontalHeader().setStretchLastSection(True)

            for row, session in enumerate(sessions):
                table.insertRow(row)
                table.setItem(row, 0, QTableWidgetItem(str(session["id"])))
                table.setItem(row, 1, QTableWidgetItem(session["session_name"]))
                table.setItem(row, 2, QTableWidgetItem(session["scan_time"]))
                table.setItem(row, 3, QTableWidgetItem(Path(session["input_directory"]).name))
                table.setItem(row, 4, QTableWidgetItem(str(session["total_files"])))
                table.setItem(row, 5, QTableWidgetItem(str(session["files_with_matches"])))

            layout.addWidget(table)

            def load_selected_session() -> None:
                selected_rows = table.selectionModel().selectedRows()
                if not selected_rows:
                    return

                row = selected_rows[0].row()
                session_id = int(table.item(row, 0).text())

                # 加载该次扫描的结果
                results = db.get_session_results(session_id)
                if results:
                    self._show_scan_results(results)
                    history_dialog.close()

            # 按钮组
            btn_layout = QHBoxLayout()

            load_btn = QPushButton("加载选中记录")
            load_btn.clicked.connect(load_selected_session)
            btn_layout.addWidget(load_btn)

            delete_btn = QPushButton("删除选中记录")
            delete_btn.clicked.connect(lambda: self._delete_session(db, table))
            btn_layout.addWidget(delete_btn)

            close_btn = QPushButton("关闭")
            close_btn.clicked.connect(history_dialog.close)
            btn_layout.addWidget(close_btn)

            layout.addLayout(btn_layout)

            history_dialog.exec_()

        except (OSError, ValueError) as e:
            QMessageBox.critical(self, "错误", f"加载历史记录失败：{e}")

    def _delete_session(self, db: ScanDatabase, table: QTableWidget) -> None:
        """删除选中的扫描记录."""
        selected_rows = table.selectionModel().selectedRows()
        if not selected_rows:
            QMessageBox.warning(self, "警告", "请先选择要删除的记录")
            return

        reply = QMessageBox.question(
            self, "确认删除", "确定要删除选中的扫描记录吗？此操作不可恢复。", QMessageBox.Yes | QMessageBox.No
        )

        if reply == QMessageBox.Yes:
            row = selected_rows[0].row()
            session_id = int(table.item(row, 0).text())

            if db.delete_session(session_id):
                QMessageBox.information(self, "成功", "记录已删除")
                table.removeRow(row)
            else:
                QMessageBox.critical(self, "错误", "删除失败")

    def update_files(self, keyword: str, files: str) -> None:
        """更新文件列表."""
        if files:
            self.tree_view.add_files(keyword, files.split(","))

    def display_current_file(self, current_file: str) -> None:
        """显示当前处理的文件."""
        self.current_file_label.setText(f"当前处理文件：{current_file}")

    def display_count(self, count: int) -> None:
        """更新文件计数."""
        self.filecount_label.setText(f"文件数：{count}")

    def change_mode(self, mode: str) -> None:
        """切换 ui 模式（已废弃，请使用 state_machine.transition）.

        Deprecated:
            此方法已废弃，建议使用 state_machine.transition() 方法进行状态转换。
        """
        import warnings

        warnings.warn(
            "change_mode() is deprecated, use state_machine.transition() instead", DeprecationWarning, stacklevel=2
        )
        # 向后兼容：尝试通过状态机进行转换
        # 注意：这只是一种兼容处理，建议直接调用 transition 方法
        mode_mapping = {
            "Init": ScannerState.INIT,
            "Counting": ScannerState.COUNTING,
            "FinishCounting": ScannerState.FINISH_COUNTING,
            "OpenDir": ScannerState.SEARCHING,
        }
        if mode in mode_mapping:
            self.state_machine.force_set_state(mode_mapping[mode])

    def update_msg(self, msg: str) -> None:
        """更新状态栏消息."""
        self.message_list_widget.addItem(msg)
        self.message_list_widget.scrollToBottom()

    def update_count_info(self, count: int) -> None:
        """更新文件计数信息."""
        self.filecount_label.setText(f"文件数：{count}")

    def finish_count(self, count: int) -> None:
        """结束计数."""
        self.filecount_label.setText(f"文件数：{count}")
        self.count_thread = None
        self.state_machine.transition("finish_counting")
        self.update_msg(f"文件统计完成，共 {count} 个文件")

    def _on_directory_changed(self, directory: str) -> None:
        """目录切换时的回调."""
        if directory:
            # 立即更新数据库路径
            self.db_path = Path(directory) / conf.DB_FILENAME
            # 检查数据库是否存在并更新按钮状态
            self._check_database_exists()

    def _check_database_exists(self) -> None:
        """检查数据库是否存在并更新相关按钮状态."""
        # 立即检查并更新按钮状态
        if self.db_path and self.db_path.exists():
            # 数据库存在，启用查看历史记录的按钮
            self.result_button.setEnabled(True)
            self.result_button.setToolTip("查看历史扫描记录")

            # 检查是否有扫描会话记录
            try:
                # 使用独立的数据库连接进行检查，避免跨线程问题
                from pytola_office.filescan.core.database import ScanDatabase as LocalScanDatabase

                db = LocalScanDatabase(self.db_path)
                has_sessions = db.has_scan_sessions()

                # 显式关闭数据库连接，避免在析构函数中跨线程关闭
                db._close()

                if has_sessions:
                    # 有历史记录，将扫描按钮改为"重新扫描"并更换为 refresh 图标
                    self.scan_button.setText("重新扫描")
                    icon = QIcon()
                    icon.addFile(":/assets/icons/refresh.svg", QSize(), QIcon.Normal, QIcon.Off)
                    self.scan_button.setIcon(icon)
                else:
                    # 没有历史记录，恢复为"扫描"和 search 图标
                    self.scan_button.setText("扫描")
                    icon = QIcon()
                    icon.addFile(":/assets/icons/search.svg", QSize(), QIcon.Normal, QIcon.Off)
                    self.scan_button.setIcon(icon)
            except (OSError, ValueError):
                # 数据库检查失败时，保持默认状态
                pass
        else:
            # 数据库不存在，禁用查看历史记录的按钮
            self.result_button.setEnabled(False)
            self.result_button.setToolTip("当前目录下没有历史扫描记录")


def main() -> int:
    """运行文件扫描器."""
    app = QApplication([])

    window = FileScanner()
    window.setWindowTitle("文件扫描器 - 支持规则匹配")
    window.show()

    return app.exec_()


if __name__ == "__main__":
    main()
