"""Scan documents and extract text, images, and metadata with certain rules."""

from __future__ import annotations

import argparse
import csv
import html
import json
import logging
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 parse
from pytola_core.config import ConfigMixin

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  # ODT support
except ImportError:
    odf_odt = None

try:
    import ebooklib  # EPUB support
    from ebooklib import epub
except ImportError:
    ebooklib = None

try:
    import markdown  # Markdown to text
except ImportError:
    markdown = None

try:
    import pypdf  # Alternative PDF library
except ImportError:
    pypdf = None

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
cwd = Path.cwd()


class DocscanConfig(ConfigMixin):
    """Configuration for document scanning."""

    # Predefined sets for faster extension checking
    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"])


conf = DocscanConfig()


class Rule:
    """Represents a scanning rule with optimized pattern matching."""

    def __init__(self, rule_data: dict[str, Any]) -> None:
        """Initialize rule from dictionary."""
        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:
                # Use re.ASCII for faster matching when possible
                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]]:
        """Search for pattern in text and return matches."""
        if not text or not self.pattern:
            return []

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

        if self.is_regex and self.compiled_pattern:
            # Regex search
            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:
            # Simple text search
            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]:
        """Get context lines around a match."""
        start = max(0, line_index - self.context_lines)
        end = min(len(lines), line_index + self.context_lines + 1)
        return lines[start:end]


