"""
File operations mixin for pyPhotoAlbum
"""

import os
from typing import TYPE_CHECKING, Optional, cast

from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtWidgets import (
    QFileDialog,
    QDialog,
    QVBoxLayout,
    QHBoxLayout,
    QLabel,
    QDoubleSpinBox,
    QSpinBox,
    QPushButton,
    QGroupBox,
    QRadioButton,
    QButtonGroup,
    QLineEdit,
    QTextEdit,
    QWidget,
    QMessageBox,
)
from pyPhotoAlbum.decorators import ribbon_action, numerical_input
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.async_project_loader import AsyncProjectLoader
from pyPhotoAlbum.loading_widget import LoadingWidget
from pyPhotoAlbum.project_serializer import save_to_zip, save_to_zip_async
from pyPhotoAlbum.models import set_asset_resolution_context
from pyPhotoAlbum.version_manager import format_version_info, CURRENT_DATA_VERSION
from pyPhotoAlbum.asset_heal_dialog import AssetHealDialog


class _SaveBridge(QObject):
    """Thread-safe signal bridge for async save callbacks.

    Signals can be safely emitted from any thread; connected slots run on
    the main (GUI) thread via Qt's queued connection.
    """

    progress = pyqtSignal(int, str)
    finished = pyqtSignal(bool, str)


