import re
import base64
from bs4 import BeautifulSoup, NavigableString
from PyQt6.QtGui import QMovie
from PyQt6.QtWidgets import (
    QDialog,
    QTreeWidget,
    QTreeWidgetItem,
    QTextEdit,
    QPushButton,
    QVBoxLayout,
    QHBoxLayout,
    QDialogButtonBox,
    QSplitter,
    QWidget,
    QCheckBox,
    QTreeWidgetItemIterator,
    QLabel,
    QMenu,
)
from PyQt6.QtCore import (
    Qt,
    QThread,
    pyqtSignal,
    QSize,
)
from abogen.utils import (
    detect_encoding,
    get_resource_path,
)
from abogen.book_parser import get_book_parser
from abogen.domain.metadata_extraction import (
    extract_book_metadata_epub,
    extract_book_metadata_pdf,
    extract_book_metadata_markdown,
    format_metadata_tags,
)

from abogen.subtitle_utils import (
    clean_text,
    calculate_text_length,
)

import os
import logging
import urllib.parse
import textwrap

# Setup logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

_HTML_TAG_PATTERN = re.compile(r"<[^>]+>")
_LEADING_DASH_PATTERN = re.compile(r"^\s*[-–—]\s*")
_LEADING_SIMPLE_DASH_PATTERN = re.compile(r"^\s*-\s*")


