"""Document scanner module for filescan - 支持多种文档格式的内容扫描."""

from __future__ import annotations

import csv
import html
import re
import sys
import threading
import time
from concurrent.futures import Future, ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

from defusedxml.ElementTree import ParseError, parse
from pytola_core.config import ConfigMixin

from ..core.database import ScanDatabase
from ..core.logger import logger

# Optional imports with fallbacks
try:
    import fitz  # PyMuPDF
except ImportError:
    fitz = None

try:
    from docx import Document
except ImportError:
    Document = None

try:
    from openpyxl import load_workbook
except ImportError:
    load_workbook = None

try:
    from PIL import Image
except ImportError:
    Image = None

try:
    import pytesseract
except ImportError:
    pytesseract = None

try:
    import odf.opendocument as odf_odt
except ImportError:
    odf_odt = None

try:
    import ebooklib
    from ebooklib import epub
except ImportError:
    ebooklib = None

try:
    import markdown
except ImportError:
    markdown = None

try:
    import pypdf
except ImportError:
    pypdf = None


class DocscanConfig(ConfigMixin):
    """文档扫描配置类."""

    DOC_EXTENSIONS: frozenset[str] = frozenset(["docx", "doc"])
    XLS_EXTENSIONS: frozenset[str] = frozenset(["xlsx", "xls"])
    PPT_EXTENSIONS: frozenset[str] = frozenset(["pptx", "ppt"])
    IMAGE_EXTENSIONS: frozenset[str] = frozenset(["jpg", "jpeg", "png", "gif", "bmp", "tiff"])


# RTF 提取常量
RTF_PRINTABLE_MIN = 32  # Space character
RTF_PRINTABLE_MAX = 126  # Tilde character (~)


conf = DocscanConfig()


class Rule:
    """表示扫描规则，支持优化的模式匹配."""

    def __init__(self, rule_data: dict[str, Any]) -> None:
        """从字典初始化规则.

        Args:
            rule_data: 包含规则配置的字典
        """
        self.name = rule_data.get("name", "")
        self.pattern = rule_data.get("pattern", "")
        self.is_regex = rule_data.get("regex", False)
        self.case_sensitive = rule_data.get("case_sensitive", False)
        self.context_lines = rule_data.get("context_lines", 3)
        self.description = rule_data.get("description", "")

        if self.is_regex:
            flags = 0 if self.case_sensitive else re.IGNORECASE
            try:
                self.compiled_pattern = re.compile(self.pattern, flags | re.ASCII)
            except re.error as e:
                logger.warning(f"无效的正则表达式模式 '{self.pattern}': {e}")
                self.compiled_pattern = None
        else:
            self.compiled_pattern = None

    def search(self, text: str) -> list[dict[str, Any]]:
        """在文本中搜索模式并返回匹配项.

        Args:
            text: 要搜索的文本

        Returns
        -------
            匹配项列表
        """
        if not text or not self.pattern:
            return []

        matches = []
        lines = text.split("\n")

        if self.is_regex and self.compiled_pattern:
            for line_num, line in enumerate(lines, 1):
                matches.extend(
                    {
                        "type": "regex",
                        "line_number": line_num,
                        "match": match.group(),
                        "start": match.start(),
                        "end": match.end(),
                        "context": self._get_context(lines, line_num - 1),
                    }
                    for match in self.compiled_pattern.finditer(line)
                )
        else:
            search_text = self.pattern if self.case_sensitive else self.pattern.lower()
            for line_num, line in enumerate(lines, 1):
                compare_line = line if self.case_sensitive else line.lower()
                start = 0
                while True:
                    pos = compare_line.find(search_text, start)
                    if pos == -1:
                        break
                    matches.append({
                        "type": "text",
                        "line_number": line_num,
                        "match": line[pos : pos + len(self.pattern)],
                        "start": pos,
                        "end": pos + len(self.pattern),
                        "context": self._get_context(lines, line_num - 1),
                    })
                    start = pos + 1

        return matches

    def _get_context(self, lines: list[str], line_index: int) -> list[str]:
        """获取匹配项周围的上下文行.

        Args:
            lines: 所有文本行
            line_index: 匹配行的索引

        Returns
        -------
            上下文行列表
        """
        start = max(0, line_index - self.context_lines)
        end = min(len(lines), line_index + self.context_lines + 1)
        return lines[start:end]