class FileOperationsMixin:
    """Mixin providing file-related operations"""

    # Type hints for expected attributes from mixing class
    def show_status(self, message: str, timeout: int = 0) -> None:
        """Expected from ApplicationStateMixin"""
        ...

    def show_error(self, title: str, message: str) -> None:
        """Expected from ApplicationStateMixin"""
        ...

    def resolve_asset_path(self, path: str) -> Optional[str]:
        """Expected from asset path mixin"""
        ...

    @ribbon_action(label="New", tooltip="Create a new project", tab="Home", group="File", shortcut="Ctrl+N")
    def new_project(self):
        """Create a new project with initial setup dialog"""
        # Create new project setup dialog
        dialog = QDialog(self)
        dialog.setWindowTitle("New Project Setup")
        dialog.setMinimumWidth(450)

        layout = QVBoxLayout()

        # Project name group
        name_group = QGroupBox("Project Name")
        name_layout = QVBoxLayout()
        name_input = QLineEdit()
        name_input.setText("New Project")
        name_input.selectAll()
        name_layout.addWidget(name_input)
        name_group.setLayout(name_layout)
        layout.addWidget(name_group)

        # Default page size group
        size_group = QGroupBox("Default Page Size")
        size_layout = QVBoxLayout()

        info_label = QLabel("This will be the default size for all new pages in this project.")
        info_label.setWordWrap(True)
        info_label.setStyleSheet("font-size: 9pt; color: gray;")
        size_layout.addWidget(info_label)

        # Width
        width_layout = QHBoxLayout()
        width_layout.addWidget(QLabel("Width:"))
        width_spinbox = QDoubleSpinBox()
        width_spinbox.setRange(10, 1000)
        width_spinbox.setValue(140)  # Default 14cm
        width_spinbox.setSuffix(" mm")
        width_layout.addWidget(width_spinbox)
        size_layout.addLayout(width_layout)

        # Height
        height_layout = QHBoxLayout()
        height_layout.addWidget(QLabel("Height:"))
        height_spinbox = QDoubleSpinBox()
        height_spinbox.setRange(10, 1000)
        height_spinbox.setValue(140)  # Default 14cm
        height_spinbox.setSuffix(" mm")
        height_layout.addWidget(height_spinbox)
        size_layout.addLayout(height_layout)

        # Add common size presets
        presets_layout = QHBoxLayout()
        presets_layout.addWidget(QLabel("Presets:"))

        def set_preset(w, h):
            width_spinbox.setValue(w)
            height_spinbox.setValue(h)

        preset_a4 = QPushButton("A4 (210×297)")
        preset_a4.clicked.connect(lambda: set_preset(210, 297))
        presets_layout.addWidget(preset_a4)

        preset_a5 = QPushButton("A5 (148×210)")
        preset_a5.clicked.connect(lambda: set_preset(148, 210))
        presets_layout.addWidget(preset_a5)

        preset_square = QPushButton("Square (200×200)")
        preset_square.clicked.connect(lambda: set_preset(200, 200))
        presets_layout.addWidget(preset_square)

        presets_layout.addStretch()
        size_layout.addLayout(presets_layout)

        size_group.setLayout(size_layout)
        layout.addWidget(size_group)

        # DPI settings group
        dpi_group = QGroupBox("DPI Settings")
        dpi_layout = QVBoxLayout()

        # Working DPI
        working_dpi_layout = QHBoxLayout()
        working_dpi_layout.addWidget(QLabel("Working DPI:"))
        working_dpi_spinbox = QSpinBox()
        working_dpi_spinbox.setRange(72, 1200)
        working_dpi_spinbox.setValue(300)
        working_dpi_layout.addWidget(working_dpi_spinbox)
        dpi_layout.addLayout(working_dpi_layout)

        # Export DPI
        export_dpi_layout = QHBoxLayout()
        export_dpi_layout.addWidget(QLabel("Export DPI:"))
        export_dpi_spinbox = QSpinBox()
        export_dpi_spinbox.setRange(72, 1200)
        export_dpi_spinbox.setValue(300)
        export_dpi_layout.addWidget(export_dpi_spinbox)
        dpi_layout.addLayout(export_dpi_layout)

        dpi_group.setLayout(dpi_layout)
        layout.addWidget(dpi_group)

        # Buttons
        button_layout = QHBoxLayout()
        cancel_btn = QPushButton("Cancel")
        cancel_btn.clicked.connect(dialog.reject)
        create_btn = QPushButton("Create Project")
        create_btn.clicked.connect(dialog.accept)
        create_btn.setDefault(True)

        button_layout.addStretch()
        button_layout.addWidget(cancel_btn)
        button_layout.addWidget(create_btn)
        layout.addLayout(button_layout)

        dialog.setLayout(layout)

        # Show dialog
        if dialog.exec() == QDialog.DialogCode.Accepted:
            # Get values
            project_name = name_input.text().strip() or "New Project"
            width_mm = width_spinbox.value()
            height_mm = height_spinbox.value()
            working_dpi = working_dpi_spinbox.value()
            export_dpi = export_dpi_spinbox.value()

            # Cleanup old project if it exists
            if hasattr(self, "project") and self.project:
                self.project.cleanup()

            # Create project with custom settings
            self.project = Project(project_name)
            self.project.page_size_mm = (width_mm, height_mm)
            self.project.working_dpi = working_dpi
            self.project.export_dpi = export_dpi

            # Set asset resolution context
            set_asset_resolution_context(self.project.folder_path)

            # Update view
            self.update_view()

            self.show_status(f"New project created: {project_name} ({width_mm}×{height_mm} mm)")
            print(f"New project created: {project_name}, default page size: {width_mm}×{height_mm} mm")
        else:
            # User cancelled - keep current project
            print("New project creation cancelled")

    @ribbon_action(label="Open", tooltip="Open an existing project", tab="Home", group="File", shortcut="Ctrl+O")
    def open_project(self):
        """Open an existing project with async loading and progress bar"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "Open Project", "", "pyPhotoAlbum Projects (*.ppz);;All Files (*)"
        )

        if file_path:
            print(f"Opening project: {file_path}")

            # Create loading widget if not exists
            if not hasattr(self, "_loading_widget"):
                self._loading_widget = LoadingWidget(self)

            # Show loading widget
            self._loading_widget.show_loading("Opening project...")

            # Create and configure async loader
            self._project_loader = AsyncProjectLoader(file_path)
            self._opening_file_path = file_path  # Store for later

            # Connect signals
            self._project_loader.progress_updated.connect(self._on_load_progress)
            self._project_loader.load_complete.connect(self._on_load_complete)
            self._project_loader.load_failed.connect(self._on_load_failed)

            # Start async loading
            self._project_loader.start()

    def _on_load_progress(self, current: int, total: int, message: str):
        """Handle loading progress updates"""
        if hasattr(self, "_loading_widget"):
            self._loading_widget.set_progress(current, total)
            self._loading_widget.set_status(message)

    def _on_load_complete(self, project):
        """Handle successful project load"""
        # Cleanup old project if it exists
        if hasattr(self, "project") and self.project:
            self.project.cleanup()

        # Set new project
        self.project = project

        # Set file path and mark as clean
        if hasattr(self, "_opening_file_path"):
            self.project.file_path = self._opening_file_path
            delattr(self, "_opening_file_path")
        self.project.mark_clean()

        self.gl_widget.current_page_index = 0  # Reset to first page

        # Hide loading widget
        if hasattr(self, "_loading_widget"):
            self._loading_widget.hide_loading()

        # Update view (this will trigger progressive image loading)
        self.update_view()

        # Check for missing assets and inform user
        missing_assets = self._check_missing_assets()
        if missing_assets:
            self._show_missing_assets_warning(missing_assets)
            self.show_status(f"Project opened: {project.name} ({len(missing_assets)} missing images)")
        else:
            self.show_status(f"Project opened: {project.name}")
        print(f"Successfully loaded project: {project.name}")

    def _on_load_failed(self, error_msg: str):
        """Handle project load failure"""
        # Hide loading widget
        if hasattr(self, "_loading_widget"):
            self._loading_widget.hide_loading()

        error_msg = f"Failed to open project: {error_msg}"
        self.show_status(error_msg)
        self.show_error("Load Failed", error_msg)
        print(error_msg)

    @ribbon_action(label="Save", tooltip="Save the current project", tab="Home", group="File", shortcut="Ctrl+S")
    def save_project(self) -> bool:
        """Save the current project asynchronously with progress feedback.

        Returns True if an async save was started, False if the user cancelled.
        """
        # Prevent concurrent saves — only one background save at a time
        if getattr(self, "_save_in_progress", False):
            self.show_status("Save already in progress...")
            return False

        # If project has a file path, use it; otherwise prompt for location
        file_path = self.project.file_path if hasattr(self.project, "file_path") and self.project.file_path else None

        if not file_path:
            file_path, _ = QFileDialog.getSaveFileName(
                self, "Save Project", "", "pyPhotoAlbum Projects (*.ppz);;All Files (*)"  # type: ignore[arg-type]
            )

        if not file_path:
            return False

        self._save_in_progress = True
        print(f"Saving project to: {file_path}")

        # Create loading widget if not exists
        if not hasattr(self, "_loading_widget"):
            self._loading_widget = LoadingWidget(self)

        # Show loading widget
        self._loading_widget.show_loading("Saving project...")

        # Bridge object: signals are thread-safe so background thread can
        # emit them and slots always run on the main (GUI) thread.
        bridge = _SaveBridge(parent=self)  # type: ignore[arg-type]

        def _on_progress(progress: int, message: str):
            if hasattr(self, "_loading_widget"):
                try:
                    self._loading_widget.set_progress(progress, 100)
                    self._loading_widget.set_status(message)
                except RuntimeError:
                    pass

        def _on_finished(success: bool, error: str):
            self._save_in_progress = False
            try:
                if hasattr(self, "_loading_widget"):
                    self._loading_widget.hide_loading()
            except RuntimeError:
                pass

            if success:
                self.project.file_path = file_path
                self.project.mark_clean()
                self.show_status(f"Project saved: {file_path}")
                print(f"Successfully saved project to: {file_path}")
                if getattr(self, "_pending_close", False):
                    self._pending_close = False
                    self.close()  # type: ignore[attr-defined]
            else:
                self._pending_close = False
                error_msg = f"Failed to save project: {error}"
                self.show_status(error_msg)
                self.show_error("Save Failed", error_msg)
                print(error_msg)

        bridge.progress.connect(_on_progress)
        bridge.finished.connect(_on_finished)

        # Start async save — callbacks emit signals (thread-safe)
        save_to_zip_async(
            self.project,
            file_path,
            on_complete=lambda ok, err: bridge.finished.emit(ok, err or ""),
            on_progress=lambda p, m: bridge.progress.emit(p, m),
        )

        # Show immediate feedback
        self.show_status("Saving project in background...", 2000)
        return True

    @ribbon_action(label="Heal Assets", tooltip="Reconnect missing image assets", tab="Home", group="File")
    def heal_assets(self):
        """Open the asset healing dialog to reconnect missing images"""
        dialog = AssetHealDialog(self.project, self)
        dialog.exec()

        # Update the view to reflect any changes
        self.update_view()

    def _check_missing_assets(self) -> list:
        """Check for missing assets in the project - returns list of missing paths"""
        from pyPhotoAlbum.models import ImageData

        missing = []
        for page in self.project.pages:
            for element in page.layout.elements:
                if isinstance(element, ImageData) and element.image_path:
                    # Absolute paths need healing
                    if os.path.isabs(element.image_path):
                        missing.append(element.image_path)
                    # Paths not in assets/ need healing
                    elif not element.image_path.startswith("assets/"):
                        missing.append(element.image_path)
                    else:
                        # Check if file exists in assets using mixin
                        if not self.resolve_asset_path(element.image_path):
                            missing.append(element.image_path)
        return list(set(missing))  # Remove duplicates

    def _show_missing_assets_warning(self, missing_assets: list):
        """Show a warning about missing assets"""
        from PyQt6.QtWidgets import QMessageBox

        # Build message with list of missing images
        if len(missing_assets) <= 5:
            asset_list = "\n".join(f"  • {path}" for path in missing_assets)
        else:
            asset_list = "\n".join(f"  • {path}" for path in missing_assets[:5])
            asset_list += f"\n  ... and {len(missing_assets) - 5} more"

        msg = QMessageBox(cast(QWidget, self))
        msg.setIcon(QMessageBox.Icon.Warning)
        msg.setWindowTitle("Missing Assets")
        msg.setText(f"{len(missing_assets)} image(s) could not be found in the assets folder:")
        msg.setInformativeText(asset_list)
        msg.setDetailedText(
            "These images need to be reconnected using the 'Heal Assets' feature.\n\n"
            "Go to: Home → Heal Assets\n\n"
            "Add search paths where the original images might be located, "
            "then click 'Attempt Healing' to find and import them."
        )
        msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Open)
        btn = msg.button(QMessageBox.StandardButton.Open)
        if btn is not None:
            btn.setText("Open Heal Assets")

        result = msg.exec()
        if result == QMessageBox.StandardButton.Open:
            self.heal_assets()

    @ribbon_action(
        label="Project Settings", tooltip="Configure project-wide page size and defaults", tab="Home", group="File"
    )
    @numerical_input(fields=[("width", "Width", "mm", 10, 1000), ("height", "Height", "mm", 10, 1000)])
    def project_settings(self):
        """Configure project-wide settings including default page size"""
        # Create dialog
        dialog = QDialog(self)
        dialog.setWindowTitle("Project Settings")
        dialog.setMinimumWidth(500)

        layout = QVBoxLayout()

        # Page size group
        size_group = QGroupBox("Default Page Size")
        size_layout = QVBoxLayout()

        # Width
        width_layout = QHBoxLayout()
        width_layout.addWidget(QLabel("Width:"))
        width_spinbox = QDoubleSpinBox()
        width_spinbox.setRange(10, 1000)
        width_spinbox.setValue(self.project.page_size_mm[0])
        width_spinbox.setSuffix(" mm")
        width_layout.addWidget(width_spinbox)
        size_layout.addLayout(width_layout)

        # Height
        height_layout = QHBoxLayout()
        height_layout.addWidget(QLabel("Height:"))
        height_spinbox = QDoubleSpinBox()
        height_spinbox.setRange(10, 1000)
        height_spinbox.setValue(self.project.page_size_mm[1])
        height_spinbox.setSuffix(" mm")
        height_layout.addWidget(height_spinbox)
        size_layout.addLayout(height_layout)

        size_group.setLayout(size_layout)
        layout.addWidget(size_group)

        # DPI settings group
        dpi_group = QGroupBox("DPI Settings")
        dpi_layout = QVBoxLayout()

        # Working DPI
        working_dpi_layout = QHBoxLayout()
        working_dpi_layout.addWidget(QLabel("Working DPI:"))
        working_dpi_spinbox = QSpinBox()
        working_dpi_spinbox.setRange(72, 1200)
        working_dpi_spinbox.setValue(self.project.working_dpi)
        working_dpi_layout.addWidget(working_dpi_spinbox)
        dpi_layout.addLayout(working_dpi_layout)

        # Export DPI
        export_dpi_layout = QHBoxLayout()
        export_dpi_layout.addWidget(QLabel("Export DPI:"))
        export_dpi_spinbox = QSpinBox()
        export_dpi_spinbox.setRange(72, 1200)
        export_dpi_spinbox.setValue(self.project.export_dpi)
        export_dpi_layout.addWidget(export_dpi_spinbox)
        dpi_layout.addLayout(export_dpi_layout)

        dpi_group.setLayout(dpi_layout)
        layout.addWidget(dpi_group)

        # Content scaling options (only if pages exist and size is changing)
        scaling_group = None
        scaling_buttons = None

        scope_buttons = None
        if self.project.pages:
            scaling_group = QGroupBox("Apply to Existing Pages")
            scaling_layout = QVBoxLayout()

            # Scope: which pages to update
            scaling_layout.addWidget(QLabel("Pages to update:"))
            scope_buttons = QButtonGroup()
            scope_non_manual = QRadioButton("Non-manual pages only")
            scope_non_manual.setChecked(True)
            scope_all = QRadioButton("All pages (override manual sizing)")
            scope_buttons.addButton(scope_non_manual, 0)
            scope_buttons.addButton(scope_all, 1)
            scaling_layout.addWidget(scope_non_manual)
            scaling_layout.addWidget(scope_all)

            # Content scaling
            scaling_layout.addWidget(QLabel("Content adjustment:"))
            scaling_buttons = QButtonGroup()

            proportional_radio = QRadioButton("Resize proportionally (fit to smallest axis)")
            proportional_radio.setToolTip("Scale content uniformly to fit the new page size")
            scaling_buttons.addButton(proportional_radio, 0)
            scaling_layout.addWidget(proportional_radio)

            stretch_radio = QRadioButton("Resize on both axes (stretch)")
            stretch_radio.setToolTip("Scale width and height independently")
            scaling_buttons.addButton(stretch_radio, 1)
            scaling_layout.addWidget(stretch_radio)

            reposition_radio = QRadioButton("Keep content size, reposition to center")
            reposition_radio.setToolTip("Maintain element sizes but center them on new page")
            scaling_buttons.addButton(reposition_radio, 2)
            scaling_layout.addWidget(reposition_radio)

            none_radio = QRadioButton("Don't adjust content (page size only)")
            none_radio.setToolTip("Only change page size, leave content as-is")
            none_radio.setChecked(True)  # Default
            scaling_buttons.addButton(none_radio, 3)
            scaling_layout.addWidget(none_radio)

            scaling_group.setLayout(scaling_layout)
            layout.addWidget(scaling_group)

        # Buttons
        button_layout = QHBoxLayout()
        cancel_btn = QPushButton("Cancel")
        cancel_btn.clicked.connect(dialog.reject)
        ok_btn = QPushButton("OK")
        ok_btn.clicked.connect(dialog.accept)
        ok_btn.setDefault(True)

        button_layout.addStretch()
        button_layout.addWidget(cancel_btn)
        button_layout.addWidget(ok_btn)
        layout.addLayout(button_layout)

        dialog.setLayout(layout)

        # Show dialog
        if dialog.exec() == QDialog.DialogCode.Accepted:
            # Get new values
            new_width = width_spinbox.value()
            new_height = height_spinbox.value()
            new_working_dpi = working_dpi_spinbox.value()
            new_export_dpi = export_dpi_spinbox.value()

            # Determine scaling mode and scope
            scaling_mode = "none"
            include_manual = False
            if scaling_buttons:
                selected_id = scaling_buttons.checkedId()
                modes = {0: "proportional", 1: "stretch", 2: "reposition", 3: "none"}
                scaling_mode = modes.get(selected_id, "none")
            if scope_buttons:
                include_manual = scope_buttons.checkedId() == 1

            # Apply settings
            old_size = self.project.page_size_mm
            self.project.page_size_mm = (new_width, new_height)
            self.project.working_dpi = new_working_dpi
            self.project.export_dpi = new_export_dpi

            # Update existing pages
            if self.project.pages and old_size != (new_width, new_height):
                self._apply_page_size_to_project(old_size, (new_width, new_height), scaling_mode, include_manual)

            self.update_view()
            self.show_status(f"Project settings updated: {new_width}×{new_height} mm", 2000)
            print(f"Project settings updated: {new_width}×{new_height} mm, scaling mode: {scaling_mode}")

    def _apply_page_size_to_project(self, old_size, new_size, scaling_mode, include_manual=False):
        """
        Apply new page size to existing pages.

        Args:
            old_size: Old page size (width, height) in mm
            new_size: New page size (width, height) in mm
            scaling_mode: 'proportional', 'stretch', 'reposition', or 'none'
            include_manual: If True, also resize manually-sized pages
        """
        old_width, old_height = old_size
        new_width, new_height = new_size

        width_ratio = new_width / old_width if old_width > 0 else 1.0
        height_ratio = new_height / old_height if old_height > 0 else 1.0

        for page in self.project.pages:
            if page.is_cover:
                continue
            if page.manually_sized and not include_manual:
                continue

            # Update page size
            old_page_width, old_page_height = page.layout.size

            # For double spreads, maintain the 2x multiplier
            if page.is_double_spread:
                page.layout.size = (new_width * 2, new_height)
            else:
                page.layout.size = (new_width, new_height)

            # Apply content scaling based on mode
            if scaling_mode == "proportional":
                # Use smallest ratio to fit content
                scale = min(width_ratio, height_ratio)
                self._scale_page_elements(page, scale, scale)
            elif scaling_mode == "stretch":
                # Scale independently on each axis
                self._scale_page_elements(page, width_ratio, height_ratio)
            elif scaling_mode == "reposition":
                # Keep size, center content
                self._reposition_page_elements(page, old_size, new_size)
            # 'none' - do nothing to elements

    def _scale_page_elements(self, page, x_scale, y_scale):
        """
        Scale all elements on a page

        Args:
            page: Page object
            x_scale: Horizontal scale factor
            y_scale: Vertical scale factor
        """
        for element in page.layout.elements:
            # Scale position
            x, y = element.position
            element.position = (x * x_scale, y * y_scale)

            # Scale size
            width, height = element.size
            element.size = (width * x_scale, height * y_scale)

    def _reposition_page_elements(self, page, old_size, new_size):
        """
        Reposition elements to center them on the new page size

        Args:
            page: Page object
            old_size: Old page size (width, height) in mm
            new_size: New page size (width, height) in mm
        """
        old_width, old_height = old_size
        new_width, new_height = new_size

        x_offset = (new_width - old_width) / 2.0
        y_offset = (new_height - old_height) / 2.0

        for element in page.layout.elements:
            x, y = element.position
            element.position = (x + x_offset, y + y_offset)

    @ribbon_action(label="Export PDF", tooltip="Export project to PDF", tab="Home", group="File")
    def export_pdf(self):
        """Export project to PDF using async backend (non-blocking)"""
        # Check if we have pages to export
        if not self.project or not self.project.pages:
            self.show_status("No pages to export")
            return

        # Show file save dialog
        file_path, _ = QFileDialog.getSaveFileName(self, "Export to PDF", "", "PDF Files (*.pdf);;All Files (*)")

        if not file_path:
            return

        # Ensure .pdf extension
        if not file_path.lower().endswith(".pdf"):
            file_path += ".pdf"

        # Use async PDF export (non-blocking, UI stays responsive)
        success = self.gl_widget.export_pdf_async(self.project, file_path, export_dpi=self.project.export_dpi)
        if success:
            self.show_status("PDF export started...", 2000)
        else:
            self.show_status("PDF export failed to start", 3000)

    @ribbon_action(
        label="Clean Assets", tooltip="Find and remove duplicate or unused image files", tab="Home", group="File"
    )
    def clean_assets(self):
        """Find and remove duplicate and unused asset files to save space"""
        from PyQt6.QtWidgets import QProgressDialog, QCheckBox
        from PyQt6.QtCore import Qt

        # Helper to format bytes
        def format_bytes(num_bytes):
            if num_bytes >= 1024 * 1024:
                return f"{num_bytes / (1024 * 1024):.1f} MB"
            elif num_bytes >= 1024:
                return f"{num_bytes / 1024:.1f} KB"
            else:
                return f"{num_bytes} bytes"

        # Scan for issues with progress dialog
        progress = QProgressDialog("Scanning assets...", "Cancel", 0, 100, self)
        progress.setWindowTitle("Clean Assets")
        progress.setWindowModality(Qt.WindowModality.WindowModal)
        progress.setValue(10)

        # Compute hashes for duplicate detection
        self.project.asset_manager.compute_all_hashes()
        progress.setValue(40)

        if progress.wasCanceled():
            return

        # Get duplicate stats
        dup_groups, dup_files, dup_bytes = self.project.asset_manager.get_duplicate_stats()
        progress.setValue(60)

        # Get unused stats
        unused_files, unused_bytes = self.project.asset_manager.get_unused_stats()
        progress.setValue(80)

        progress.close()

        # Check if there's anything to clean
        if dup_files == 0 and unused_files == 0:
            QMessageBox.information(
                self, "Assets Clean", "No duplicate or unused files were found in your project assets."
            )
            return

        # Build dialog with checkboxes for each cleanup type
        dialog = QDialog(self)
        dialog.setWindowTitle("Clean Assets")
        dialog.setMinimumWidth(450)

        layout = QVBoxLayout()

        # Info label
        info_label = QLabel("Select which cleanup operations to perform:")
        layout.addWidget(info_label)

        # Duplicates checkbox
        dup_checkbox = None
        if dup_files > 0:
            dup_checkbox = QCheckBox(
                f"Remove {dup_files} duplicate file(s) in {dup_groups} group(s) " f"(saves {format_bytes(dup_bytes)})"
            )
            dup_checkbox.setChecked(True)
            dup_checkbox.setToolTip(
                "Duplicate files have identical content but different names.\n"
                "Image references will be automatically updated to use the kept file."
            )
            layout.addWidget(dup_checkbox)

        # Unused checkbox
        unused_checkbox = None
        if unused_files > 0:
            unused_checkbox = QCheckBox(f"Remove {unused_files} unused file(s) (saves {format_bytes(unused_bytes)})")
            unused_checkbox.setChecked(True)
            unused_checkbox.setToolTip(
                "Unused files exist in the assets folder but are not referenced\n"
                "by any image element in your project."
            )
            layout.addWidget(unused_checkbox)

        # Summary
        total_files = dup_files + unused_files
        total_bytes = dup_bytes + unused_bytes
        summary_label = QLabel(f"\nTotal potential savings: {format_bytes(total_bytes)} from {total_files} file(s)")
        summary_label.setStyleSheet("font-weight: bold;")
        layout.addWidget(summary_label)

        # Buttons
        button_layout = QHBoxLayout()
        cancel_btn = QPushButton("Cancel")
        cancel_btn.clicked.connect(dialog.reject)
        clean_btn = QPushButton("Clean Selected")
        clean_btn.clicked.connect(dialog.accept)
        clean_btn.setDefault(True)

        button_layout.addStretch()
        button_layout.addWidget(cancel_btn)
        button_layout.addWidget(clean_btn)
        layout.addLayout(button_layout)

        dialog.setLayout(layout)

        if dialog.exec() != QDialog.DialogCode.Accepted:
            return

        # Perform selected cleanups
        total_removed = 0
        total_saved = 0

        # Remove duplicates if selected
        if dup_checkbox and dup_checkbox.isChecked():

            def update_image_references(old_path: str, new_path: str):
                """Update all ImageData elements that reference the old path"""
                from pyPhotoAlbum.models import ImageData

                for page in self.project.pages:
                    for element in page.layout.elements:
                        if isinstance(element, ImageData) and element.image_path == old_path:
                            element.image_path = new_path
                            element.mark_modified()
                            print(f"Updated image reference: {old_path} -> {new_path}")

            removed, saved = self.project.asset_manager.deduplicate_assets(
                update_references_callback=update_image_references
            )
            total_removed += removed
            total_saved += saved

        # Remove unused if selected
        if unused_checkbox and unused_checkbox.isChecked():
            removed, saved = self.project.asset_manager.remove_unused_assets()
            total_removed += removed
            total_saved += saved

        if total_removed > 0:
            # Mark project as dirty since we modified it
            self.project.mark_dirty()

            # Update view
            self.update_view()

            # Show result
            QMessageBox.information(
                self,
                "Cleanup Complete",
                f"Removed {total_removed} file(s).\n\n"
                f"Saved {format_bytes(total_saved)} of disk space.\n\n"
                f"Remember to save your project to preserve these changes.",
            )

            self.show_status(
                f"Asset cleanup complete: removed {total_removed} files, saved {format_bytes(total_saved)}"
            )
        else:
            self.show_status("No files were removed")

    @ribbon_action(label="About", tooltip="About pyPhotoAlbum and data format version", tab="Home", group="File")
    def show_about(self):
        """Show about dialog with version information"""
        dialog = QDialog(self)
        dialog.setWindowTitle("About pyPhotoAlbum")
        dialog.setMinimumWidth(600)
        dialog.setMinimumHeight(400)

        layout = QVBoxLayout()

        # Application info
        app_info = QLabel("<h2>pyPhotoAlbum</h2>")
        app_info.setWordWrap(True)
        layout.addWidget(app_info)

        description = QLabel(
            "A photo album layout and design application with advanced "
            "page composition features and PDF export capabilities."
        )
        description.setWordWrap(True)
        layout.addWidget(description)

        # Version information
        version_text = QTextEdit()
        version_text.setReadOnly(True)
        version_text.setPlainText(format_version_info())
        layout.addWidget(version_text)

        # Close button
        close_button = QPushButton("Close")
        close_button.clicked.connect(dialog.accept)
        layout.addWidget(close_button)

        dialog.setLayout(layout)
        dialog.exec()