class DocumentScanner:
    """High-performance document scanner with multi-format support."""

    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,
    ) -> None:
        """Initialize scanner with input directory and rules.

        Args:
            input_dir: Directory containing documents to scan
            rules: List of scanning rules
            file_types: List of file extensions to scan
            use_pdf_ocr: Use OCR for PDF files
            use_process_pool: Use process pool instead of thread pool for CPU-intensive tasks
            batch_size: Number of files to process in each batch
        """
        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.results = []
        self.paused = False
        self.paused_event = threading.Event()
        self.paused_event.set()  # Initially not paused
        self.stopped = False
        self._progress_callback = None
        self._executor = None  # Keep reference to executor for forced shutdown

    def set_progress_callback(self, callback: Callable[[int, int], None]) -> None:
        """Set callback function for progress updates.

        Args:
            callback: Function to call with progress (current, total)
        """
        self._progress_callback = callback

    def pause(self) -> None:
        """Pause the scanning process."""
        self.paused = True
        self.paused_event.clear()

    def resume(self) -> None:
        """Resume the scanning process."""
        self.paused = False
        self.paused_event.set()
        logger.info("扫描已恢复")

    def stop(self) -> None:
        """Stop the scanning process."""
        self.stopped = True
        self.paused_event.set()  # Ensure thread can exit
        logger.info("停止扫描...")

    def is_paused(self) -> bool:
        """Check if the scanner is paused."""
        return self.paused

    def is_stopped(self) -> bool:
        """Check if the scanner is stopped."""
        return self.stopped

    def scan(self, threads: int = 4, *, show_progress: bool = False) -> dict[str, Any]:
        """Scan all documents in input directory.

        Orchestrates the complete document scanning process including file collection,
        parallel processing, progress tracking, and result aggregation. Supports
        pause/resume functionality and graceful shutdown.

        Args:
            threads: Number of worker threads/processes for parallel processing
            show_progress: Whether to display periodic progress updates

        Returns
        -------
            Dictionary containing comprehensive scan results including matches,
            processing statistics, and scan metadata
        """
        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:
        """Reset scanner state for new scanning session.

        Initializes the stopped and paused flags, and ensures the pause event
        is properly set to allow immediate execution.
        """
        self.stopped = False
        self.paused = False
        self.paused_event.set()

    def _prepare_scan(self, files: list[Path]) -> list[Path]:
        """Prepare for scanning by logging file collection information.

        Args:
            files: List of files to be scanned

        Returns
        -------
            The same list of files (for method chaining)
        """
        logger.info(f"正在扫描目录：{self.input_dir!s}")
        logger.info(f"发现 {len(files)} 个文件待扫描")
        return files

    def _initialize_results(self, files: list[Path]) -> dict[str, Any]:
        """Initialize the results dictionary with scan metadata.

        Args:
            files: List of files that will be scanned

        Returns
        -------
            Initialized results dictionary with scan information structure
        """
        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]:
        """Execute the main scanning loop with parallel processing.

        Handles the core scanning logic including parallel task submission,
        result processing, pause/resume functionality, and graceful shutdown.

        Args:
            files: List of files to scan
            results: Results dictionary to populate
            threads: Number of worker threads
            show_progress: Whether to show progress updates

        Returns
        -------
            Updated results dictionary with scan outcomes
        """
        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(executor=executor, processed=processed, results=results)

        return results

    def _submit_tasks_to_executor(
        self, executor: ProcessPoolExecutor | ThreadPoolExecutor, files: list[Path]
    ) -> list[Any]:
        """Submit all file scanning tasks to the executor.

        Args:
            executor: The executor to submit tasks to
            files: List of files to scan

        Returns
        -------
            List of submitted futures
        """
        submitted_futures = []
        was_paused = False

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

            # Wait if paused before submitting new tasks
            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:
        """Process results from completed scanning tasks.

        Args:
            submitted_futures: List of futures to process
            results: Results dictionary to update
            files: List of files being scanned
            processed: Counter for processed files (passed by reference)
            show_progress: Whether to show progress updates
        """
        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:
        """Check if execution should continue (not stopped).

        Returns
        -------
            True if execution should continue, False if stopped
        """
        if self.stopped:
            logger.info("用户停止扫描，正在取消剩余任务...")
            return False
        return True

    def _wait_if_paused(self, *, was_paused: bool) -> bool:
        """Wait if scanner is paused, updating pause state.

        Args:
            was_paused: Previous pause state

        Returns
        -------
            Updated pause state
        """
        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[Any], results: dict[str, Any]) -> None:
        """Handle the result from a single future.

        Args:
            future: The future to process
            results: Results dictionary to update
        """
        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("扫描文件时出错：{error}")

    def _update_progress(self, processed: int, files: list[Path], *, show_progress: bool) -> None:
        """Update progress information.

        Args:
            processed: Number of files processed so far
            files: Total list of files
            show_progress: Whether to show progress updates
        """
        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,
        executor: ProcessPoolExecutor | ThreadPoolExecutor,  # noqa: ARG002
        processed: int,
        results: dict[str, Any],
    ) -> None:
        """Finalize execution by shutting down executor and updating results.

        Args:
            executor: The executor to shut down
            processed: Number of files processed
            results: Results dictionary to finalize
        """
        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.stopped:
            logger.info(f"扫描已停止。已处理 {processed} 个文件")
        else:
            logger.info(f"扫描完成。在 {len(results['matches'])} 个文件中找到匹配项")

    def _scan_file_with_pause_check(self, file_path: Path) -> dict[str, Any]:
        """Scan a single file with pause/resume capability.

        Checks for pause and stop signals before processing each file to enable
        responsive control during long scanning operations.

        Args:
            file_path: Path to the file to scan

        Returns
        -------
            Dictionary containing scan results for the file, or empty dict if stopped
        """
        # Check if stopped before processing
        if self.stopped:
            return {}

        # Check if paused before processing
        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]:
        """Collect all files matching the specified types from input directory.

        Recursively searches the input directory for files with extensions matching
        the configured file types. Image files are only included if OCR is enabled.

        Returns
        -------
            List of Path objects representing files to scan (duplicates removed)
        """
        files = []
        image_extensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]

        for ext in self.file_types:
            # If extension is an image format and OCR is not enabled, skip
            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))  # Remove duplicates

    def _check_stop_or_pause(self) -> bool:
        """Check if stopped or paused, and wait if necessary.

        Returns
        -------
            True if should continue, False if stopped
        """
        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]]:
        """Extract content from a file based on its extension.

        Routes to the appropriate extractor method based on file extension.
        Includes pause/stop checks during extraction for long-running operations.

        Args:
            file_path: Path to the file to extract content from
            ext: File extension (lowercase, without dot)

        Returns
        -------
            Tuple of extracted text and metadata dictionary
        """
        # Pre-extraction stop/pause check
        if not self._check_stop_or_pause():
            return "", {}

        # Map file extensions to extractor methods
        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),
            "html": lambda: self._extract_html(file_path),
            "htm": lambda: self._extract_html(file_path),
            "md": lambda: self._extract_markdown(file_path),
        }

        # Add DOC extensions
        for doc_ext in conf.DOC_EXTENSIONS:
            extractors[doc_ext] = lambda p=file_path: self._extract_docx(p)

        # Add XLS extensions
        for xls_ext in conf.XLS_EXTENSIONS:
            extractors[xls_ext] = lambda p=file_path: self._extract_xlsx(p)

        # Add PPT extensions
        for ppt_ext in conf.PPT_EXTENSIONS:
            extractors[ppt_ext] = lambda p=file_path: self._extract_pptx(p)

        # Handle image extensions with OCR check
        for img_ext in conf.IMAGE_EXTENSIONS:
            if self.use_pdf_ocr:
                extractors[img_ext] = lambda p=file_path: self._extract_image(p)
            else:
                extractors[img_ext] = lambda: ("", {})

        # Get extractor or default to text extraction
        extractor = extractors.get(ext, lambda p=file_path: self._extract_text(p))

        return extractor()

    def _apply_rules(self, text: str) -> list[dict[str, Any]]:
        """Apply all scanning rules to the extracted text.

        Iterates through all configured rules and searches for matches in the text.
        Includes pause/stop checks between rule applications.

        Args:
            text: Text content to apply rules to

        Returns
        -------
            List of match dictionaries with rule information
        """
        file_matches = []

        for rule in self.rules:
            # Check stop/pause before each rule
            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]:  # noqa: PLR0911
        """Scan a single file and apply all configured rules.

        Extracts text content from the file based on its format, then applies
        all configured scanning rules to find matches. Supports pause/resume
        functionality for responsive operation.

        Args:
            file_path: Path to the file to scan

        Returns
        -------
            Dictionary containing file path, extracted text, metadata, and rule matches
        """
        # Initial stop/pause check
        if not self._check_stop_or_pause():
            return {}

        file_start_time = time.perf_counter()
        ext = file_path.suffix.lower().lstrip(".")
        text = ""
        metadata = {}

        try:
            # Route to appropriate extractor
            extract_result = self._extract_content(file_path, ext)
            text, metadata = extract_result

        except OSError as e:
            logger.warning(f"无法从 {file_path} 提取文本：{e}")
            return {}
        except ValueError as e:
            logger.warning(f"无法从 {file_path} 提取文本：{e}")
            return {}

        # Post-extraction stop/pause check
        if not self._check_stop_or_pause():
            return {}

        processing_time = time.perf_counter() - file_start_time

        if not text:
            return {}

        # Apply all rules with stop/pause checks
        file_matches = self._apply_rules(text)

        if not file_matches:
            return {}

        # Add processing time to metadata
        metadata["processing_time_seconds"] = round(processing_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."""
        # Try PyMuPDF first (faster)
        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}")

        # Fallback to pypdf
        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 (fastest method)."""
        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: Any, file_path: Path) -> tuple[str, dict[str, Any]]:  # noqa: ANN401
        """Process PDF document and extract text with metadata.

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

        Returns
        -------
            Tuple of extracted text and metadata dictionary
        """
        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 (not stopped/paused).

        Returns
        -------
            True if processing should continue, False if stopped
        """
        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 and extract text.

        Args:
            page: PyMuPDF page object
            page_num: Page number for labeling

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

            pix = page.get_pixmap()  # ty:ignore[unresolved-attribute]
            img_data = pix.tobytes("png")
            image = Image.open(io.BytesIO(img_data))
            text = pytesseract.image_to_string(image)
        else:
            text = page.get_text()  # ty:ignore[unresolved-attribute]

        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 (fallback method)."""
        if not pypdf:
            logger.warning("未安装 pypdf, 跳过 PDF 提取")
            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 (OpenDocument Text) file."""
        if odf_odt is None:
            logger.warning("未安装 odfpy, 跳过 ODT 提取")
            return "", {}

        try:
            doc = odf_odt.load(file_path)
            text = doc.textual_content  # pyright: ignore[reportAttributeAccessIssue]
        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 (Rich Text Format) file."""
        try:
            content = Path(file_path).read_bytes()

            # Simple RTF text extraction (removes control words)
            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
                    # Skip control words
                    while (
                        i < len(content)
                        and content[i] != ord(" ")
                        and content[i] != ord("{")
                        and content[i] != ord("}")
                    ):
                        i += 1
                elif content[i] >= 32 and content[i] <= 126:  # Printable ASCII  # noqa: PLR2004
                    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:
            metadata = {"format": "RTF"}
            return text, metadata

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

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

            # Extract text from all items
            for item in book.get_items():
                if not self._check_stop_or_pause_for_extraction():
                    return "", {}

                if item.get_type() == ebooklib.ITEM_DOCUMENT:  # pyright: ignore[reportAttributeAccessIssue]
                    # Remove HTML tags
                    html_content = item.get_content().decode("utf-8")  # pyright: ignore[reportAttributeAccessIssue]
                    import re

                    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 "",  # pyright: ignore[reportAttributeAccessIssue]
                "author": book.get_metadata("DC", "creator")[0][0] if book.get_metadata("DC", "creator") else "",  # pyright: ignore[reportAttributeAccessIssue]
                "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:
            metadata = {"format": "CSV"}
            return "\n".join(text_parts), metadata

    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()

            # Extract all text content
            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 as e:
            logger.warning(f"提取 XML 时出错：{e}")
            return "", {}
        except Exception as e:  # noqa: BLE001
            # Catch XML parsing errors (ParseError, ExpatError, etc.)
            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:
                # Convert Markdown to HTML then extract text
                html_content = markdown.markdown(content)  # pyright: ignore[reportAttributeAccessIssue]

                text = re.sub(r"<[^>]+>", " ", html_content)
                text = html.unescape(text)
                text = re.sub(r"\s+", " ", text).strip()
            else:
                # Simple Markdown processing
                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 "", {}

        # Extract paragraphs
        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)

        # Extract tables
        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 operations.

        Returns
        -------
            True if should continue, False if stopped
        """
        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"))  # pyright: ignore[reportAttributeAccessIssue]

        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"]

        for encoding in encodings:
            try:
                text = Path(file_path).read_text(encoding=encoding, errors="ignore")
                return text, {"encoding": encoding}
            except UnicodeDecodeError:  # noqa: PERF203
                continue
            else:
                return text, {"encoding": encoding}

        return "", {}


def main() -> None:
    """Run main entry point for document scanner."""
    parser = argparse.ArgumentParser(description="扫描文档并使用特定规则提取文本、图像和元数据。")
    parser.add_argument("input", type=str, nargs="?", default=str(cwd), help="包含待扫描文档的输入目录")
    parser.add_argument("-r", "--rules", type=str, default="rules.json", help="规则文件 (JSON 格式)")
    parser.add_argument("--recursive", action="store_true", help="递归扫描文件")
    parser.add_argument(
        "-f",
        "--file-types",
        help="要扫描的文件类型 (逗号分隔)",
        default="pdf,docx,xlsx,pptx,txt,odt,rtf,epub,csv,xml,html,md,jpg,jpeg,png,gif,bmp,tiff",
    )
    parser.add_argument("--use-pdf-ocr", help="对基于图像的 PDF 文件使用 OCR", action="store_true")
    parser.add_argument("--use-process-pool", help="使用进程池而非线程池 (更适合 CPU 密集型任务)", action="store_true")
    parser.add_argument("-b", "--batch-size", help="每批次处理的文件数量", default=50, type=int)
    parser.add_argument("-t", "--threads", help="并行扫描的线程数", default=4, type=int)
    parser.add_argument("--progress", help="显示进度条", action="store_true")
    parser.add_argument("-v", "--verbose", help="详细输出", action="store_true")

    args = parser.parse_args()

    if args.verbose:
        logger.setLevel(logging.DEBUG)

    t0 = time.perf_counter()
    # Validate input directory
    input_dir = Path(args.input)
    if not input_dir.exists() or not input_dir.is_dir():
        logger.error(f"输入目录不存在：{args.input}")
        return
    logger.info(f"正在扫描目录：{input_dir!s}")

    # Load rules file
    rules_file = Path(args.rules)
    if not rules_file.exists() or not rules_file.is_file():
        rule_files_in_input_dir = list(input_dir.glob("rules*.json"))

        if rule_files_in_input_dir:
            rules_file = rule_files_in_input_dir[0]
        else:
            logger.error(f"规则文件不存在：{args.rules}")
            return
    logger.info(f"使用规则文件：{rules_file!s}")

    try:
        with Path(rules_file).open(encoding="utf-8") as f:
            rules_data = json.load(f)
    except json.JSONDecodeError:
        logger.exception("规则文件中的 JSON 无效")
        return

    # Parse rules
    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"]]
    else:
        logger.error("无效的规则格式。期望包含'rules'键的列表或字典")
        return

    if not rules:
        logger.error("未找到有效规则")
        return

    # Parse file types
    file_types = [ft.strip() for ft in args.file_types.split(",")]

    # Create scanner and run scan
    scanner = DocumentScanner(
        input_dir=input_dir,
        rules=rules,
        file_types=file_types,
        use_pdf_ocr=args.use_pdf_ocr,
        use_process_pool=args.use_process_pool,
        batch_size=args.batch_size,
    )
    results = scanner.scan(threads=args.threads, show_progress=args.progress)

    # Save results to JSON file in input directory
    output_file = input_dir / f"scan_results_{datetime.now(tz=timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
    with Path(output_file).open("w", encoding="utf-8") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)

    logger.info(f"结果已保存至:\t{output_file!s}")
    logger.info(f"总耗时：{round(time.perf_counter() - t0, 2):.2f}秒")


if __name__ == "__main__":
    main()