class HandlerDialog(QDialog):
    # Class variables to remember checkbox states between dialog instances
    _save_chapters_separately = False
    _merge_chapters_at_end = True
    _save_as_project = False  # New class variable for save_as_project option

    # Cache for processed book content to avoid reprocessing
    # Key: (book_path, modification_time, file_type)
    # Value: dict with content_texts, content_lengths, doc_content (for epub), markdown_toc (for markdown)
    _content_cache = {}

    class _LoaderThread(QThread):
        """Minimal QThread that runs a callable and emits an error string on exception."""

        error = pyqtSignal(str)

        def __init__(self, target_callable):
            super().__init__()
            self._target = target_callable

        def run(self):
            try:
                self._target()
            except Exception as e:
                self.error.emit(str(e))

    @classmethod
    def clear_content_cache(cls, book_path=None):
        """Clear the content cache. If book_path is provided, only clear that book's cache."""
        if book_path is None:
            cls._content_cache.clear()
            logging.info("Cleared all content cache")
        else:
            keys_to_remove = [
                key for key in cls._content_cache.keys() if key[0] == book_path
            ]
            for key in keys_to_remove:
                del cls._content_cache[key]
            if keys_to_remove:
                logging.info(f"Cleared content cache for {os.path.basename(book_path)}")

    def __init__(self, book_path, file_type=None, checked_chapters=None, parent=None):
        super().__init__(parent)

        # Normalize path
        book_path = os.path.normpath(os.path.abspath(book_path))
        self.book_path = book_path

        # Initialize Parser
        try:
            # Factory handles file type detection if file_type is None
            self.parser = get_book_parser(book_path, file_type=file_type)
            # Parser loads automatically in init now
        except Exception as e:
            logging.error(f"Failed to initialize parser for {book_path}: {e}")
            raise

        # Extract book name from file path
        book_name = os.path.splitext(os.path.basename(book_path))[0]

        # Set window title based on file type and book name
        item_type = "Chapters" if self.parser.file_type in ["epub", "markdown"] else "Pages"
        self.setWindowTitle(f"Select {item_type} - {book_name}")
        self.resize(1200, 900)
        self._block_signals = False  # Flag to prevent recursive signals
        # Configure window: remove help button and allow resizing
        self.setWindowFlags(
            Qt.WindowType.Window
            | Qt.WindowType.WindowCloseButtonHint
            | Qt.WindowType.WindowMaximizeButtonHint
        )
        self.setWindowModality(Qt.WindowModality.NonModal)
        # Initialize save chapters flags from class variables
        self.save_chapters_separately = HandlerDialog._save_chapters_separately
        self.merge_chapters_at_end = HandlerDialog._merge_chapters_at_end
        self.save_as_project = HandlerDialog._save_as_project

        # Initialize metadata dict; will be populated in _preprocess_content by the background loader
        self.book_metadata = {}

        # Initialize UI elements that are used in other methods
        self.save_chapters_checkbox = None
        self.merge_chapters_checkbox = None

        # Build treeview
        self.treeWidget = QTreeWidget(self)
        self.treeWidget.setHeaderHidden(True)
        self.treeWidget.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
        self.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.treeWidget.customContextMenuRequested.connect(self.on_tree_context_menu)

        # Initialize checked_chapters set
        self.checked_chapters = set(checked_chapters) if checked_chapters else set()

        # For storing content and lengths (will be filled by background loader)
        self.content_texts = {}
        self.content_lengths = {}
        # Also maintain refs for structure
        self.processed_nav_structure = []

        # Add a placeholder "Information" item so the tree isn't empty immediately
        info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
        info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo")
        info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
        font = info_item.font(0)
        font.setBold(True)
        info_item.setFont(0, font)

        # Setup UI now so dialog appears immediately
        self._setup_ui()

        # Create a centered loading overlay and show it while background load runs
        self._create_loading_overlay()
        # Hide the main UI so only the overlay is visible initially
        if getattr(self, "splitter", None) is not None:
            self.splitter.setVisible(False)
        self._show_loading_overlay("Loading...")

        # Start background loading of book content so the dialog opens immediately
        self._start_background_load()

        # Hide expand/collapse decoration if there are no parent items
        has_parents = False
        for i in range(self.treeWidget.topLevelItemCount()):
            if self.treeWidget.topLevelItem(i).childCount() > 0:
                has_parents = True
                break
        self.treeWidget.setRootIsDecorated(has_parents)

    def _create_loading_overlay(self):
        """Create a centered loading indicator with a GIF on the left and text on the right.

        The indicator is added to the dialog's main layout above the splitter so
        when the splitter is hidden only the indicator is visible.
        """
        try:
            # Container to hold gif + text and allow centering via stretches
            container = QWidget(self)
            container.setVisible(False)
            h = QHBoxLayout(container)
            h.setContentsMargins(0, 8, 0, 8)
            h.setSpacing(10)

            # Left: GIF label (animated)
            gif_label = QLabel(container)
            gif_label.setVisible(False)

            loading_gif_path = get_resource_path("abogen.assets", "loading.gif")
            movie = None
            if loading_gif_path:
                try:
                    movie = QMovie(loading_gif_path)
                    # Make GIF smaller so it doesn't dominate the text
                    movie.setScaledSize(QSize(25, 25))
                    gif_label.setMovie(movie)
                    gif_label.setFixedSize(25, 25)
                    gif_label.setVisible(True)
                except Exception:
                    movie = None

            # Right: Text label
            text_label = QLabel(container)
            text_label.setStyleSheet("font-size: 14pt;")

            # Add stretches to center the content horizontally
            h.addStretch(1)
            h.addWidget(gif_label, 0, Qt.AlignmentFlag.AlignVCenter)
            h.addWidget(text_label, 0, Qt.AlignmentFlag.AlignVCenter)
            h.addStretch(1)

            # Insert at top of main layout if present, otherwise keep as child
            try:
                layout = self.layout()
                if layout is not None:
                    layout.insertWidget(0, container)
            except Exception:
                pass

            # Store refs
            self._loading_container = container
            self._loading_gif_label = gif_label
            self._loading_text_label = text_label
            self._loading_movie = movie
        except Exception:
            self._loading_container = None
            self._loading_gif_label = None
            self._loading_text_label = None
            self._loading_movie = None

    def _show_loading_overlay(self, text: str):
        container = getattr(self, "_loading_container", None)
        text_lbl = getattr(self, "_loading_text_label", None)
        movie = getattr(self, "_loading_movie", None)
        gif_lbl = getattr(self, "_loading_gif_label", None)
        if container is None or text_lbl is None:
            return
        text_lbl.setText(text)
        if movie is not None and gif_lbl is not None:
            try:
                movie.start()
                gif_lbl.setVisible(True)
            except Exception:
                pass
        container.setVisible(True)

    def _hide_loading_overlay(self):
        container = getattr(self, "_loading_container", None)
        movie = getattr(self, "_loading_movie", None)
        if container is None:
            return
        if movie is not None:
            try:
                movie.stop()
            except Exception:
                pass
        container.setVisible(False)

    def _start_background_load(self):
        """Start a QThread that runs the preprocessing in background."""
        # Start a minimal QThread which executes _preprocess_content
        self._loader_thread = HandlerDialog._LoaderThread(self._preprocess_content)
        self._loader_thread.finished.connect(self._on_load_finished)
        self._loader_thread.error.connect(self._on_load_error)
        # ensure thread instance is deleted when done
        self._loader_thread.finished.connect(self._loader_thread.deleteLater)
        self._loader_thread.start()

    def _on_load_error(self, err_msg):
        logging.error(f"Error loading book in background: {err_msg}")
        if getattr(self, "previewEdit", None) is not None:
            self.previewEdit.setPlainText(f"Error loading book: {err_msg}")
        if getattr(self, "splitter", None) is not None:
            self.splitter.setVisible(True)
        self._hide_loading_overlay()

    def _on_load_finished(self):
        """Called in the main thread when background loading finished."""
        # Build the tree now that content_texts/content_lengths/etc. are ready
        try:
            # Rebuild tree based on file type
            self._build_tree()

            # Run auto-check if no provided checks are relevant
            if not self._are_provided_checks_relevant():
                self._run_auto_check()

            # Connect signals (after tree exists)
            self.treeWidget.currentItemChanged.connect(self.update_preview)
            self.treeWidget.itemChanged.connect(self.handle_item_check)
            self.treeWidget.itemChanged.connect(
                lambda _: self._update_checkbox_states()
            )
            self.treeWidget.itemDoubleClicked.connect(self.handle_item_double_click)

            # Expand and select first item
            self.treeWidget.expandAll()
            if self.treeWidget.topLevelItemCount() > 0:
                self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(0))
                self.treeWidget.setFocus()

            # Update checkbox states
            self._update_checkbox_states()

            # Update preview for the current selection
            current = self.treeWidget.currentItem()
            self.update_preview(current)

        except Exception as e:
            logging.error(f"Error finalizing book load: {e}")
        # Show the main UI and hide loading text
        if getattr(self, "splitter", None) is not None:
            self.splitter.setVisible(True)
        self._hide_loading_overlay()

    def _preprocess_content(self):
        """Pre-process content from the document"""
        # Create cache key from file path, modification time, file type, and replace_single_newlines setting
        try:
            mod_time = os.path.getmtime(self.book_path)
        except Exception:
            mod_time = 0

        # Include replace_single_newlines in cache key since it affects text cleaning
        from abogen.utils import load_config

        cfg = load_config()
        replace_single_newlines = cfg.get("replace_single_newlines", True)

        cache_key = (self.book_path, mod_time, self.parser.file_type, replace_single_newlines)

        # Check if content is already cached
        if cache_key in HandlerDialog._content_cache:
            cached_data = HandlerDialog._content_cache[cache_key]
            self.content_texts = cached_data["content_texts"]
            self.content_lengths = cached_data["content_lengths"]
            if "processed_nav_structure" in cached_data:
                self.processed_nav_structure = cached_data["processed_nav_structure"]
            if "book_metadata" in cached_data:
                self.book_metadata = cached_data["book_metadata"]
            
            # Apply to parser so it stays in sync if used elsewhere
            self.parser.content_texts = self.content_texts
            self.parser.content_lengths = self.content_lengths
            self.parser.processed_nav_structure = self.processed_nav_structure
            self.parser.book_metadata = self.book_metadata

            logging.info(f"Using cached content for {os.path.basename(self.book_path)}")
            return

        # Process content if not cached
        try:
            self.parser.process_content(replace_single_newlines=replace_single_newlines)
            self.content_texts = self.parser.content_texts
            self.content_lengths = self.parser.content_lengths
            self.processed_nav_structure = self.parser.processed_nav_structure
            self.book_metadata = self.parser.get_metadata()
        except Exception as e:
            logging.error(f"Error processing content: {e}", exc_info=True)
            # Handle empty/failure case
            self.content_texts = {}
            self.content_lengths = {}

        # Cache the processed content
        cache_data = {
            "content_texts": self.content_texts,
            "content_lengths": self.content_lengths,
            "processed_nav_structure": self.processed_nav_structure,
            "book_metadata": self.book_metadata,
        }

        HandlerDialog._content_cache[cache_key] = cache_data
        logging.info(f"Cached content for {os.path.basename(self.book_path)}")


    def _build_tree(self):
        self.treeWidget.clear()

        info_item = QTreeWidgetItem(self.treeWidget, ["Information"])
        info_item.setData(0, Qt.ItemDataRole.UserRole, "info:bookinfo")
        info_item.setFlags(info_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
        font = info_item.font(0)
        font.setBold(True)
        info_item.setFont(0, font)

        if self.processed_nav_structure:
            self._build_tree_from_nav(
                self.processed_nav_structure, self.treeWidget
            )
        else:
             # If no structure found but content exists (rare fallback), list flat
             for ch_id, ch_len in self.content_lengths.items():
                 # Simple flat list
                 item = QTreeWidgetItem(self.treeWidget, [ch_id])
                 item.setData(0, Qt.ItemDataRole.UserRole, ch_id)
                 item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                 if self.content_texts.get(ch_id):
                     item.setCheckState(0, Qt.CheckState.Checked if ch_id in self.checked_chapters else Qt.CheckState.Unchecked)

        has_parents = False
        iterator = QTreeWidgetItemIterator(
            self.treeWidget, QTreeWidgetItemIterator.IteratorFlag.HasChildren
        )
        if iterator.value():
            has_parents = True
        self.treeWidget.setRootIsDecorated(has_parents)

    def _update_checkbox_states(self):
        """Update the checkbox states based on the current checked chapters."""
        for i in range(self.treeWidget.topLevelItemCount()):
            item = self.treeWidget.topLevelItem(i)
            self._update_item_checkbox_state(item)

    def _build_tree_from_nav(
        self, nav_nodes, parent_item, seen_content_hashes=None
    ):
        if seen_content_hashes is None:
            seen_content_hashes = set()
        for node in nav_nodes:
            title = node.get("title", "Unknown")
            src = node.get("src")
            children = node.get("children", [])

            item = QTreeWidgetItem(parent_item, [title])
            item.setData(0, Qt.ItemDataRole.UserRole, src)

            is_empty = (
                src
                and (src in self.content_texts)
                and (not self.content_texts[src].strip())
            )
            is_duplicate = False
            if src and src in self.content_texts and self.content_texts[src].strip():
                content_hash = hash(self.content_texts[src])
                if content_hash in seen_content_hashes:
                    is_duplicate = True
                else:
                    seen_content_hashes.add(content_hash)

            if src and not is_empty and not is_duplicate:
                item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                is_checked = src in self.checked_chapters
                item.setCheckState(
                    0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked
                )
            elif is_duplicate:
                # Mark as duplicate and remove checkbox
                item.setText(0, f"{title} (Duplicate)")
                item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
            elif children:
                item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                item.setCheckState(0, Qt.CheckState.Unchecked)
            else:
                item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)

            if children:
                self._build_tree_from_nav(children, item, seen_content_hashes)


    def _are_provided_checks_relevant(self):
        if not self.checked_chapters:
            return False

        all_identifiers = set()
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)
                if identifier:
                    all_identifiers.add(identifier)
            iterator += 1

        return bool(self.checked_chapters.intersection(all_identifiers))

    def _setup_ui(self):
        self.previewEdit = QTextEdit(self)
        self.previewEdit.setReadOnly(True)
        self.previewEdit.setMinimumWidth(300)
        self.previewEdit.setStyleSheet("QTextEdit { border: none; }")

        self.previewInfoLabel = QLabel(
            '*Note: You can modify the content later using the "Edit" button in the input box or by accessing the temporary files directory through settings (if not saved in a project folder).',
            self,
        )
        self.previewInfoLabel.setWordWrap(True)
        self.previewInfoLabel.setStyleSheet(
            "QLabel { color: #666; font-style: italic; }"
        )

        previewLayout = QVBoxLayout()
        previewLayout.setContentsMargins(0, 0, 0, 0)
        previewLayout.addWidget(self.previewEdit, 1)
        previewLayout.addWidget(self.previewInfoLabel, 0)

        rightWidget = QWidget()
        rightWidget.setLayout(previewLayout)

        buttons = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
            self,
        )
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        item_type = "chapters" if self.parser.file_type in ["epub", "markdown"] else "pages"

        self.auto_select_btn = QPushButton(f"Auto-select {item_type}", self)
        self.auto_select_btn.clicked.connect(self.auto_select_chapters)
        self.auto_select_btn.setToolTip(f"Automatically select main {item_type}")

        buttons_layout = QVBoxLayout()
        buttons_layout.setContentsMargins(0, 0, 0, 0)
        buttons_layout.setSpacing(10)

        auto_select_layout = QHBoxLayout()
        auto_select_layout.addWidget(self.auto_select_btn)
        buttons_layout.addLayout(auto_select_layout)

        select_layout = QHBoxLayout()
        self.select_all_btn = QPushButton("Select all", self)
        self.select_all_btn.clicked.connect(self.select_all_chapters)
        self.deselect_all_btn = QPushButton("Clear all", self)
        self.deselect_all_btn.clicked.connect(self.deselect_all_chapters)
        select_layout.addWidget(self.select_all_btn)
        select_layout.addWidget(self.deselect_all_btn)
        buttons_layout.addLayout(select_layout)

        parent_layout = QHBoxLayout()
        self.select_parents_btn = QPushButton("Select parents", self)
        self.select_parents_btn.clicked.connect(self.select_parent_chapters)
        self.deselect_parents_btn = QPushButton("Unselect parents", self)
        self.deselect_parents_btn.clicked.connect(self.deselect_parent_chapters)
        parent_layout.addWidget(self.select_parents_btn)
        parent_layout.addWidget(self.deselect_parents_btn)
        buttons_layout.addLayout(parent_layout)

        expand_layout = QHBoxLayout()
        self.expand_all_btn = QPushButton("Expand All", self)
        self.expand_all_btn.clicked.connect(self.treeWidget.expandAll)
        self.collapse_all_btn = QPushButton("Collapse All", self)
        self.collapse_all_btn.clicked.connect(self.treeWidget.collapseAll)
        expand_layout.addWidget(self.expand_all_btn)
        expand_layout.addWidget(self.collapse_all_btn)
        buttons_layout.addLayout(expand_layout)

        leftLayout = QVBoxLayout()
        leftLayout.setContentsMargins(0, 0, 5, 0)
        leftLayout.addLayout(buttons_layout)
        leftLayout.addWidget(self.treeWidget)

        checkbox_text = (
            "Save each chapter separately"
            if self.parser.file_type in ["epub", "markdown"]
            else "Save each page separately"
        )
        self.save_chapters_checkbox = QCheckBox(checkbox_text, self)
        self.save_chapters_checkbox.setChecked(self.save_chapters_separately)
        self.save_chapters_checkbox.stateChanged.connect(self.on_save_chapters_changed)
        leftLayout.addWidget(self.save_chapters_checkbox)
        self.merge_chapters_checkbox = QCheckBox(
            "Create a merged version at the end", self
        )
        self.merge_chapters_checkbox.setChecked(self.merge_chapters_at_end)
        self.merge_chapters_checkbox.stateChanged.connect(
            self.on_merge_chapters_changed
        )
        leftLayout.addWidget(self.merge_chapters_checkbox)

        self.save_as_project_checkbox = QCheckBox(
            "Save in a project folder with metadata", self
        )
        self.save_as_project_checkbox.setToolTip(
            "Save the converted item in a project folder with metadata files. "
            "(Useful if you want to work with converted items in the future.)"
        )
        self.save_as_project_checkbox.setChecked(self.save_as_project)
        self.save_as_project_checkbox.stateChanged.connect(
            self.on_save_as_project_changed
        )
        leftLayout.addWidget(self.save_as_project_checkbox)

        leftLayout.addWidget(buttons)

        leftWidget = QWidget()
        leftWidget.setLayout(leftLayout)

        self.splitter = QSplitter(Qt.Orientation.Horizontal)
        self.splitter.addWidget(leftWidget)
        self.splitter.addWidget(rightWidget)
        self.splitter.setSizes([280, 420])

        mainLayout = QVBoxLayout(self)
        mainLayout.addWidget(self.splitter)
        self.setLayout(mainLayout)

    def _update_checkbox_states(self):
        if (
            not hasattr(self, "save_chapters_checkbox")
            or not self.save_chapters_checkbox
        ):
            return

        if (
            self.parser.file_type == "pdf"
            and hasattr(self, "has_pdf_bookmarks")
            and not self.has_pdf_bookmarks
        ):
            self.save_chapters_checkbox.setEnabled(False)
            self.merge_chapters_checkbox.setEnabled(False)
            return

        checked_count = 0

        if self.parser.file_type in ["epub", "markdown"]:
            iterator = QTreeWidgetItemIterator(self.treeWidget)
            while iterator.value():
                item = iterator.value()
                if (
                    item.flags() & Qt.ItemFlag.ItemIsUserCheckable
                    and item.checkState(0) == Qt.CheckState.Checked
                ):
                    checked_count += 1
                    if checked_count >= 2:
                        break
                iterator += 1

        else:
            parent_groups = set()

            iterator = QTreeWidgetItemIterator(self.treeWidget)
            while iterator.value():
                item = iterator.value()
                if (
                    item.flags() & Qt.ItemFlag.ItemIsUserCheckable
                    and item.checkState(0) == Qt.CheckState.Checked
                ):
                    parent = item.parent()
                    if parent and parent != self.treeWidget.invisibleRootItem():
                        parent_groups.add(id(parent))
                    else:
                        parent_groups.add(id(item))
                iterator += 1

            checked_count = len(parent_groups)

        min_groups_required = 2
        self.save_chapters_checkbox.setEnabled(checked_count >= min_groups_required)

        self.merge_chapters_checkbox.setEnabled(
            self.save_chapters_checkbox.isEnabled()
            and self.save_chapters_checkbox.isChecked()
        )

    def select_all_chapters(self):
        self._block_signals = True
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                item.setCheckState(0, Qt.CheckState.Checked)
            iterator += 1
        self._block_signals = False
        self._update_checked_set_from_tree()

    def deselect_all_chapters(self):
        self._block_signals = True
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                item.setCheckState(0, Qt.CheckState.Unchecked)
            iterator += 1
        self._block_signals = False
        self._update_checked_set_from_tree()

    def select_parent_chapters(self):
        self._block_signals = True
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0:
                item.setCheckState(0, Qt.CheckState.Checked)
            iterator += 1
        self._block_signals = False
        self._update_checked_set_from_tree()

    def deselect_parent_chapters(self):
        self._block_signals = True
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() > 0:
                item.setCheckState(0, Qt.CheckState.Unchecked)
            iterator += 1
        self._block_signals = False
        self._update_checked_set_from_tree()

    def auto_select_chapters(self):
        self._run_auto_check()

    def _run_auto_check(self):
        self._block_signals = True

        if self.parser.file_type == "epub":
            self._run_epub_auto_check()
        elif self.parser.file_type == "markdown":
            self._run_markdown_auto_check()
        else:
            self._run_pdf_auto_check()

        self._block_signals = False
        self._update_checked_set_from_tree()

    def _run_epub_auto_check(self):
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
                iterator += 1
                continue

            src = item.data(0, Qt.ItemDataRole.UserRole)

            has_significant_content = src and self.content_lengths.get(src, 0) > 1000
            is_parent = item.childCount() > 0

            if has_significant_content or is_parent:
                item.setCheckState(0, Qt.CheckState.Checked)
                if is_parent:
                    for i in range(item.childCount()):
                        child = item.child(i)
                        if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                            child_src = child.data(0, Qt.ItemDataRole.UserRole)
                            child_has_content = (
                                child_src and self.content_lengths.get(child_src, 0) > 0
                            )
                            child_is_parent = child.childCount() > 0
                            if child_has_content or child_is_parent:
                                child.setCheckState(0, Qt.CheckState.Checked)
            else:
                item.setCheckState(0, Qt.CheckState.Unchecked)

            iterator += 1

    def _run_markdown_auto_check(self):
        """Auto-select markdown chapters with significant content"""
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
                iterator += 1
                continue

            identifier = item.data(0, Qt.ItemDataRole.UserRole)

            # Select chapters with content > 500 characters or parent items
            has_significant_content = (
                identifier and self.content_lengths.get(identifier, 0) > 500
            )
            is_parent = item.childCount() > 0

            if has_significant_content or is_parent:
                item.setCheckState(0, Qt.CheckState.Checked)
                # Also check children if this is a parent
                if is_parent:
                    for i in range(item.childCount()):
                        child = item.child(i)
                        if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                            child_identifier = child.data(0, Qt.ItemDataRole.UserRole)
                            child_has_content = (
                                child_identifier
                                and self.content_lengths.get(child_identifier, 0) > 0
                            )
                            child_is_parent = child.childCount() > 0
                            if child_has_content or child_is_parent:
                                child.setCheckState(0, Qt.CheckState.Checked)
            else:
                item.setCheckState(0, Qt.CheckState.Unchecked)

            iterator += 1

    def _run_pdf_auto_check(self):
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
                iterator += 1
                continue

            identifier = item.data(0, Qt.ItemDataRole.UserRole)
            if not identifier:
                 iterator += 1
                 continue
                 
            # Logic: Check item if it has content (already handled by ItemIsUserCheckable flag really)
            # But duplicate logic from previous implementation:
            item.setCheckState(0, Qt.CheckState.Checked)

            iterator += 1

    def _update_checked_set_from_tree(self):
        self.checked_chapters.clear()
        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.checkState(0) == Qt.CheckState.Checked:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)
                if identifier:
                    self.checked_chapters.add(identifier)
            iterator += 1
        if hasattr(self, "save_chapters_checkbox") and self.save_chapters_checkbox:
            self._update_checkbox_states()

    def handle_item_check(self, item):
        if self._block_signals:
            return

        self._block_signals = True

        if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
            for i in range(item.childCount()):
                child = item.child(i)
                if child.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                    child.setCheckState(0, item.checkState(0))

        self._block_signals = False
        self._update_checked_set_from_tree()

    def handle_item_double_click(self, item, column=0):
        if item.flags() & Qt.ItemFlag.ItemIsUserCheckable and item.childCount() == 0:
            rect = self.treeWidget.visualItemRect(item)
            checkbox_width = 20

            mouse_pos = self.treeWidget.mapFromGlobal(self.treeWidget.cursor().pos())

            if mouse_pos.x() > rect.x() + checkbox_width:
                new_state = (
                    Qt.CheckState.Unchecked
                    if item.checkState(0) == Qt.CheckState.Checked
                    else Qt.CheckState.Checked
                )
                item.setCheckState(0, new_state)

    def update_preview(self, current):
        if not current:
            self.previewEdit.clear()
            return

        identifier = current.data(0, Qt.ItemDataRole.UserRole)

        if identifier == "info:bookinfo":
            self._display_book_info()
            return

        text = None
        if self.parser.file_type == "epub":
            text = self.content_texts.get(identifier)
        else:
            text = self.content_texts.get(identifier)

        if text is None:
            title = current.text(0)
            self.previewEdit.setPlainText(
                f"{title}\n\n(No content available for this item)"
            )
        elif not text.strip():
            title = current.text(0)
            self.previewEdit.setPlainText(f"{title}\n\n(This item is empty)")
        else:
            # Apply clean_text to preview so replace_single_newlines setting is respected
            cleaned_text = clean_text(text)
            self.previewEdit.setPlainText(cleaned_text)

    def _display_book_info(self):
        self.previewEdit.clear()
        html_content = "<html><body style='font-family: Arial, sans-serif;'>"

        cover_image = self.book_metadata.get("cover_image")
        if cover_image:
            try:
                image_data = base64.b64encode(cover_image).decode("utf-8")

                image_type = "jpeg"
                if cover_image.startswith(b"\x89PNG"):
                    image_type = "png"
                elif cover_image.startswith(b"GIF"):
                    image_type = "gif"

                html_content += (
                    f"<div style='text-align: center; margin-bottom: 20px;'>"
                )
                html_content += (
                    f"<img src='data:image/{image_type};base64,{image_data}' "
                )
                html_content += "width='300' style='object-fit: contain;' /></div>"
            except Exception as e:
                html_content += f"<p>Error displaying cover image: {str(e)}</p>"

        title = self.book_metadata.get("title")
        if title:
            html_content += (
                f"<h2 style='text-align: center;'>{title}</h2>"
            )

        authors = self.book_metadata.get("authors")
        if authors:
            authors_text = ", ".join(authors)
            html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>"

        publisher = self.book_metadata.get("publisher")
        pub_year = self.book_metadata.get("publication_year")

        if publisher or pub_year:
            pub_info = []
            if publisher:
                pub_info.append(f"Published by {publisher}")
            if pub_year:
                pub_info.append(f"Year: {pub_year}")
            html_content += f"<p style='text-align: center;'>{' | '.join(pub_info)}</p>"

        html_content += "<hr/>"

        description = self.book_metadata.get("description")
        if description:
            # Use pre-compiled pattern for better performance
            desc = _HTML_TAG_PATTERN.sub("", description)
            html_content += f"<h3>Description:</h3><p>{desc}</p>"

        if self.parser.file_type == "pdf":
            # Access pdf_doc from parser if available
            pdf_doc = getattr(self.parser, "pdf_doc", None)
            page_count = len(pdf_doc) if pdf_doc else 0
            html_content += f"<p>File type: PDF<br>Page count: {page_count}</p>"

        html_content += "</body></html>"
        self.previewEdit.setHtml(html_content)

    def _extract_book_metadata(self):
        if self.parser.file_type == "epub":
            return extract_book_metadata_epub(self.book)
        elif self.parser.file_type == "markdown":
            return extract_book_metadata_markdown(
                self.markdown_text, self.markdown_toc
            )
        else:
            return extract_book_metadata_pdf(self.pdf_doc)

    def get_selected_text(self):
        # If a background loader thread is running, wait for it to finish to
        # preserve compatibility with callers that expect content to be ready
        # when they create a HandlerDialog and immediately request selected text.
        try:
            if (
                hasattr(self, "_loader_thread")
                and getattr(self, "_loader_thread") is not None
            ):
                # Wait for thread to finish (blocks until done)
                if self._loader_thread.isRunning():
                    self._loader_thread.wait()
        except Exception:
            pass

        if self.parser.file_type == "epub":
            return self._get_epub_selected_text()
        elif self.parser.file_type == "markdown":
            return self._get_markdown_selected_text()
        else:
            return self._get_pdf_selected_text()

    def _format_metadata_tags(self):
        """Format metadata tags for insertion at the beginning of the text"""
        from abogen.utils import get_user_cache_path

        filename = os.path.splitext(os.path.basename(self.book_path))[0]
        chapter_count = len(self.checked_chapters)
        cache_dir = get_user_cache_path()

        return format_metadata_tags(
            self.book_metadata,
            filename,
            chapter_count,
            self.parser.file_type,
            cover_bytes=self.book_metadata.get("cover_image"),
            cache_dir=cache_dir,
        )

    def _get_markdown_selected_text(self):
        """Get selected text from markdown chapters"""
        all_checked_identifiers = set()
        chapter_texts = []

        # Add metadata tags at the beginning
        metadata_tags = self._format_metadata_tags()

        item_order_counter = 0
        ordered_checked_items = []

        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            item_order_counter += 1
            if item.checkState(0) == Qt.CheckState.Checked:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)

                if identifier and identifier != "info:bookinfo":
                    all_checked_identifiers.add(identifier)
                    ordered_checked_items.append((item_order_counter, item, identifier))
            iterator += 1

        ordered_checked_items.sort(key=lambda x: x[0])

        for order, item, identifier in ordered_checked_items:
            text = self.content_texts.get(identifier)
            if text and text.strip():
                title = item.text(0)
                # Remove leading dashes from title using pre-compiled pattern
                title = _LEADING_DASH_PATTERN.sub("", title).strip()
                marker = f"<<CHAPTER_MARKER:{title}>>"
                chapter_texts.append(marker + "\n" + text)

        full_text = metadata_tags + "\n\n" + "\n\n".join(chapter_texts)
        return full_text, all_checked_identifiers

    def _get_epub_selected_text(self):
        all_checked_identifiers = set()
        chapter_texts = []

        # Add metadata tags at the beginning
        metadata_tags = self._format_metadata_tags()

        item_order_counter = 0
        ordered_checked_items = []

        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            item_order_counter += 1
            if item.checkState(0) == Qt.CheckState.Checked:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)
                if identifier and identifier != "info:bookinfo":
                    all_checked_identifiers.add(identifier)
                    ordered_checked_items.append((item_order_counter, item, identifier))
            iterator += 1

        ordered_checked_items.sort(key=lambda x: x[0])

        for order, item, identifier in ordered_checked_items:
            text = self.content_texts.get(identifier)
            if text and text.strip():
                title = item.text(0)
                # Use pre-compiled pattern for better performance
                title = _LEADING_DASH_PATTERN.sub("", title).strip()
                marker = f"<<CHAPTER_MARKER:{title}>>"
                chapter_texts.append(marker + "\n" + text)

        full_text = metadata_tags + "\n\n" + "\n\n".join(chapter_texts)
        return full_text, all_checked_identifiers

    def _get_pdf_selected_text(self):
        all_checked_identifiers = set()
        included_text_ids = set()
        section_titles = []
        all_content = []

        # Add metadata tags at the beginning
        metadata_tags = self._format_metadata_tags()

        pdf_has_no_bookmarks = (
            hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks
        )

        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.checkState(0) == Qt.CheckState.Checked:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)
                if identifier:
                    all_checked_identifiers.add(identifier)
            iterator += 1

        if pdf_has_no_bookmarks:
            sorted_page_ids = sorted(
                [id for id in all_checked_identifiers if id.startswith("page_")],
                key=lambda x: int(x.split("_")[1]) if x.split("_")[1].isdigit() else 0,
            )
            for page_id in sorted_page_ids:
                if page_id not in included_text_ids:
                    text = self.content_texts.get(page_id, "")
                    if text:
                        all_content.append(text)
                        included_text_ids.add(page_id)
            return (
                metadata_tags + "\n\n" + "\n\n".join(all_content),
                all_checked_identifiers,
            )

        iterator = QTreeWidgetItemIterator(self.treeWidget)
        while iterator.value():
            item = iterator.value()
            if item.childCount() > 0:
                parent_checked = item.checkState(0) == Qt.CheckState.Checked
                parent_id = item.data(0, Qt.ItemDataRole.UserRole)
                parent_title = item.text(0)
                checked_children = []
                for i in range(item.childCount()):
                    child = item.child(i)
                    child_id = child.data(0, Qt.ItemDataRole.UserRole)
                    if (
                        child.checkState(0) == Qt.CheckState.Checked
                        and child_id
                        and child_id not in included_text_ids
                    ):
                        checked_children.append((child, child_id))
                if parent_checked and parent_id and parent_id not in included_text_ids:
                    combined_text = self.content_texts.get(parent_id, "")
                    for child, child_id in checked_children:
                        child_text = self.content_texts.get(child_id, "")
                        if child_text:
                            combined_text += "\n\n" + child_text
                        included_text_ids.add(child_id)
                    if combined_text.strip():
                        # Use pre-compiled pattern for better performance
                        title = _LEADING_SIMPLE_DASH_PATTERN.sub(
                            "", parent_title
                        ).strip()
                        marker = f"<<CHAPTER_MARKER:{title}>>"
                        section_titles.append((title, marker + "\n" + combined_text))
                        included_text_ids.add(parent_id)
                elif not parent_checked and checked_children:
                    # Use pre-compiled pattern for better performance
                    title = _LEADING_SIMPLE_DASH_PATTERN.sub("", parent_title).strip()
                    marker = f"<<CHAPTER_MARKER:{title}>>"
                    for idx, (child, child_id) in enumerate(checked_children):
                        text = self.content_texts.get(child_id, "")
                        if text:
                            if idx == 0:
                                section_titles.append((title, marker + "\n" + text))
                            else:
                                section_titles.append((title, text))
                        included_text_ids.add(child_id)
            elif item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                identifier = item.data(0, Qt.ItemDataRole.UserRole)
                if (
                    identifier
                    and identifier not in included_text_ids
                    and item.checkState(0) == Qt.CheckState.Checked
                ):
                    text = self.content_texts.get(identifier, "")
                    if text:
                        title = item.text(0)
                        # Use pre-compiled pattern for better performance
                        title = _LEADING_SIMPLE_DASH_PATTERN.sub("", title).strip()
                        marker = f"<<CHAPTER_MARKER:{title}>>"
                        section_titles.append((title, marker + "\n" + text))
                        included_text_ids.add(identifier)
            iterator += 1

        return (
            metadata_tags + "\n\n" + "\n\n".join([t[1] for t in section_titles]),
            all_checked_identifiers,
        )

    def on_save_chapters_changed(self, state):
        self.save_chapters_separately = bool(state)
        self.merge_chapters_checkbox.setEnabled(self.save_chapters_separately)
        HandlerDialog._save_chapters_separately = self.save_chapters_separately

    def on_merge_chapters_changed(self, state):
        self.merge_chapters_at_end = bool(state)
        HandlerDialog._merge_chapters_at_end = self.merge_chapters_at_end

    def on_save_as_project_changed(self, state):
        self.save_as_project = bool(state)
        HandlerDialog._save_as_project = self.save_as_project

    def get_save_chapters_separately(self):
        return (
            self.save_chapters_separately
            if self.save_chapters_checkbox.isEnabled()
            else False
        )

    def get_merge_chapters_at_end(self):
        return self.merge_chapters_at_end

    def get_save_as_project(self):
        return self.save_as_project

    def check_selected_items(self):
        self.set_selected_items_checked(True)

    def uncheck_selected_items(self):
        self.set_selected_items_checked(False)

    def set_selected_items_checked(self, state: bool):
        print(f"Checking selected items: {state}")
        self.treeWidget.blockSignals(True)
        for item in self.treeWidget.selectedItems():
            if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                item.setCheckState(
                    0, Qt.CheckState.Checked if state else Qt.CheckState.Unchecked
                )
        self.treeWidget.blockSignals(False)
        self._update_checked_set_from_tree()

    def on_tree_context_menu(self, pos):
        item = self.treeWidget.itemAt(pos)
        # multi-select context menu
        if self.treeWidget.selectedItems() and len(self.treeWidget.selectedItems()) > 1:
            menu = QMenu(self)
            action = menu.addAction("Select")
            action.triggered.connect(self.check_selected_items)
            action = menu.addAction("Clear")
            action.triggered.connect(self.uncheck_selected_items)
            menu.exec(self.treeWidget.mapToGlobal(pos))
            return

        if (
            not item
            or item.childCount() == 0
            or not (item.flags() & Qt.ItemFlag.ItemIsUserCheckable)
        ):
            return

        menu = QMenu(self)
        checked = item.checkState(0) == Qt.CheckState.Checked
        text = "Unselect only this" if checked else "Select only this"
        action = menu.addAction(text)

        def do_toggle():
            self.treeWidget.blockSignals(True)
            new_state = Qt.CheckState.Unchecked if checked else Qt.CheckState.Checked
            item.setCheckState(0, new_state)
            self.treeWidget.blockSignals(False)
            self._update_checked_set_from_tree()

        action.triggered.connect(do_toggle)
        menu.exec(self.treeWidget.mapToGlobal(pos))


