import re
import ebooklib
import base64
import fitz  # PyMuPDF for PDF support
from ebooklib import epub
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 (
    clean_text,
    calculate_text_length,
    detect_encoding,
    get_resource_path,
)
import os
import logging  # Add logging
import urllib.parse
import markdown
import textwrap

# Setup logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)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))

        # Determine file type if not explicitly provided
        if file_type:
            self.file_type = file_type
        elif book_path.lower().endswith(".pdf"):
            self.file_type = "pdf"
        elif book_path.lower().endswith((".md", ".markdown")):
            self.file_type = "markdown"
        else:
            self.file_type = "epub"
        self.book_path = book_path

        # 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.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

        # Load the book based on file type
        try:
            if self.file_type == "epub":
                self.book = epub.read_epub(book_path)
            elif self.file_type == "markdown":
                self.book = None  # Markdown doesn't use ebooklib
            else:
                self.book = None
        except KeyError as e:
            logging.error(
                f"EPUB file is missing a referenced file: {e}. Skipping missing file."
            )
            # Try to patch ebooklib to skip missing files (monkey-patch read_file)
            import types

            orig_read_file = None
            try:
                from ebooklib import epub as _epub_module

                reader_class = _epub_module.EpubReader
                orig_read_file = reader_class.read_file

                def safe_read_file(self, name):
                    try:
                        return orig_read_file(self, name)
                    except KeyError:
                        logging.warning(
                            f"Missing file in EPUB: {name}. Returning empty bytes."
                        )
                        return b""

                reader_class.read_file = safe_read_file
                self.book = epub.read_epub(book_path)
                reader_class.read_file = orig_read_file  # Restore
            except Exception as patch_e:
                logging.error(f"Failed to patch ebooklib for missing files: {patch_e}")
                raise e
        self.pdf_doc = fitz.open(book_path) if self.file_type == "pdf" else None
        self.markdown_text = None
        if self.file_type == "markdown":
            try:
                encoding = detect_encoding(book_path)
                with open(book_path, "r", encoding=encoding, errors="replace") as f:
                    self.markdown_text = f.read()
            except Exception as e:
                logging.error(f"Error reading markdown file: {e}")
                self.markdown_text = ""
        self.markdown_toc = []  # For storing parsed markdown TOC

        # Extract book metadata
        self.book_metadata = self._extract_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 = {}

        # 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", False)

        cache_key = (self.book_path, mod_time, self.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 "doc_content" in cached_data:
                self.doc_content = cached_data["doc_content"]
            if "markdown_toc" in cached_data:
                self.markdown_toc = cached_data["markdown_toc"]
            logging.info(f"Using cached content for {os.path.basename(self.book_path)}")
            return

        # Process content if not cached
        if self.file_type == "epub":
            try:
                self._process_epub_content_nav()  # Use the new navigation-based method
            except Exception as e:
                logging.error(
                    f"Error processing EPUB with navigation: {e}. Falling back to TOC/spine.",
                    exc_info=True,
                )
                # Fallback to a simpler spine-based processing if nav fails
                self._process_epub_content_spine_fallback()
        elif self.file_type == "markdown":
            self._preprocess_markdown_content()
        else:
            self._preprocess_pdf_content()

        # Cache the processed content
        cache_data = {
            "content_texts": self.content_texts,
            "content_lengths": self.content_lengths,
        }
        if hasattr(self, "doc_content"):
            cache_data["doc_content"] = self.doc_content
        if hasattr(self, "markdown_toc"):
            cache_data["markdown_toc"] = self.markdown_toc

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

    def _preprocess_pdf_content(self):
        """Pre-process all page contents from PDF document"""
        for page_num in range(len(self.pdf_doc)):
            text = clean_text(self.pdf_doc[page_num].get_text())
            # Remove bracketed numbers (citations, footnotes)
            text = re.sub(r"\[\s*\d+\s*\]", "", text)

            # Remove standalone page numbers (numbers alone on a line)
            text = re.sub(r"^\s*\d+\s*$", "", text, flags=re.MULTILINE)

            # Remove page numbers at the end of paragraphs
            # This pattern looks for digits surrounded by whitespace at the end of paragraphs
            text = re.sub(r"\s+\d+\s*$", "", text, flags=re.MULTILINE)

            # Also remove page numbers followed by a hyphen or dash at paragraph end
            # (common in headers/footers like "- 42 -")
            text = re.sub(r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", "", text, flags=re.MULTILINE)

            page_id = f"page_{page_num + 1}"
            self.content_texts[page_id] = text
            self.content_lengths[page_id] = calculate_text_length(text)

    def _preprocess_markdown_content(self):
        if not self.markdown_text:
            return

        # Generate TOC from the original (dedented) markdown BEFORE cleaning,
        # so header ids/anchors are preserved for reliable position detection.
        original_text = textwrap.dedent(self.markdown_text)
        md = markdown.Markdown(extensions=["toc", "fenced_code"])
        html = md.convert(original_text)
        self.markdown_toc = md.toc_tokens

        # Use cleaned text for stored content/length calculations
        cleaned_full_text = clean_text(original_text)

        soup = BeautifulSoup(html, "html.parser")
        self.content_texts = {}
        self.content_lengths = {}

        if not self.markdown_toc:
            chapter_id = "markdown_content"
            self.content_texts[chapter_id] = cleaned_full_text
            self.content_lengths[chapter_id] = calculate_text_length(cleaned_full_text)
            return

        all_headers = []

        def flatten_toc(toc_list):
            for header in toc_list:
                all_headers.append(header)
                if header.get("children"):
                    flatten_toc(header["children"])

        flatten_toc(self.markdown_toc)

        header_positions = []
        for header in all_headers:
            header_id = header["id"]
            id_pattern = f'id="{header_id}"'
            pos = html.find(id_pattern)
            if pos != -1:
                tag_start = html.rfind("<", 0, pos)
                header_positions.append(
                    {"id": header_id, "start": tag_start, "name": header["name"]}
                )
        header_positions.sort(key=lambda x: x["start"])

        for i, header_pos in enumerate(header_positions):
            header_id = header_pos["id"]
            header_name = header_pos["name"]
            content_start = header_pos["start"]
            content_end = (
                header_positions[i + 1]["start"]
                if i + 1 < len(header_positions)
                else len(html)
            )
            section_html = html[content_start:content_end]
            section_soup = BeautifulSoup(section_html, "html.parser")
            header_tag = section_soup.find(attrs={"id": header_id})
            if header_tag:
                header_tag.decompose()
            # Clean section text for storage/lengths
            section_text = clean_text(section_soup.get_text()).strip()
            chapter_id = header_id
            if section_text:
                full_content = f"{header_name}\n\n{section_text}"
                self.content_texts[chapter_id] = full_content
                self.content_lengths[chapter_id] = calculate_text_length(full_content)
            else:
                self.content_texts[chapter_id] = header_name
                self.content_lengths[chapter_id] = calculate_text_length(header_name)

    def _process_epub_content_spine_fallback(self):
        """Fallback EPUB processing based purely on spine order."""
        logging.info("Using spine fallback for EPUB processing.")
        self.doc_content = {}
        spine_docs = []
        for spine_item_tuple in self.book.spine:
            item_id = spine_item_tuple[0]
            item = self.book.get_item_with_id(item_id)
            if item:
                spine_docs.append(item.get_name())
            else:
                logging.warning(f"Spine item with id '{item_id}' not found.")

        # Cache content
        for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
            href = item.get_name()
            if href in spine_docs:
                try:
                    html_content = item.get_content().decode("utf-8", errors="ignore")
                    self.doc_content[href] = html_content
                except Exception as e:
                    logging.error(f"Error decoding content for {href}: {e}")
                    self.doc_content[href] = ""

        # Create a simple TOC based on spine order
        synthetic_toc = []
        self.content_texts = {}
        self.content_lengths = {}
        for i, doc_href in enumerate(spine_docs):
            html_content = self.doc_content.get(doc_href, "")
            if html_content:
                soup = BeautifulSoup(html_content, "html.parser")

                # Handle ordered lists by prepending numbers to list items
                for ol in soup.find_all("ol"):
                    # Get start attribute or default to 1
                    start = int(ol.get("start", 1))
                    for i, li in enumerate(ol.find_all("li", recursive=False)):
                        # Insert the number at the beginning of the list item
                        number_text = f"{start + i}) "
                        if li.string:
                            li.string.replace_with(number_text + li.string)
                        else:
                            li.insert(0, NavigableString(number_text))

                # Remove sup and sub tags
                for tag in soup.find_all(["sup", "sub"]):
                    tag.decompose()

                text = clean_text(soup.get_text()).strip()
                if text:
                    self.content_texts[doc_href] = text
                    self.content_lengths[doc_href] = len(text)

                    title = None
                    if soup.title and soup.title.string:
                        title = soup.title.string.strip()
                    elif (h1 := soup.find("h1")) and h1.get_text(strip=True):
                        title = h1.get_text(strip=True)

                    if not title:
                        title = f"Untitled Chapter {i + 1}"
                    synthetic_toc.append(
                        (epub.Link(doc_href, title, doc_href), [])
                    )  # Wrap in tuple and empty list for compatibility

        # Replace book.toc with the synthetic one if it was empty or fallback was triggered
        if not self.book.toc or not hasattr(
            self, "processed_nav_structure"
        ):  # Check if nav processing failed
            self.book.toc = synthetic_toc
            logging.info(f"Generated synthetic TOC with {len(synthetic_toc)} entries.")

    def _process_epub_content_nav(self):
        """
        Process EPUB content using ITEM_NAVIGATION (NAV HTML) or ITEM_NCX.
        Globally orders navigation entries and slices content between them.
        """
        logging.info(
            "Attempting to process EPUB using navigation document (NAV/NCX)..."
        )
        nav_item = None
        nav_type = None

        # 1. Check ITEM_NAVIGATION for actual NAV HTML (.xhtml/.html)
        nav_items = list(self.book.get_items_of_type(ebooklib.ITEM_NAVIGATION))
        if nav_items:
            # Prefer files explicitly named 'nav.xhtml' or similar
            preferred_nav = next(
                (
                    item
                    for item in nav_items
                    if "nav" in item.get_name().lower()
                    and item.get_name().lower().endswith((".xhtml", ".html"))
                ),
                None,
            )
            if preferred_nav:
                nav_item = preferred_nav
                nav_type = "html"
                logging.info(f"Found preferred NAV HTML item: {nav_item.get_name()}")
            else:
                # Check if any ITEM_NAVIGATION is actually HTML
                html_nav = next(
                    (
                        item
                        for item in nav_items
                        if item.get_name().lower().endswith((".xhtml", ".html"))
                    ),
                    None,
                )
                if html_nav:
                    nav_item = html_nav
                    nav_type = "html"
                    logging.info(
                        f"Found NAV HTML item in ITEM_NAVIGATION: {html_nav.get_name()}"
                    )

        # 2. If no NAV HTML found via ITEM_NAVIGATION, check if ITEM_NAVIGATION points to NCX
        if not nav_item and nav_items:
            ncx_in_nav = next(
                (
                    item
                    for item in nav_items
                    if item.get_name().lower().endswith(".ncx")
                ),
                None,
            )
            if ncx_in_nav:
                nav_item = ncx_in_nav
                nav_type = "ncx"
                logging.info(
                    f"Found NCX item via ITEM_NAVIGATION: {ncx_in_nav.get_name()}"
                )

        # 3. If still no nav_item, check for NCX or fallback to NAV HTML in all ITEM_DOCUMENTs
        ncx_constant = getattr(epub, "ITEM_NCX", None)
        if not nav_item and ncx_constant is not None:
            ncx_items = list(self.book.get_items_of_type(ncx_constant))
            if ncx_items:
                nav_item = ncx_items[0]
                nav_type = "ncx"
                logging.info(f"Found NCX item via ITEM_NCX: {nav_item.get_name()}")
        # Fallback: search all ITEM_DOCUMENTs for a NAV HTML with <nav epub:type="toc">
        if not nav_item:
            for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
                try:
                    html_content = item.get_content().decode("utf-8", errors="ignore")
                    if "<nav" in html_content and 'epub:type="toc"' in html_content:
                        soup = BeautifulSoup(html_content, "html.parser")
                        nav_tag = soup.find("nav", attrs={"epub:type": "toc"})
                        if nav_tag:
                            nav_item = item
                            nav_type = "html"
                            logging.info(
                                f"Found NAV HTML with TOC in: {item.get_name()}"
                            )
                            break
                except Exception as e:
                    continue
        # 4. If no navigation item found by any method, trigger fallback
        if not nav_item or not nav_type:
            logging.warning(
                "No suitable EPUB navigation document (NAV HTML or NCX) found. Falling back."
            )
            raise ValueError("No navigation document found")  # Trigger fallback

        # Determine parser based on the confirmed nav_type
        parser_type = "html.parser" if nav_type == "html" else "xml"
        logging.info(f"Using parser: '{parser_type}' for {nav_item.get_name()}")
        try:
            nav_content = nav_item.get_content().decode("utf-8", errors="ignore")
            nav_soup = BeautifulSoup(nav_content, parser_type)
        except Exception as e:
            logging.error(
                f"Failed to parse navigation content ({nav_item.get_name()}) using {parser_type}: {e}",
                exc_info=True,
            )
            raise ValueError(
                f"Failed to parse navigation content: {e}"
            )  # Trigger fallback

        # --- Rest of the processing logic ---
        # 1. Cache all document HTML and determine spine order (no changes needed here)
        self.doc_content = {}
        spine_docs = []
        for spine_item_tuple in self.book.spine:
            item_id = spine_item_tuple[0]
            item = self.book.get_item_with_id(item_id)
            if item:
                spine_docs.append(item.get_name())
            else:
                logging.warning(f"Spine item with id '{item_id}' not found.")
        doc_order = {href: i for i, href in enumerate(spine_docs)}
        # Add a mapping for unquoted (decoded) hrefs as well
        doc_order_decoded = {
            urllib.parse.unquote(href): i for href, i in doc_order.items()
        }

        # Clear previous content/lengths before processing
        self.content_texts = {}
        self.content_lengths = {}

        for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
            href = item.get_name()
            if href in doc_order or any(
                href in nav_point.get("src", "")
                for nav_point in nav_soup.find_all(["content", "a"])
            ):
                try:
                    html_content = item.get_content().decode("utf-8", errors="ignore")
                    self.doc_content[href] = html_content
                except Exception as e:
                    logging.error(f"Error decoding content for {href}: {e}")
                    self.doc_content[href] = ""

        # 2. Extract and order navigation entries globally
        ordered_nav_entries = []

        # Define find_position locally or ensure self._find_position_robust is used correctly
        # Using self._find_position_robust is preferred as it's a method of the class
        find_position_func = self._find_position_robust

        # Store the parsed structure for tree building later
        self.processed_nav_structure = []

        # Call the correct parsing function based on confirmed nav_type
        parse_successful = False
        if nav_type == "ncx":
            nav_map = nav_soup.find("navMap")
            if nav_map:
                logging.info("Parsing NCX <navMap>...")
                for nav_point in nav_map.find_all("navPoint", recursive=False):
                    self._parse_ncx_navpoint(
                        nav_point,
                        ordered_nav_entries,
                        doc_order,
                        doc_order_decoded,
                        self.processed_nav_structure,
                        find_position_func,
                    )
                parse_successful = bool(
                    ordered_nav_entries
                )  # Success if entries were added
            else:
                logging.warning("Could not find <navMap> in NCX file.")
        elif nav_type == "html":
            logging.info("Parsing NAV HTML...")
            toc_nav = nav_soup.find("nav", attrs={"epub:type": "toc"})
            if not toc_nav:
                # Fallback: look for any <nav> element containing an <ol>
                all_navs = nav_soup.find_all("nav")
                for nav in all_navs:
                    if nav.find("ol"):
                        toc_nav = nav
                        logging.info("Found fallback TOC structure in <nav> with <ol>.")
                        break
            if toc_nav:
                top_ol = toc_nav.find("ol", recursive=False)
                if top_ol:
                    for li in top_ol.find_all("li", recursive=False):
                        self._parse_html_nav_li(
                            li,
                            ordered_nav_entries,
                            doc_order,
                            doc_order_decoded,
                            self.processed_nav_structure,
                            find_position_func,
                        )
                    parse_successful = bool(
                        ordered_nav_entries
                    )  # Success if entries were added
                else:
                    logging.warning("Found <nav> for TOC but no top-level <ol> inside.")
            else:
                logging.warning(
                    "Could not find TOC structure (<nav epub:type='toc'> or <nav><ol>) in NAV HTML."
                )

        # Handle case where parsing ran but found no valid entries OR parsing failed
        if not parse_successful:
            logging.warning(
                "Navigation parsing completed but found no valid entries, or parsing failed. Falling back."
            )
            raise ValueError("No valid navigation entries found after parsing")

        # Sort entries globally by document order and position within the document
        ordered_nav_entries.sort(key=lambda x: (x["doc_order"], x["position"]))
        logging.info(f"Sorted {len(ordered_nav_entries)} navigation entries.")

        # 3. Slice content ONLY between sorted TOC entries
        num_entries = len(ordered_nav_entries)
        for i in range(num_entries):
            current_entry = ordered_nav_entries[i]
            current_src = current_entry["src"]
            current_doc = current_entry["doc_href"]
            current_pos = current_entry["position"]
            current_doc_html = self.doc_content.get(current_doc, "")

            start_slice_pos = current_pos
            slice_html = ""

            next_entry = ordered_nav_entries[i + 1] if (i + 1) < num_entries else None

            if next_entry:
                next_doc = next_entry["doc_href"]
                next_pos = next_entry["position"]

                # Always include all content from current position to next position, even if next_doc is before current_doc
                if current_doc == next_doc:
                    slice_html = current_doc_html[start_slice_pos:next_pos]
                else:
                    # Collect all content from current_doc (from start_slice_pos to end),
                    # then all intermediate docs (in spine order),
                    # then up to next_pos in next_doc (even if next_doc is before current_doc in spine)
                    slice_html = current_doc_html[start_slice_pos:]
                    docs_between = []
                    try:
                        idx_current = spine_docs.index(current_doc)
                        idx_next = spine_docs.index(next_doc)
                        if idx_current < idx_next:
                            for doc_idx in range(idx_current + 1, idx_next):
                                docs_between.append(spine_docs[doc_idx])
                        elif idx_current > idx_next:
                            for doc_idx in range(idx_current + 1, len(spine_docs)):
                                docs_between.append(spine_docs[doc_idx])
                            for doc_idx in range(0, idx_next):
                                docs_between.append(spine_docs[doc_idx])
                    except Exception:
                        pass
                    for doc_href in docs_between:
                        slice_html += self.doc_content.get(doc_href, "")
                    next_doc_html = self.doc_content.get(next_doc, "")
                    slice_html += next_doc_html[:next_pos]
            else:
                # Last TOC entry: include all content from current position to end of book
                slice_html = current_doc_html[start_slice_pos:]
                try:
                    idx_current = spine_docs.index(current_doc)
                    for doc_idx in range(idx_current + 1, len(spine_docs)):
                        intermediate_doc_href = spine_docs[doc_idx]
                        slice_html += self.doc_content.get(intermediate_doc_href, "")
                except Exception:
                    pass
            # Fallback: if slice_html is empty, try to get the whole file's text
            if not slice_html.strip() and current_doc_html:
                logging.warning(
                    f"No content found for src '{current_src}', using full file as fallback."
                )
                slice_html = current_doc_html
            if slice_html.strip():
                slice_soup = BeautifulSoup(slice_html, "html.parser")
                # Add line breaks after paragraphs and divs
                for tag in slice_soup.find_all(["p", "div"]):
                    tag.append("\n\n")

                # Handle ordered lists by prepending numbers to list items
                for ol in slice_soup.find_all("ol"):
                    # Get start attribute or default to 1
                    start = int(ol.get("start", 1))
                    for i, li in enumerate(ol.find_all("li", recursive=False)):
                        # Insert the number at the beginning of the list item
                        number_text = f"{start + i}) "
                        if li.string:
                            li.string.replace_with(number_text + li.string)
                        else:
                            li.insert(0, NavigableString(number_text))

                # Remove sup and sub tags that might contain footnotes
                for tag in slice_soup.find_all(["sup", "sub"]):
                    tag.decompose()

                text = clean_text(slice_soup.get_text()).strip()
                if text:
                    self.content_texts[current_src] = text
                    self.content_lengths[current_src] = len(text)
                else:
                    self.content_texts[current_src] = ""
                    self.content_lengths[current_src] = 0
            else:
                self.content_texts[current_src] = ""
                self.content_lengths[current_src] = 0

        # 4. Extract text and store using the original TOC entry src as the key
        if ordered_nav_entries:
            first_entry = ordered_nav_entries[0]
            first_doc_href = first_entry["doc_href"]
            first_pos = first_entry["position"]
            first_doc_order = first_entry["doc_order"]
            prefix_html = ""

            for doc_idx in range(first_doc_order):
                if doc_idx < len(spine_docs):
                    intermediate_doc_href = spine_docs[doc_idx]
                    prefix_html += self.doc_content.get(intermediate_doc_href, "")
                else:
                    logging.warning(
                        f"Document index {doc_idx} out of bounds for spine (length {len(spine_docs)})."
                    )

            first_doc_html = self.doc_content.get(first_doc_href, "")
            prefix_html += first_doc_html[:first_pos]

            if prefix_html.strip():
                prefix_soup = BeautifulSoup(prefix_html, "html.parser")
                for tag in prefix_soup.find_all(["sup", "sub"]):
                    tag.decompose()
                prefix_text = clean_text(prefix_soup.get_text()).strip()

                if prefix_text:
                    prefix_chapter_src = "internal:prefix_content"
                    self.content_texts[prefix_chapter_src] = prefix_text
                    self.content_lengths[prefix_chapter_src] = len(prefix_text)
                    self.processed_nav_structure.insert(
                        0,
                        {
                            "src": prefix_chapter_src,
                            "title": "Introduction",
                            "children": [],
                        },
                    )
                    logging.info(
                        f"Added prefix content chapter '{prefix_chapter_src}'."
                    )

        logging.info(
            f"Finished processing EPUB navigation. Found {len(self.content_texts)} content sections linked to TOC."
        )

    def _find_doc_key(self, base_href, doc_order, doc_order_decoded):
        """Find the best matching doc_key for a given base_href using robust matching."""
        candidates = [
            base_href,
            urllib.parse.unquote(base_href),
        ]
        base_name = os.path.basename(base_href).lower()
        for k in list(doc_order.keys()) + list(doc_order_decoded.keys()):
            if os.path.basename(k).lower() == base_name:
                candidates.append(k)
        for candidate in candidates:
            if candidate in doc_order:
                return candidate, doc_order[candidate]
            elif candidate in doc_order_decoded:
                return candidate, doc_order_decoded[candidate]
        return None, None

    def _parse_ncx_navpoint(
        self,
        nav_point,
        ordered_entries,
        doc_order,
        doc_order_decoded,
        tree_structure_list,
        find_position_func,
    ):
        nav_label = nav_point.find("navLabel")
        content = nav_point.find("content")
        title = (
            nav_label.find("text").get_text(strip=True)
            if nav_label and nav_label.find("text")
            else "Untitled Section"
        )
        src = content["src"] if content and "src" in content.attrs else None

        current_entry_node = {"title": title, "src": src, "children": []}

        if src:
            base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
            doc_key, doc_idx = self._find_doc_key(
                base_href, doc_order, doc_order_decoded
            )
            if not doc_key:
                logging.warning(
                    f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list (even after basename fallback)."
                )
                current_entry_node["has_content"] = False
            else:
                position = find_position_func(doc_key, fragment)
                entry_data = {
                    "src": src,
                    "title": title,
                    "doc_href": doc_key,
                    "position": position,
                    "doc_order": doc_idx,
                }
                ordered_entries.append(entry_data)
                current_entry_node["has_content"] = True
        else:
            logging.warning(f"Navigation entry '{title}' has no 'src' attribute.")
            current_entry_node["has_content"] = False

        child_navpoints = nav_point.find_all("navPoint", recursive=False)
        if child_navpoints:
            for child_np in child_navpoints:
                # Pass find_position_func down recursively
                self._parse_ncx_navpoint(
                    child_np,
                    ordered_entries,
                    doc_order,
                    doc_order_decoded,
                    current_entry_node["children"],
                    find_position_func,
                )

        if title and (
            current_entry_node.get("has_content", False)
            or current_entry_node["children"]
        ):
            tree_structure_list.append(current_entry_node)

    def _parse_html_nav_li(
        self,
        li_element,
        ordered_entries,
        doc_order,
        doc_order_decoded,
        tree_structure_list,
        find_position_func,
    ):
        link = li_element.find("a", recursive=False)
        span_text = li_element.find("span", recursive=False)
        title = "Untitled Section"
        src = None
        current_entry_node = {"children": []}

        if link and "href" in link.attrs:
            src = link["href"]
            title = link.get_text(strip=True) or title
            if not title.strip() and span_text:
                title = span_text.get_text(strip=True) or title
            if not title.strip():
                li_text = "".join(
                    t for t in li_element.contents if isinstance(t, NavigableString)
                ).strip()
                title = li_text or title
        elif span_text:
            title = span_text.get_text(strip=True) or title
            if not title.strip():
                li_text = "".join(
                    t for t in li_element.contents if isinstance(t, NavigableString)
                ).strip()
                title = li_text or title
        else:
            li_text = "".join(
                t for t in li_element.contents if isinstance(t, NavigableString)
            ).strip()
            title = li_text or title

        current_entry_node["title"] = title
        current_entry_node["src"] = src

        doc_key = None
        doc_idx = None
        position = 0
        fragment = None
        if src:
            base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
            doc_key, doc_idx = self._find_doc_key(
                base_href, doc_order, doc_order_decoded
            )
            if doc_key is not None:
                position = find_position_func(doc_key, fragment)
                entry_data = {
                    "src": src,
                    "title": title,
                    "doc_href": doc_key,
                    "position": position,
                    "doc_order": doc_idx,
                }
                ordered_entries.append(entry_data)
                current_entry_node["has_content"] = True
            else:
                logging.warning(
                    f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list (even after basename fallback)."
                )
                current_entry_node["has_content"] = False
        else:
            current_entry_node["has_content"] = False

        for child_ol in li_element.find_all("ol", recursive=False):
            for child_li in child_ol.find_all("li", recursive=False):
                self._parse_html_nav_li(
                    child_li,
                    ordered_entries,
                    doc_order,
                    doc_order_decoded,
                    current_entry_node["children"],
                    find_position_func,
                )
        tree_structure_list.append(current_entry_node)

    def _find_position_robust(self, doc_href, fragment_id):
        if doc_href not in self.doc_content:
            logging.warning(f"Document '{doc_href}' not found in cached content.")
            return 0
        html_content = self.doc_content[doc_href]
        if not fragment_id:
            return 0

        try:
            temp_soup = BeautifulSoup(f"<div>{html_content}</div>", "html.parser")
            target_element = temp_soup.find(id=fragment_id)
            if target_element:
                tag_str = str(target_element)
                pos = html_content.find(tag_str[: min(len(tag_str), 200)])
                if pos != -1:
                    logging.debug(
                        f"Found position for id='{fragment_id}' in {doc_href} using BeautifulSoup: {pos}"
                    )
                    return pos
        except Exception as e:
            logging.warning(
                f"BeautifulSoup failed to find id='{fragment_id}' in {doc_href}: {e}"
            )

        safe_fragment_id = re.escape(fragment_id)
        id_name_pattern = re.compile(
            f"<[^>]+(?:id|name)\\s*=\\s*[\"']{safe_fragment_id}[\"']", re.IGNORECASE
        )
        match = id_name_pattern.search(html_content)
        if match:
            pos = match.start()
            logging.debug(
                f"Found position for id/name='{fragment_id}' in {doc_href} using regex: {pos}"
            )
            return pos

        id_match_str = f'id="{fragment_id}"'
        name_match_str = f'name="{fragment_id}"'
        id_pos = html_content.find(id_match_str)
        name_pos = html_content.find(name_match_str)

        pos = -1
        if id_pos != -1 and name_pos != -1:
            pos = min(id_pos, name_pos)
        elif id_pos != -1:
            pos = id_pos
        elif name_pos != -1:
            pos = name_pos

        if pos != -1:
            tag_start_pos = html_content.rfind("<", 0, pos)
            final_pos = tag_start_pos if tag_start_pos != -1 else 0
            logging.debug(
                f"Found position for id/name='{fragment_id}' in {doc_href} using string search: {final_pos}"
            )
            return final_pos

        logging.warning(
            f"Anchor '{fragment_id}' not found in {doc_href}. Defaulting to position 0."
        )
        return 0

    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.file_type == "epub":
            if (
                hasattr(self, "processed_nav_structure")
                and self.processed_nav_structure
            ):
                self._build_epub_tree_from_nav(
                    self.processed_nav_structure, self.treeWidget
                )
            else:
                logging.warning("Building EPUB tree using fallback book.toc.")
                self._build_epub_tree_fallback(self.book.toc, self.treeWidget)
        elif self.file_type == "markdown":
            self._build_markdown_tree()
        else:
            self._build_pdf_tree()

        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_epub_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_epub_tree_from_nav(children, item, seen_content_hashes)

    def _build_epub_tree_fallback(self, toc_entries, parent_item):
        for entry in toc_entries:
            href, title, children = None, "Unknown", []
            entry_obj = None
            if isinstance(entry, ebooklib.epub.Link):
                href, title = entry.href, entry.title or entry.href
                entry_obj = entry
            elif isinstance(entry, tuple) and len(entry) >= 1:
                section_or_link = entry[0]
                entry_obj = section_or_link
                if isinstance(section_or_link, ebooklib.epub.Section):
                    title = section_or_link.title
                    href = getattr(section_or_link, "href", None)
                elif isinstance(section_or_link, ebooklib.epub.Link):
                    href, title = (
                        section_or_link.href,
                        section_or_link.title or section_or_link.href,
                    )

                if len(entry) > 1 and isinstance(entry[1], list):
                    children = entry[1]
            else:
                continue

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

            has_content = (
                href and href in self.content_texts and self.content_texts[href].strip()
            )

            if has_content or children:
                item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                is_checked = href and href in self.checked_chapters
                item.setCheckState(
                    0, Qt.CheckState.Checked if is_checked else Qt.CheckState.Unchecked
                )
            else:
                item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)

            if children:
                self._build_epub_tree_fallback(children, item)

    def _build_pdf_tree(self):
        outline = self.pdf_doc.get_toc()
        self.has_pdf_bookmarks = bool(outline)
        self.bookmark_items_map = {}

        if not outline:
            self._build_pdf_pages_tree()
            return

        bookmark_pages = []
        page_to_bookmark = {}
        next_page_boundaries = {}
        added_pages = set()

        def extract_page_numbers(entries):
            for entry in entries:
                if len(entry) >= 3:
                    _, title, page = entry[:3]
                    page_num = (
                        page - 1
                        if isinstance(page, int)
                        else self.pdf_doc.resolve_link(page)[0]
                    )
                    bookmark_pages.append((page_num, title))

                    if len(entry) > 3 and isinstance(entry[3], list):
                        extract_page_numbers(entry[3])

        extract_page_numbers(outline)
        bookmark_pages.sort()

        for i, (page_num, title) in enumerate(bookmark_pages):
            if i < len(bookmark_pages) - 1:
                next_page_boundaries[page_num] = bookmark_pages[i + 1][0]
            page_to_bookmark[page_num] = title

        def build_outline_tree(entries, parent_item):
            for entry in entries:
                if len(entry) >= 3:
                    entry_level, title, page = entry[:3]
                    page_num = (
                        page - 1
                        if isinstance(page, int)
                        else self.pdf_doc.resolve_link(page)[0]
                    )
                    page_id = f"page_{page_num + 1}"
                    # attach chapters on same page under original
                    if page_num in self.bookmark_items_map:
                        orig = self.bookmark_items_map[page_num]
                        child = QTreeWidgetItem(orig, [f"{title} (Same page)"])
                        child.setData(0, Qt.ItemDataRole.UserRole, page_id)
                        child.setFlags(child.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
                        continue
                    bookmark_item = QTreeWidgetItem(parent_item, [title])
                    bookmark_item.setData(0, Qt.ItemDataRole.UserRole, page_id)
                    # only allow checking if this chapter has content
                    if self.content_lengths.get(page_id, 0) > 0:
                        bookmark_item.setFlags(
                            bookmark_item.flags() | Qt.ItemFlag.ItemIsUserCheckable
                        )
                        bookmark_item.setCheckState(
                            0,
                            (
                                Qt.CheckState.Checked
                                if page_id in self.checked_chapters
                                else Qt.CheckState.Unchecked
                            ),
                        )
                    else:
                        bookmark_item.setFlags(
                            bookmark_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable
                        )
                    # map for uncategorized pages
                    self.bookmark_items_map[page_num] = bookmark_item

                    added_pages.add(page_num)

                    next_page = next_page_boundaries.get(page_num, len(self.pdf_doc))
                    for sub_page_num in range(page_num + 1, next_page):
                        if (
                            sub_page_num in page_to_bookmark
                            or sub_page_num in added_pages
                        ):
                            continue

                        page_id = f"page_{sub_page_num + 1}"
                        page_title = f"Page {sub_page_num + 1}"

                        page_text = self.content_texts.get(page_id, "").strip()
                        if page_text:
                            first_line = page_text.split("\n", 1)[0].strip()
                            if first_line and len(first_line) < 100:
                                page_title += f" - {first_line}"

                        page_item = QTreeWidgetItem(bookmark_item, [page_title])
                        page_item.setData(0, Qt.ItemDataRole.UserRole, page_id)
                        # only allow checking if this sub-page has content
                        if self.content_lengths.get(page_id, 0) > 0:
                            page_item.setFlags(
                                page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable
                            )
                            page_item.setCheckState(
                                0,
                                (
                                    Qt.CheckState.Checked
                                    if page_id in self.checked_chapters
                                    else Qt.CheckState.Unchecked
                                ),
                            )
                        else:
                            page_item.setFlags(
                                page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable
                            )

                        added_pages.add(sub_page_num)

        build_outline_tree(outline, self.treeWidget)

        covered_pages = set(added_pages)
        # attach any pages without direct bookmarks under nearest preceding chapter
        uncategorized_pages = [
            i for i in range(len(self.pdf_doc)) if i not in covered_pages
        ]
        for page_num in uncategorized_pages:
            # find nearest previous bookmark
            prev_nums = [n for n in sorted(self.bookmark_items_map) if n < page_num]
            parent_item = (
                self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget
            )
            page_id = f"page_{page_num + 1}"
            title = f"Page {page_num + 1}"
            text = self.content_texts.get(page_id, "").strip()
            if text:
                first = text.split("\n", 1)[0].strip()
                if first and len(first) < 100:
                    title += f" - {first}"
            page_item = QTreeWidgetItem(parent_item, [title])
            page_item.setData(0, Qt.ItemDataRole.UserRole, page_id)
            # only allow checking if uncategorized page has content
            if self.content_lengths.get(page_id, 0) > 0:
                page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                page_item.setCheckState(
                    0,
                    (
                        Qt.CheckState.Checked
                        if page_id in self.checked_chapters
                        else Qt.CheckState.Unchecked
                    ),
                )
            else:
                page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)

    def _build_markdown_tree(self):
        """Build tree structure for markdown file based on parsed TOC."""
        if not self.markdown_text:
            return

        if not self.markdown_toc:
            # Handle case with no headers (single content block)
            if self.content_texts:
                chapter_id = list(self.content_texts.keys())[0]
                title = "Content"
                item = QTreeWidgetItem(self.treeWidget, [title])
                item.setData(0, Qt.ItemDataRole.UserRole, chapter_id)
                if self.content_lengths.get(chapter_id, 0) > 0:
                    item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                    is_checked = chapter_id in self.checked_chapters
                    item.setCheckState(
                        0,
                        (
                            Qt.CheckState.Checked
                            if is_checked
                            else Qt.CheckState.Unchecked
                        ),
                    )
                else:
                    item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
            return

        def build_from_toc(toc_list, parent_item):
            for header in toc_list:
                title = header["name"]
                chapter_id = header["id"]

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

                has_content = self.content_lengths.get(chapter_id, 0) > 0
                has_children = bool(header.get("children"))

                if has_content or has_children:
                    item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                    is_checked = chapter_id in self.checked_chapters
                    item.setCheckState(
                        0,
                        (
                            Qt.CheckState.Checked
                            if is_checked
                            else Qt.CheckState.Unchecked
                        ),
                    )
                else:
                    item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)

                if has_children:
                    build_from_toc(header["children"], item)

        build_from_toc(self.markdown_toc, self.treeWidget)

    def _build_pdf_pages_tree(self):
        pages_item = QTreeWidgetItem(self.treeWidget, ["Pages"])
        pages_item.setFlags(pages_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
        font = pages_item.font(0)
        font.setBold(True)
        pages_item.setFont(0, font)

        for page_num in range(len(self.pdf_doc)):
            page_id = f"page_{page_num + 1}"
            page_title = f"Page {page_num + 1}"

            page_text = self.content_texts.get(page_id, "").strip()
            if page_text:
                first_line = page_text.split("\n", 1)[0].strip()
                if first_line and len(first_line) < 100:
                    page_title += f" - {first_line}"

            page_item = QTreeWidgetItem(pages_item, [page_title])
            page_item.setData(0, Qt.ItemDataRole.UserRole, page_id)
            # only allow checking if standalone page has content
            if self.content_lengths.get(page_id, 0) > 0:
                page_item.setFlags(page_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                page_item.setCheckState(
                    0,
                    (
                        Qt.CheckState.Checked
                        if page_id in self.checked_chapters
                        else Qt.CheckState.Unchecked
                    ),
                )
            else:
                page_item.setFlags(page_item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)

    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.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.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.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.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.file_type == "epub":
            self._run_epub_auto_check()
        elif self.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):
        if hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks:
            iterator = QTreeWidgetItemIterator(self.treeWidget)
            while iterator.value():
                item = iterator.value()
                if item.flags() & Qt.ItemFlag.ItemIsUserCheckable:
                    item.setCheckState(0, Qt.CheckState.Checked)
                iterator += 1
            return

        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

            if (
                not identifier.startswith("page_")
                or self.content_lengths.get(identifier, 0) > 0
            ):
                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.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;'>"

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

                image_type = "jpeg"
                if self.book_metadata["cover_image"].startswith(b"\x89PNG"):
                    image_type = "png"
                elif self.book_metadata["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 += f"width='300' style='object-fit: contain;' /></div>"
            except Exception as e:
                html_content += f"<p>Error displaying cover image: {str(e)}</p>"

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

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

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

        html_content += "<hr/>"

        if self.book_metadata["description"]:
            desc = re.sub(r"<[^>]+>", "", self.book_metadata["description"])
            html_content += f"<h3>Description:</h3><p>{desc}</p>"

        if self.file_type == "pdf":
            page_count = len(self.pdf_doc) if self.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):
        metadata = {
            "title": None,
            "authors": [],
            "description": None,
            "cover_image": None,
            "publisher": None,
            "publication_year": None,
        }

        if self.file_type == "epub":
            try:
                title_items = self.book.get_metadata("DC", "title")
                if title_items and len(title_items) > 0:
                    metadata["title"] = title_items[0][0]
            except Exception as e:
                logging.warning(f"Error extracting title metadata: {e}")

            try:
                author_items = self.book.get_metadata("DC", "creator")
                if author_items:
                    metadata["authors"] = [
                        author[0] for author in author_items if len(author) > 0
                    ]
            except Exception as e:
                logging.warning(f"Error extracting author metadata: {e}")

            try:
                desc_items = self.book.get_metadata("DC", "description")
                if desc_items and len(desc_items) > 0:
                    metadata["description"] = desc_items[0][0]
            except Exception as e:
                logging.warning(f"Error extracting description metadata: {e}")

            try:
                publisher_items = self.book.get_metadata("DC", "publisher")
                if publisher_items and len(publisher_items) > 0:
                    metadata["publisher"] = publisher_items[0][0]
            except Exception as e:
                logging.warning(f"Error extracting publisher metadata: {e}")

            # Try to extract publication year
            try:
                date_items = self.book.get_metadata("DC", "date")
                if date_items and len(date_items) > 0:
                    date_str = date_items[0][0]
                    # Try to extract just the year from the date string
                    year_match = re.search(r"\b(19|20)\d{2}\b", date_str)
                    if year_match:
                        metadata["publication_year"] = year_match.group(0)
                    else:
                        metadata["publication_year"] = date_str
            except Exception as e:
                logging.warning(f"Error extracting publication date metadata: {e}")

            for item in self.book.get_items_of_type(ebooklib.ITEM_COVER):
                metadata["cover_image"] = item.get_content()
                break

            if not metadata["cover_image"]:
                for item in self.book.get_items_of_type(ebooklib.ITEM_IMAGE):
                    if "cover" in item.get_name().lower():
                        metadata["cover_image"] = item.get_content()
                        break
        elif self.file_type == "markdown":
            # Extract metadata from markdown frontmatter or first heading
            if self.markdown_text:
                # Try to extract YAML frontmatter
                frontmatter_match = re.match(
                    r"^---\s*\n(.*?)\n---\s*\n", self.markdown_text, re.DOTALL
                )
                if frontmatter_match:
                    try:
                        frontmatter = frontmatter_match.group(1)
                        # Simple YAML-like parsing for common fields
                        title_match = re.search(
                            r"^title:\s*(.+)$",
                            frontmatter,
                            re.MULTILINE | re.IGNORECASE,
                        )
                        if title_match:
                            metadata["title"] = (
                                title_match.group(1).strip().strip("\"'")
                            )

                        author_match = re.search(
                            r"^author:\s*(.+)$",
                            frontmatter,
                            re.MULTILINE | re.IGNORECASE,
                        )
                        if author_match:
                            metadata["authors"] = [
                                author_match.group(1).strip().strip("\"'")
                            ]

                        desc_match = re.search(
                            r"^description:\s*(.+)$",
                            frontmatter,
                            re.MULTILINE | re.IGNORECASE,
                        )
                        if desc_match:
                            metadata["description"] = (
                                desc_match.group(1).strip().strip("\"'")
                            )

                        date_match = re.search(
                            r"^date:\s*(.+)$", frontmatter, re.MULTILINE | re.IGNORECASE
                        )
                        if date_match:
                            date_str = date_match.group(1).strip().strip("\"'")
                            year_match = re.search(r"\b(19|20)\d{2}\b", date_str)
                            if year_match:
                                metadata["publication_year"] = year_match.group(0)
                    except Exception as e:
                        logging.warning(f"Error parsing markdown frontmatter: {e}")

                # Fallback: use first H1 header as title if no frontmatter title
                if not metadata["title"] and self.markdown_toc:
                    # Find the first level 1 header
                    first_h1 = next(
                        (h for h in self.markdown_toc if h["level"] == 1), None
                    )
                    if first_h1:
                        metadata["title"] = first_h1["name"]
        else:
            pdf_info = self.pdf_doc.metadata
            if pdf_info:
                metadata["title"] = pdf_info.get("title", None)

                author = pdf_info.get("author", None)
                if author:
                    metadata["authors"] = [author]

                metadata["description"] = pdf_info.get("subject", None)

                keywords = pdf_info.get("keywords", None)
                if keywords:
                    if metadata["description"]:
                        metadata["description"] += f"\n\nKeywords: {keywords}"
                    else:
                        metadata["description"] = f"Keywords: {keywords}"

                metadata["publisher"] = pdf_info.get("creator", None)

                # Try to extract publication date from PDF metadata
                if "creationDate" in pdf_info:
                    date_str = pdf_info["creationDate"]
                    year_match = re.search(r"D:(\d{4})", date_str)
                    if year_match:
                        metadata["publication_year"] = year_match.group(1)
                elif "modDate" in pdf_info:
                    date_str = pdf_info["modDate"]
                    year_match = re.search(r"D:(\d{4})", date_str)
                    if year_match:
                        metadata["publication_year"] = year_match.group(1)

            if len(self.pdf_doc) > 0:
                try:
                    pix = self.pdf_doc[0].get_pixmap(matrix=fitz.Matrix(2, 2))
                    metadata["cover_image"] = pix.tobytes("png")
                except Exception:
                    pass

        return metadata

    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.file_type == "epub":
            return self._get_epub_selected_text()
        elif self.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"""
        import datetime
        from abogen.utils import get_user_cache_path

        metadata = self.book_metadata
        filename = os.path.splitext(os.path.basename(self.book_path))[0]
        current_year = str(datetime.datetime.now().year)

        # Get values with fallbacks
        title = metadata.get("title") or filename
        authors = metadata.get("authors") or ["Unknown"]
        authors_text = ", ".join(authors)
        album_artist = authors_text or "Unknown"
        year = (
            metadata.get("publication_year") or current_year
        )  # Use publication year if available

        # Count chapters/pages
        total_chapters = len(self.checked_chapters)
        chapter_text = (
            f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
        )

        # Handle cover image
        cover_tag = ""
        if metadata.get("cover_image"):
            try:
                import uuid

                cache_dir = get_user_cache_path()
                cover_path = os.path.join(cache_dir, f"cover_{uuid.uuid4()}.jpg")
                cover_path = os.path.normpath(cover_path)
                with open(cover_path, "wb") as f:
                    f.write(metadata["cover_image"])
                cover_tag = f"<<METADATA_COVER_PATH:{cover_path}>>"
            except Exception as e:
                logging.warning(f"Failed to save cover image: {e}")

        # Format metadata tags
        metadata_tags = [
            f"<<METADATA_TITLE:{title}>>",
            f"<<METADATA_ARTIST:{authors_text}>>",
            f"<<METADATA_ALBUM:{title} ({chapter_text})>>",
            f"<<METADATA_YEAR:{year}>>",
            f"<<METADATA_ALBUM_ARTIST:{album_artist}>>",
            f"<<METADATA_COMPOSER:Narrator>>",
            f"<<METADATA_GENRE:Audiobook>>",
        ]

        if cover_tag:
            metadata_tags.append(cover_tag)

        return "\n".join(metadata_tags)

    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
                title = re.sub(r"^\s*[-–—]\s*", "", 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)
                title = re.sub(r"^\s*[-–—]\s*", "", 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():
                        title = re.sub(r"^\s*-\s*", "", 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:
                    title = re.sub(r"^\s*-\s*", "", 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)
                        title = re.sub(r"^\s*-\s*", "", 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))

    def closeEvent(self, event):
        if self.pdf_doc is not None:
            try:
                if hasattr(self.pdf_doc, "is_closed"):
                    if not self.pdf_doc.is_closed:
                        self.pdf_doc.close()
                else:
                    # Fallback: try/except close
                    self.pdf_doc.close()
            except Exception:
                pass
        event.accept()