class DocumentScanner:
    """高性能多格式文档扫描器."""

    def __init__(
        self,
        input_dir: Path,
        rules: list[Rule],
        file_types: list[str],
        *,
        use_pdf_ocr: bool = False,
        use_process_pool: bool = False,
        batch_size: int = 50,
        save_to_db: bool = True,
        db_path: Path | str | None = None,
        session_name: str | None = None,
    ) -> None:
        """初始化扫描器.

        Args:
            input_dir: 包含待扫描文档的目录
            rules: 扫描规则列表
            file_types: 要扫描的文件扩展名列表
            use_pdf_ocr: 对 PDF 文件使用 OCR
            use_process_pool: 使用进程池而非线程池
            batch_size: 每批处理的文件数
            save_to_db: 保存结果到 SQLite 数据库
            db_path: SQLite 数据库文件路径
            session_name: 数据库会话名称
        """
        self.input_dir = Path(input_dir)
        self.rules = rules
        self.file_types = file_types
        self.use_pdf_ocr = use_pdf_ocr
        self.use_process_pool = use_process_pool
        self.batch_size = batch_size
        self.save_to_db = save_to_db
        self.db_path = db_path
        self.session_name = session_name
        self.results = []
        self.paused = False
        self.paused_event = threading.Event()
        self.paused_event.set()
        self.stopped = False
        self._progress_callback = None
        self._executor = None
        self._db_manager: ScanDatabase | None = None

    def set_progress_callback(self, callback: Callable[[int, int], None]) -> None:
        """设置进度回调函数."""
        self._progress_callback = callback

    def pause(self) -> None:
        """暂停扫描过程."""
        self.paused = True
        self.paused_event.clear()

    def resume(self) -> None:
        """恢复扫描过程."""
        self.paused = False
        self.paused_event.set()
        logger.info("扫描已恢复")

    def stop(self) -> None:
        """停止扫描过程."""
        self.stopped = True
        self.paused_event.set()
        logger.info("停止扫描...")

    def is_paused(self) -> bool:
        """检查扫描器是否已暂停."""
        return self.paused

    def is_stopped(self) -> bool:
        """检查扫描器是否已停止."""
        return self.stopped

    def scan(self, threads: int = 4, *, show_progress: bool = False) -> dict[str, Any]:
        """扫描输入目录中的所有文件.

        Args:
            threads: 工作线程/进程数
            show_progress: 是否显示定期进度更新

        Returns
        -------
            包含扫描结果的字典
        """
        self._reset_state()
        files = self._prepare_scan(files=self._collect_files())
        results = self._initialize_results(files)
        return self._execute_scan(files, results, threads, show_progress=show_progress)

    def _reset_state(self) -> None:
        """重置扫描器状态以开始新的扫描会话."""
        self.stopped = False
        self.paused = False
        self.paused_event.set()

    def _prepare_scan(self, files: list[Path]) -> list[Path]:
        """准备扫描，记录文件收集信息."""
        logger.info(f"正在扫描目录：{self.input_dir!s}")
        logger.info(f"发现 {len(files)} 个文件待扫描")
        return files

    def _initialize_results(self, files: list[Path]) -> dict[str, Any]:
        """初始化结果字典."""
        return {
            "scan_info": {
                "input_directory": str(self.input_dir),
                "scan_time": datetime.now(tz=timezone.utc).isoformat(),
                "file_types_scanned": self.file_types,
                "total_files": len(files),
                "rules_count": len(self.rules),
                "use_pdf_ocr": self.use_pdf_ocr,
                "use_process_pool": self.use_process_pool,
            },
            "rules": [{"name": r.name, "pattern": r.pattern, "is_regex": r.is_regex} for r in self.rules],
            "matches": [],
        }

    def _execute_scan(
        self, files: list[Path], results: dict[str, Any], threads: int, *, show_progress: bool
    ) -> dict[str, Any]:
        """使用并行处理执行主扫描循环."""
        processed = 0
        executor_class = ProcessPoolExecutor if self.use_process_pool else ThreadPoolExecutor
        executor = executor_class(max_workers=threads)
        self._executor = executor

        try:
            submitted_futures = self._submit_tasks_to_executor(executor, files)
            self._process_completed_tasks(submitted_futures, results, files, processed, show_progress=show_progress)
        finally:
            self._finalize_execution(processed=processed, results=results)

        return results

    def _submit_tasks_to_executor(
        self, executor: ProcessPoolExecutor | ThreadPoolExecutor, files: list[Path]
    ) -> list[Any]:
        """向执行器提交所有文件扫描任务."""
        submitted_futures = []
        was_paused = False

        for file in files:
            if self.stopped:
                logger.info("用户在提交所有任务之前停止扫描")
                break

            while self.paused:
                if not was_paused:
                    logger.info("扫描已暂停")
                    was_paused = True
                self.paused_event.wait(0.1)
                if self.stopped:
                    logger.info("扫描在暂停状态下停止")
                    break

            if was_paused and not self.paused:
                logger.info("扫描已恢复")
                was_paused = False

            if self.stopped:
                break

            future = executor.submit(self._scan_file_with_pause_check, file)
            submitted_futures.append(future)

        return submitted_futures

    def _process_completed_tasks(
        self,
        submitted_futures: list[Any],
        results: dict[str, Any],
        files: list[Path],
        processed: int,
        *,
        show_progress: bool,
    ) -> None:
        """处理已完成的扫描任务的结果."""
        was_paused = False

        for future in as_completed(submitted_futures):
            if not self._check_continue_execution():
                break

            was_paused = self._wait_if_paused(was_paused=was_paused)

            if not self._check_continue_execution():
                break

            self._handle_future_result(future, results)
            processed += 1
            self._update_progress(processed, files, show_progress=show_progress)

    def _check_continue_execution(self) -> bool:
        """检查是否继续执行."""
        if self.stopped:
            logger.info("用户停止扫描，正在取消剩余任务...")
            return False
        return True

    def _wait_if_paused(self, *, was_paused: bool) -> bool:
        """如果扫描器已暂停则等待."""
        if not was_paused and self.paused:
            logger.info("扫描已暂停")
            was_paused = True

        while self.paused:
            self.paused_event.wait(0.1)
            if self.stopped:
                logger.info("扫描在暂停状态下停止")
                break

        if was_paused and not self.paused:
            logger.info("扫描已恢复")
            was_paused = False

        return was_paused

    def _handle_future_result(self, future: "Future[dict[str, Any]]", results: dict[str, Any]) -> None:
        """处理单个 future 的结果.

        Args:
            future: 包含扫描结果的 Future 对象
            results: 存储匹配结果的字典
        """
        try:
            file_result = future.result(timeout=1.0)
            if file_result and file_result.get("matches"):
                results["matches"].append(file_result)
                logger.info(f"找到匹配项：{Path(file_result.get('file_path', '')).name}")
        except TimeoutError:
            logger.warning("任务超时，扫描可能正在停止")
        except OSError:
            if not self.stopped:
                logger.exception("扫描文件时出错")

    def _update_progress(self, processed: int, files: list[Path], *, show_progress: bool) -> None:
        """更新进度信息."""
        if show_progress and processed % 10 == 0:
            logger.info(f"进度：{processed}/{len(files)} 文件已处理")

        if self._progress_callback:
            self._progress_callback(processed, len(files))

    def _finalize_execution(self, processed: int, results: dict[str, Any]) -> None:
        """通过关闭执行器来结束执行."""
        if self.stopped and self._executor:
            logger.info("强制关闭执行器...")
            if sys.version_info >= (3, 9):
                self._executor.shutdown(wait=False, cancel_futures=True)
            else:
                self._executor.shutdown(wait=False)
        elif self._executor:
            self._executor.shutdown(wait=True)
        self._executor = None

        results["scan_info"]["files_with_matches"] = len(results["matches"])
        results["scan_info"]["files_processed"] = processed
        results["stopped"] = self.stopped

        # 保存到数据库
        if self.save_to_db and not self.stopped:
            try:
                self._save_to_database(results)
            except (OSError, ValueError, RuntimeError) as e:
                # 捕获数据库操作中常见的异常类型
                logger.warning(f"保存数据库失败：{e}")

        if self.stopped:
            logger.info(f"扫描已停止。已处理 {processed} 个文件")
        else:
            logger.info(f"扫描完成。在 {len(results['matches'])} 个文件中找到匹配项")

    def _save_to_database(self, results: dict[str, Any]) -> None:
        """将扫描结果保存到 SQLite 数据库.

        Args:
            results: 扫描结果字典
        """
        try:
            # 确定数据库路径
            db_path = Path(self.db_path) if self.db_path else self.input_dir / "scan_results.db"

            # 检查数据库是否已存在
            db_path.exists()

            # 创建数据库管理器
            self._db_manager = ScanDatabase(db_path)

            # 保存结果
            session_id = self._db_manager.save_scan_results(results, session_name=self.session_name)

            logger.info(f"扫描结果已保存到数据库：{db_path} (会话 ID: {session_id})")

        except (OSError, ValueError, RuntimeError):
            # 捕获数据库操作中常见的异常类型，并重新抛出
            logger.exception("保存扫描结果到数据库时出错")
            raise

    def _scan_file_with_pause_check(self, file_path: Path) -> dict[str, Any]:
        """扫描单个文件，支持暂停/恢复功能."""
        if self.stopped:
            return {}

        while self.paused:
            self.paused_event.wait(0.1)
            if self.stopped:
                return {}

        return self._scan_file(file_path)

    def _collect_files(self) -> list[Path]:
        """收集所有匹配指定类型的文件."""
        files = []
        image_extensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]

        for ext in self.file_types:
            if ext.lower() in image_extensions and not self.use_pdf_ocr:
                continue
            files.extend(self.input_dir.rglob(f"*.{ext.lower()}"))
            files.extend(self.input_dir.rglob(f"*.{ext.upper()}"))
        return list(set(files))

    def _check_stop_or_pause(self) -> bool:
        """检查是否停止或暂停."""
        if self.stopped:
            return False

        while self.paused:
            self.paused_event.wait(0.1)
            if self.stopped:
                return False

        return True

    def _extract_content(self, file_path: Path, ext: str) -> tuple[str, dict[str, Any]]:
        """根据文件扩展名从文件中提取内容."""
        if not self._check_stop_or_pause():
            return "", {}

        ext_lower = ext.lower()

        # 定义文件类型到提取方法的映射
        text_extractors = {
            "pdf": lambda: self._extract_pdf(file_path),
            "odt": lambda: self._extract_odt(file_path),
            "rtf": lambda: self._extract_rtf(file_path),
            "epub": lambda: self._extract_epub(file_path),
            "csv": lambda: self._extract_csv(file_path),
            "xml": lambda: self._extract_xml(file_path),
            "md": lambda: self._extract_markdown(file_path),
        }

        # 检查是否为文本类文件
        if ext_lower in text_extractors:
            return text_extractors[ext_lower]()

        # HTML 文件（支持多个扩展名）
        if ext_lower in {"html", "htm"}:
            return self._extract_html(file_path)

        # 文档类文件委托给专门的方法
        return self._extract_document_file(file_path, ext_lower)

    def _extract_document_file(self, file_path: Path, ext: str) -> tuple[str, dict[str, Any]]:
        """从文档文件 (DOCX, XLSX, PPTX, images) 中提取内容.

        Args:
            file_path: 文件路径
            ext: 小写的文件扩展名

        Returns
        -------
            提取的文本和元组数据
        """
        if ext in conf.DOC_EXTENSIONS:
            return self._extract_docx(file_path)
        if ext in conf.XLS_EXTENSIONS:
            return self._extract_xlsx(file_path)
        if ext in conf.PPT_EXTENSIONS:
            return self._extract_pptx(file_path)
        if ext in conf.IMAGE_EXTENSIONS:
            if self.use_pdf_ocr:
                return self._extract_image(file_path)
            return "", {}

        # 默认作为文本文件处理
        return self._extract_text(file_path)

    def _apply_rules(self, text: str) -> list[dict[str, Any]]:
        """将所有扫描规则应用于提取的文本."""
        file_matches = []

        for rule in self.rules:
            if not self._check_stop_or_pause():
                return []

            rule_matches = rule.search(text)
            if rule_matches:
                for match in rule_matches:
                    match["rule_name"] = rule.name
                    match["rule_description"] = rule.description
                file_matches.extend(rule_matches)

        return file_matches

    def _scan_file(self, file_path: Path) -> dict[str, Any]:
        """扫描单个文件并应用所有配置的规则."""
        if not self._check_stop_or_pause():
            return {}

        file_start_time = time.perf_counter()
        ext = file_path.suffix.lower().lstrip(".")

        # 提取文本内容
        try:
            text, metadata = self._extract_content(file_path, ext)
        except (OSError, ValueError) as e:
            logger.warning(f"无法从 {file_path} 提取文本：{e}")
            return {}

        # 检查是否继续处理
        if not self._check_stop_or_pause() or not text:
            return {}

        # 应用规则扫描
        file_matches = self._apply_rules(text)
        if not file_matches:
            return {}

        # 构建返回结果
        metadata["processing_time_seconds"] = round(processing_time := time.perf_counter() - file_start_time, 3)

        logger.info(
            f"已处理 {file_path.name} ({ext}), "
            f"耗时 {round(processing_time, 3):.3f}秒 "
            f"- 找到 {len(file_matches)} 个匹配项"
        )

        return {
            "file_path": str(file_path),
            "file_type": ext,
            "file_size": file_path.stat().st_size,
            "metadata": metadata,
            "matches": file_matches,
        }

    def _extract_pdf(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from PDF file with fallback."""
        if fitz is not None:
            try:
                return self._extract_pdf_fitz(file_path)
            except OSError as e:
                logger.warning(f"{file_path.name} 的 PyMuPDF 失败：{e}")
            except ValueError as e:
                logger.warning(f"{file_path.name} 的 PyMuPDF 失败：{e}")

        if pypdf is not None:
            try:
                return self._extract_pdf_pypdf(file_path)
            except OSError:
                logger.exception(f"{file_path.name} 的 pypdf 也失败")
                return "", {}
            except ValueError:
                logger.exception(f"{file_path.name} 的 pypdf 也失败")
                return "", {}

        logger.warning("未安装 PDF 库 (pymupdf 或 pypdf)")
        return "", {}

    def _extract_pdf_fitz(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from PDF using PyMuPDF."""
        if not fitz:
            logger.warning("未安装 PyMuPDF")
            return "", {}

        doc = None
        try:
            doc = fitz.open(str(file_path))
            return self._process_pdf_document(doc, file_path)
        except OSError as e:
            if doc:
                doc.close()
            logger.warning(f"PyMuPDF 在 {file_path} 上出错：{e}, 尝试使用 pypdf 回退")
            raise
        except ValueError as e:
            if doc:
                doc.close()
            logger.warning(f"PyMuPDF 在 {file_path} 上出错：{e}, 尝试使用 pypdf 回退")
            raise

    def _process_pdf_document(self, doc: "fitz.Document", file_path: Path) -> tuple[str, dict[str, Any]]:
        """Process PDF document and extract text.

        Args:
            doc: PyMuPDF document object
            file_path: Path to the PDF file

        Returns
        -------
            Tuple of extracted text and metadata
        """
        if doc.page_count == 0:
            logger.warning(f"在 {file_path} 中未找到页面")
            return "", {}
        if not doc.metadata:
            logger.warning(f"在 {file_path} 中未找到元数据")
            return "", {}

        metadata = {
            "page_count": doc.page_count,
            "title": doc.metadata.get("title", ""),
            "author": doc.metadata.get("author", ""),
            "subject": doc.metadata.get("subject", ""),
            "creator": doc.metadata.get("creator", ""),
        }

        text_parts = [
            self._process_pdf_page(page, page_num)
            for page_num, page in enumerate(doc, 1)
            if self._should_continue_processing()
        ]

        doc.close()
        if text_parts:
            return "\n\n".join(text_parts), metadata
        return "", {}

    def _should_continue_processing(self) -> bool:
        """Check if processing should continue."""
        if self.stopped:
            return False

        while self.paused:
            self.paused_event.wait(0.1)
            if self.stopped:
                return False

        return True

    def _process_pdf_page(self, page: "fitz.Page", page_num: int) -> str:
        """Process a single PDF page.

        Args:
            page: PyMuPDF page object
            page_num: Page number

        Returns
        -------
            Extracted text from the page
        """
        if self.use_pdf_ocr and pytesseract and Image:
            import io

            pix = page.get_pixmap()
            img_data = pix.tobytes("png")
            image = Image.open(io.BytesIO(img_data))
            text = pytesseract.image_to_string(image)
        else:
            text = page.get_text()

        return f"[Page {page_num}]\n{text}"

    def _extract_pdf_pypdf(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from PDF using pypdf."""
        if not pypdf:
            logger.warning("未安装 pypdf")
            return "", {}

        text_parts = []
        metadata = {}
        try:
            with Path(file_path).open("rb") as f:
                pdf_reader = pypdf.PdfReader(f)

                if not pdf_reader.metadata:
                    logger.warning(f"在 {file_path} 中未找到元数据")
                    return "", {}

                metadata = {
                    "page_count": len(pdf_reader.pages),
                    "title": pdf_reader.metadata.get("/Title", ""),
                    "author": pdf_reader.metadata.get("/Author", ""),
                }

                for page_num, page in enumerate(pdf_reader.pages, 1):
                    if not self._check_stop_or_pause_for_extraction():
                        return "", {}

                    text = page.extract_text()
                    text_parts.append(f"[Page {page_num}]\n{text}")
        except OSError as e:
            logger.warning(f"使用 pypdf 提取 PDF {file_path} 时出错：{e}")
            return "", {}

        return "\n\n".join(text_parts), metadata

    def _extract_odt(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from ODT file."""
        if odf_odt is None:
            logger.warning("未安装 odfpy, 跳过 ODT 提取")
            return "", {}

        try:
            doc = odf_odt.load(file_path)
            text = doc.textual_content
        except OSError as e:
            logger.warning(f"提取 ODT 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 ODT 时出错：{e}")
            return "", {}
        else:
            return text, {"format": "ODT"}

    def _extract_rtf(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from RTF file."""
        try:
            content = Path(file_path).read_bytes()

            text = ""
            i = 0
            while i < len(content):
                if content[i] == ord("\\") and i + 1 < len(content):
                    if content[i + 1] in {ord("'"), ord("*"), ord("\\")}:
                        i += 2
                        continue
                    while (
                        i < len(content)
                        and content[i] != ord(" ")
                        and content[i] != ord("{")
                        and content[i] != ord("}")
                    ):
                        i += 1
                elif RTF_PRINTABLE_MIN <= content[i] <= RTF_PRINTABLE_MAX:
                    text += chr(content[i])
                i += 1
        except OSError as e:
            logger.warning(f"提取 RTF 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 RTF 时出错：{e}")
            return "", {}
        else:
            return text, {"format": "RTF"}

    def _extract_epub(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from EPUB file."""
        if ebooklib is None:
            logger.warning("未安装 ebooklib, 跳过 EPUB 提取")
            return "", {}

        try:
            book = epub.read_epub(file_path)
            text_parts = []

            for item in book.get_items():
                if not self._check_stop_or_pause_for_extraction():
                    return "", {}

                if item.get_type() == ebooklib.ITEM_DOCUMENT:
                    html_content = item.get_content().decode("utf-8")
                    text = re.sub(r"<[^>]+>", " ", html_content)
                    text = html.unescape(text)
                    text_parts.append(text)
        except OSError as e:
            logger.warning(f"提取 EPUB 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 EPUB 时出错：{e}")
            return "", {}
        else:
            metadata = {
                "title": book.get_metadata("DC", "title")[0][0] if book.get_metadata("DC", "title") else "",
                "author": book.get_metadata("DC", "creator")[0][0] if book.get_metadata("DC", "creator") else "",
                "format": "EPUB",
            }
            return "\n\n".join(text_parts), metadata

    def _extract_csv(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from CSV file."""
        try:
            text_parts = []
            with Path(file_path).open(encoding="utf-8", errors="ignore") as f:
                reader = csv.reader(f)
                for row in reader:
                    if not self._check_stop_or_pause_for_extraction():
                        return "", {}

                    row_text = " | ".join(str(cell) for cell in row)
                    text_parts.append(row_text)
        except OSError as e:
            logger.warning(f"提取 CSV 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 CSV 时出错：{e}")
            return "", {}
        else:
            return "\n".join(text_parts), {"format": "CSV"}

    def _extract_xml(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from XML file."""
        try:
            tree = parse(file_path)
            root = tree.getroot()

            text_parts = [elem.text for elem in root.iter() if elem.text and elem.text.strip()]
            text = "\n".join(text_parts)

            metadata = {"format": "XML", "root_tag": root.tag}
        except OSError as e:
            logger.warning(f"提取 XML 时出错：{e}")
            return "", {}
        except (ValueError, ParseError) as e:
            # 包括 defusedxml 的 ParseError
            logger.warning(f"提取 XML 时解析失败：{e}")
            return "", {}
        except RuntimeError as e:
            # 捕获其他可能的运行时错误
            logger.warning(f"提取 XML 时发生错误：{e}")
            return "", {}
        else:
            return text, metadata

    def _extract_html(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from HTML file."""
        try:
            html_content = Path(file_path).read_text(encoding="utf-8", errors="ignore")

            text = re.sub(r"<[^>]+>", " ", html_content)
            text = html.unescape(text)
            text = re.sub(r"\s+", " ", text).strip()
        except OSError as e:
            logger.warning(f"提取 HTML 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 HTML 时出错：{e}")
            return "", {}
        else:
            return text, {"format": "HTML"}

    def _extract_markdown(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from Markdown file."""
        try:
            content = Path(file_path).read_text(encoding="utf-8", errors="ignore")

            if markdown:
                html_content = markdown.markdown(content)
                text = re.sub(r"<[^>]+>", " ", html_content)
                text = html.unescape(text)
                text = re.sub(r"\s+", " ", text).strip()
            else:
                text = content
        except OSError as e:
            logger.warning(f"提取 Markdown 时出错：{e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"提取 Markdown 时出错：{e}")
            return "", {}
        else:
            return text, {"format": "Markdown"}

    def _extract_docx(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from DOCX file."""
        if Document is None:
            logger.warning("未安装 python-docx, 跳过 DOCX 提取")
            return "", {}

        text_parts = []
        try:
            doc = Document(str(file_path))
        except (OSError, ValueError) as e:
            logger.warning(f"提取 DOCX 时出错：{e}")
            return "", {}

        for paragraph in doc.paragraphs:
            if not self._check_stop_or_pause_for_extraction():
                doc.close()  # ty:ignore[unresolved-attribute]
                return "", {}
            text_parts.append(paragraph.text)

        for table in doc.tables:
            if not self._check_stop_or_pause_for_extraction():
                doc.close()  # ty:ignore[unresolved-attribute]
                return "", {}
            for row in table.rows:
                row_text = " | ".join(cell.text for cell in row.cells)
                text_parts.append(row_text)

        metadata = {"paragraph_count": len(doc.paragraphs), "table_count": len(doc.tables)}
        doc.close()  # ty:ignore[unresolved-attribute]
        return "\n".join(text_parts), metadata

    def _check_stop_or_pause_for_extraction(self) -> bool:
        """Check stop/pause status during extraction."""
        if self.stopped:
            return False

        while self.paused:
            self.paused_event.wait(0.1)
            if self.stopped:
                return False

        return True

    def _extract_xlsx(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from XLSX file."""
        if load_workbook is None:
            logger.warning("未安装 openpyxl, 跳过 XLSX 提取")
            return "", {}

        wb = load_workbook(file_path, read_only=True, data_only=True)
        text_parts = []

        for sheet_name in wb.sheetnames:
            if not self._check_stop_or_pause_for_extraction():
                wb.close()
                return "", {}

            sheet = wb[sheet_name]
            text_parts.append(f"[Sheet: {sheet_name}]")
            for row in sheet.iter_rows(values_only=True):
                if not self._check_stop_or_pause_for_extraction():
                    wb.close()
                    return "", {}

                row_text = " | ".join(str(cell) if cell is not None else "" for cell in row)
                if row_text.strip():
                    text_parts.append(row_text)

        metadata = {"sheet_count": len(wb.sheetnames), "sheets": wb.sheetnames}
        wb.close()
        return "\n".join(text_parts), metadata

    def _extract_pptx(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from PPTX file."""
        try:
            from pptx import Presentation
        except ImportError:
            logger.warning("未安装 python-pptx, 跳过 PPTX 提取")
            return "", {}

        prs = Presentation(str(file_path))
        text_parts = []

        for slide_num, slide in enumerate(prs.slides, 1):
            if not self._check_stop_or_pause_for_extraction():
                return "", {}

            text_parts.append(f"[Slide {slide_num}]")
            text_parts.extend(shape.text for shape in slide.shapes if hasattr(shape, "text"))

        metadata = {"slide_count": len(prs.slides)}
        return "\n".join(text_parts), metadata

    def _extract_image(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from image file using OCR."""
        if Image is None or pytesseract is None:
            logger.warning("未安装 PIL 或 pytesseract, 跳过图像 OCR")
            return "", {}

        try:
            img = Image.open(file_path)
            text = pytesseract.image_to_string(img)
            metadata = {"format": img.format, "mode": img.mode, "size": img.size}
        except OSError as e:
            logger.warning(f"无法对 {file_path} 执行 OCR: {e}")
            return "", {}
        except ValueError as e:
            logger.warning(f"无法对 {file_path} 执行 OCR: {e}")
            return "", {}
        else:
            return text, metadata

    def _extract_text(self, file_path: Path) -> tuple[str, dict[str, Any]]:
        """Extract text from plain text file."""
        encodings = ["utf-8", "latin-1", "cp1252", "utf-16"]

        # 尝试 UTF-8 作为首选编码（最常见）
        try:
            text = Path(file_path).read_text(encoding="utf-8", errors="ignore")
        except UnicodeDecodeError:
            pass
        else:
            return text, {"encoding": "utf-8"}

        # 对其他编码使用循环，先收集所有可能的文本结果
        for encoding in encodings[1:]:
            text = Path(file_path).read_text(encoding=encoding, errors="ignore")
            if text:  # 如果成功读取文本，直接返回
                return text, {"encoding": encoding}

        return "", {}
