"""
Project and page management for pyPhotoAlbum
"""

import os
import math
import uuid
from datetime import datetime, timezone
from tempfile import TemporaryDirectory
from typing import List, Dict, Any, Optional, Tuple, Union
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.commands import CommandHistory
from pyPhotoAlbum.asset_manager import AssetManager


class Page:
    """Class representing a single page in the photo album"""

    def __init__(self, layout: Optional[PageLayout] = None, page_number: int = 1, is_double_spread: bool = False):
        """
        Initialize a page.

        Args:
            layout: PageLayout instance (created automatically if None)
            page_number: The page number (for spreads, this is the left page number)
            is_double_spread: If True, this is a facing page spread (2x width)
        """
        self.page_number = page_number
        self.is_cover = False
        self.is_double_spread = is_double_spread
        self.manually_sized = False  # Track if user manually changed page size

        # UUID for merge conflict resolution (v3.0+)
        self.uuid = str(uuid.uuid4())

        # Timestamps for merge conflict resolution (v3.0+)
        now = datetime.now(timezone.utc).isoformat()
        self.created = now
        self.last_modified = now

        # Deletion tracking for merge (v3.0+)
        self.deleted = False
        self.deleted_at: Optional[str] = None

        # Create layout with appropriate width
        if layout is None:
            self.layout = PageLayout(is_facing_page=is_double_spread)
        else:
            self.layout = layout
            # Ensure layout matches the is_double_spread setting
            if is_double_spread != self.layout.is_facing_page:
                # Need to update the layout for the new facing page state
                self.layout.is_facing_page = is_double_spread
                height = self.layout.size[1]
                # Use the base_width if available, otherwise derive it
                if hasattr(self.layout, "base_width"):
                    base_width = self.layout.base_width
                else:
                    # If base_width not set, assume current width is correct
                    # and derive base_width from current state
                    base_width = self.layout.size[0] / 2 if not is_double_spread else self.layout.size[0]
                    self.layout.base_width = base_width

                # Set the new width based on facing page state
                self.layout.size = (base_width * 2 if is_double_spread else base_width, height)

    def get_page_numbers(self) -> List[int]:
        """
        Get the page numbers this page represents.

        Returns:
            List of page numbers (2 numbers for spreads, 1 for single pages)
        """
        if self.is_double_spread:
            return [self.page_number, self.page_number + 1]
        else:
            return [self.page_number]

    def get_page_count(self) -> int:
        """
        Get the number of physical pages this represents.

        Returns:
            2 for spreads, 1 for single pages
        """
        return 2 if self.is_double_spread else 1

    def mark_modified(self):
        """Update the last_modified timestamp to now."""
        self.last_modified = datetime.now(timezone.utc).isoformat()

    def mark_deleted(self):
        """Mark this page as deleted."""
        self.deleted = True
        self.deleted_at = datetime.now(timezone.utc).isoformat()
        self.mark_modified()

    def render(self):
        """Render the entire page"""
        print(f"Rendering page {self.page_number}")
        self.layout.render()

    def serialize(self) -> Dict[str, Any]:
        """Serialize page to dictionary"""
        return {
            "page_number": self.page_number,
            "is_cover": self.is_cover,
            "is_double_spread": self.is_double_spread,
            "manually_sized": self.manually_sized,
            "layout": self.layout.serialize(),
            # v3.0+ fields
            "uuid": self.uuid,
            "created": self.created,
            "last_modified": self.last_modified,
            "deleted": self.deleted,
            "deleted_at": self.deleted_at,
        }

    def deserialize(self, data: Dict[str, Any]):
        """Deserialize from dictionary"""
        self.page_number = data.get("page_number", 1)
        self.is_cover = data.get("is_cover", False)
        self.is_double_spread = data.get("is_double_spread", False)
        self.manually_sized = data.get("manually_sized", False)

        # v3.0+ fields (with backwards compatibility)
        self.uuid = data.get("uuid", str(uuid.uuid4()))
        now = datetime.now(timezone.utc).isoformat()
        self.created = data.get("created", now)
        self.last_modified = data.get("last_modified", now)
        self.deleted = data.get("deleted", False)
        self.deleted_at = data.get("deleted_at", None)

        layout_data = data.get("layout", {})
        self.layout = PageLayout()
        self.layout.deserialize(layout_data)


class Project:
    """Class representing the entire photo album project"""

    def __init__(self, name: str = "Untitled Project", folder_path: Optional[str] = None):
        self.name = name
        self.folder_path = folder_path or os.path.join("./projects", name.replace(" ", "_"))
        self.pages: List[Page] = []
        self.default_min_distance = 10.0  # Default minimum distance between images
        self.cover_size = (800, 600)  # Default cover size in pixels
        self.page_size = (800, 600)  # Default page size in pixels
        self.page_size_mm = (140, 140)  # Default page size in mm (14cm x 14cm square)
        self.working_dpi = 300  # Default working DPI
        self.export_dpi = 300  # Default export DPI
        self.page_spacing_mm = 10.0  # Default spacing between pages (1cm)

        # Project ID for merge detection (v3.0+)
        # Projects with same ID should be merged, different IDs should be concatenated
        self.project_id = str(uuid.uuid4())

        # Timestamps for project-level changes (v3.0+)
        now = datetime.now(timezone.utc).isoformat()
        self.created = now
        self.last_modified = now

        # Cover configuration
        self.has_cover = False  # Whether project has a cover
        self.paper_thickness_mm = 0.2  # Paper thickness for spine calculation (default 0.2mm)
        self.cover_bleed_mm = 0.0  # Bleed margin for cover (default 0mm)
        self.binding_type = "saddle_stitch"  # Binding type for spine calculation

        # Print guide configuration
        self.page_bleed_mm = 0.0  # Bleed margin for interior pages (default 0mm, e.g. 3mm for printing)
        self.page_safe_area_mm = 5.0  # Safe area margin inside cut line (default 5mm)
        self.show_print_guides = False  # Show bleed/cut/safe-area guides in the editor

        # Embedded templates - templates that travel with the project
        self.embedded_templates: Dict[str, Dict[str, Any]] = {}

        # Temporary directory management (if loaded from .ppz)
        # Using TemporaryDirectory instance that auto-cleans on deletion
        self._temp_dir: Optional[TemporaryDirectory[str]] = None

        # Global snapping settings (apply to all pages)
        self.snap_to_grid = False
        self.snap_to_edges = True
        self.snap_to_guides = True
        self.grid_size_mm = 10.0
        self.snap_threshold_mm = 5.0
        self.show_grid = False  # Show grid lines independently of snap_to_grid
        self.show_snap_lines = True  # Show snap lines (guides) during dragging

        # Initialize asset manager
        self.asset_manager = AssetManager(self.folder_path)

        # Initialize command history with asset manager and project reference
        self.history = CommandHistory(max_history=100, asset_manager=self.asset_manager, project=self)

        # Track unsaved changes
        self._dirty = False
        self.file_path = None  # Path to the saved .ppz file

    def mark_dirty(self):
        """Mark the project as having unsaved changes."""
        self._dirty = True
        self.mark_modified()

    def mark_clean(self):
        """Mark the project as saved (no unsaved changes)."""
        self._dirty = False

    def is_dirty(self) -> bool:
        """Check if the project has unsaved changes."""
        return self._dirty

    def mark_modified(self):
        """Update the last_modified timestamp to now."""
        self.last_modified = datetime.now(timezone.utc).isoformat()

    def add_page(self, page: Page, index: Optional[int] = None):
        """
        Add a page to the project.

        Args:
            page: The page to add
            index: Optional index to insert at. If None, appends to end.
        """
        if index is None:
            self.pages.append(page)
        else:
            self.pages.insert(index, page)
        # Update cover dimensions if we have a cover
        if self.has_cover and self.pages:
            self.update_cover_dimensions()
        self.mark_dirty()

    def remove_page(self, page: Page):
        """Remove a page from the project"""
        self.pages.remove(page)
        # Update cover dimensions if we have a cover
        if self.has_cover and self.pages:
            self.update_cover_dimensions()
        self.mark_dirty()

    def calculate_spine_width(self) -> float:
        """
        Calculate spine width based on page count and paper thickness.

        For saddle stitch binding:
        - Each sheet = 4 pages (2 pages per side when folded)
        - Spine width = (Number of sheets × Paper thickness × 2)

        Returns:
            Spine width in mm
        """
        if not self.has_cover:
            return 0.0

        # Count content pages (excluding cover)
        content_page_count = sum(page.get_page_count() for page in self.pages if not page.is_cover)

        if self.binding_type == "saddle_stitch":
            # Calculate number of sheets (each sheet = 4 pages)
            sheets = math.ceil(content_page_count / 4)
            # Spine width = sheets × paper thickness × 2 (folded)
            spine_width = sheets * self.paper_thickness_mm * 2
            return spine_width

        return 0.0

    def update_cover_dimensions(self):
        """
        Update cover page dimensions based on current page count and settings.
        Calculates: Front width + Spine width + Back width + Bleed margins
        """
        if not self.has_cover or not self.pages:
            return

        # Find cover page (should be first page)
        cover_page = None
        for page in self.pages:
            if page.is_cover:
                cover_page = page
                break

        if not cover_page:
            return

        # Get standard page dimensions
        page_width_mm, page_height_mm = self.page_size_mm

        # Calculate spine width
        spine_width = self.calculate_spine_width()

        # Calculate cover dimensions
        # Cover = Front + Spine + Back + Bleed on all sides
        cover_width = (page_width_mm * 2) + spine_width + (self.cover_bleed_mm * 2)
        cover_height = page_height_mm + (self.cover_bleed_mm * 2)

        # Update cover page layout
        cover_page.layout.size = (cover_width, cover_height)
        cover_page.layout.base_width = page_width_mm  # Store base width for reference
        cover_page.manually_sized = True  # Mark as manually sized

        print(
            f"Cover dimensions updated: {cover_width:.1f} × {cover_height:.1f} mm "
            f"(Front: {page_width_mm}, Spine: {spine_width:.2f}, Back: {page_width_mm}, "
            f"Bleed: {self.cover_bleed_mm})"
        )

    def get_page_display_name(self, page: Page) -> str:
        """
        Get display name for a page.

        Args:
            page: The page to get the display name for

        Returns:
            Display name like "Cover", "Page 1", "Pages 1-2", etc.
        """
        if page.is_cover:
            return "Cover"

        # Calculate adjusted page number (excluding cover from count)
        adjusted_num = page.page_number
        if self.has_cover:
            # Subtract 1 to account for cover
            adjusted_num = page.page_number - 1

        if page.is_double_spread:
            return f"Pages {adjusted_num}-{adjusted_num + 1}"
        else:
            return f"Page {adjusted_num}"

    def calculate_page_layout_with_ghosts(self) -> List[Tuple[str, Optional["Page"], int]]:
        """
        Calculate page layout including ghost pages for alignment.
        Excludes cover from spread calculations.

        Returns:
            List of tuples (page_type, page_or_ghost, logical_position)
            where page_type is 'page' or 'ghost',
            page_or_ghost is the Page object or None for ghost,
            logical_position is the position in the album (1=right, 2=left, etc.)
        """
        from pyPhotoAlbum.models import GhostPageData

        layout: list[tuple[str, Optional["Page"], int]] = []
        current_position = 1  # Start at position 1 (right page)

        for page in self.pages:
            # Skip cover in spread calculations
            if page.is_cover:
                # Cover is rendered separately, doesn't participate in spreads
                continue
            # Check if we need a ghost page for alignment
            # Ghost pages are needed when a single page would appear on the left
            # but should be on the right (odd positions)
            if not page.is_double_spread and current_position % 2 == 0:
                # Current position is even (left page), but we have a single page
                # This is fine - single page goes on left
                pass
            elif not page.is_double_spread and current_position % 2 == 1:
                # Current position is odd (right page), single page is fine
                pass

            # Actually, let me reconsider the logic:
            # In a photobook:
            # - Position 1 is the right page (when opened, first content page)
            # - Position 2 is the left page of the next spread
            # - Position 3 is the right page of the next spread
            # - etc.
            #
            # Double spreads occupy TWO positions (both left and right of a spread)
            # They must start on an even position (left side) so they span across both pages

            # Check if this is a double spread starting at an odd position
            if page.is_double_spread and current_position % 2 == 1:
                # Need to insert a ghost page to push the double spread to next position
                layout.append(("ghost", None, current_position))
                current_position += 1

            # Add the actual page
            layout.append(("page", page, current_position))

            # Update position based on page type
            if page.is_double_spread:
                current_position += 2  # Double spread takes 2 positions
            else:
                current_position += 1  # Single page takes 1 position

        return layout

    def render_all_pages(self):
        """Render all pages in the project"""
        for page in self.pages:
            page.render()

    def serialize(self) -> Dict[str, Any]:
        """Serialize entire project to dictionary"""
        return {
            "name": self.name,
            "folder_path": self.folder_path,
            "default_min_distance": self.default_min_distance,
            "cover_size": self.cover_size,
            "page_size": self.page_size,
            "page_size_mm": self.page_size_mm,
            "working_dpi": self.working_dpi,
            "export_dpi": self.export_dpi,
            "page_spacing_mm": self.page_spacing_mm,
            "has_cover": self.has_cover,
            "paper_thickness_mm": self.paper_thickness_mm,
            "cover_bleed_mm": self.cover_bleed_mm,
            "binding_type": self.binding_type,
            "page_bleed_mm": self.page_bleed_mm,
            "page_safe_area_mm": self.page_safe_area_mm,
            "show_print_guides": self.show_print_guides,
            "embedded_templates": self.embedded_templates,
            "snap_to_grid": self.snap_to_grid,
            "snap_to_edges": self.snap_to_edges,
            "snap_to_guides": self.snap_to_guides,
            "grid_size_mm": self.grid_size_mm,
            "snap_threshold_mm": self.snap_threshold_mm,
            "show_grid": self.show_grid,
            "show_snap_lines": self.show_snap_lines,
            "pages": [page.serialize() for page in self.pages],
            "history": self.history.serialize(),
            "asset_manager": self.asset_manager.serialize(),
            # v3.0+ fields
            "project_id": self.project_id,
            "created": self.created,
            "last_modified": self.last_modified,
        }

    def deserialize(self, data: Dict[str, Any]):
        """Deserialize from dictionary"""
        self.name = data.get("name", "Untitled Project")
        self.folder_path = data.get("folder_path", os.path.join("./projects", self.name.replace(" ", "_")))
        self.default_min_distance = data.get("default_min_distance", 10.0)
        self.cover_size = tuple(data.get("cover_size", (800, 600)))
        self.page_size = tuple(data.get("page_size", (800, 600)))
        self.page_size_mm = tuple(data.get("page_size_mm", (210, 297)))
        self.working_dpi = data.get("working_dpi", 300)
        self.export_dpi = data.get("export_dpi", 300)
        self.page_spacing_mm = data.get("page_spacing_mm", 10.0)
        self.has_cover = data.get("has_cover", False)
        self.paper_thickness_mm = data.get("paper_thickness_mm", 0.2)
        self.cover_bleed_mm = data.get("cover_bleed_mm", 0.0)
        self.binding_type = data.get("binding_type", "saddle_stitch")
        self.page_bleed_mm = data.get("page_bleed_mm", 0.0)
        self.page_safe_area_mm = data.get("page_safe_area_mm", 5.0)
        self.show_print_guides = data.get("show_print_guides", False)

        # Deserialize embedded templates
        self.embedded_templates = data.get("embedded_templates", {})

        # Deserialize global snapping settings
        self.snap_to_grid = data.get("snap_to_grid", False)
        self.snap_to_edges = data.get("snap_to_edges", True)
        self.snap_to_guides = data.get("snap_to_guides", True)
        self.grid_size_mm = data.get("grid_size_mm", 10.0)
        self.snap_threshold_mm = data.get("snap_threshold_mm", 5.0)
        self.show_grid = data.get("show_grid", False)
        self.show_snap_lines = data.get("show_snap_lines", True)

        # v3.0+ fields (with backwards compatibility)
        self.project_id = data.get("project_id", str(uuid.uuid4()))
        now = datetime.now(timezone.utc).isoformat()
        self.created = data.get("created", now)
        self.last_modified = data.get("last_modified", now)

        self.pages = []

        # Deserialize asset manager
        self.asset_manager = AssetManager(self.folder_path)
        asset_data = data.get("asset_manager")
        if asset_data:
            self.asset_manager.deserialize(asset_data)

        # Deserialize pages
        for page_data in data.get("pages", []):
            page = Page()
            page.deserialize(page_data)
            self.pages.append(page)

        # Deserialize command history with asset manager and project reference
        history_data = data.get("history")
        if history_data:
            self.history = CommandHistory(max_history=100, asset_manager=self.asset_manager, project=self)
            self.history.deserialize(history_data, self)
        else:
            self.history = CommandHistory(max_history=100, asset_manager=self.asset_manager, project=self)

    def cleanup(self):
        """
        Cleanup project resources, including temporary directories.
        Should be called when the project is closed or no longer needed.
        """
        if self._temp_dir is not None:
            try:
                # Let TemporaryDirectory clean itself up
                temp_path = self._temp_dir.name
                self._temp_dir.cleanup()
                self._temp_dir = None
                print(f"Cleaned up temporary project directory: {temp_path}")
            except Exception as e:
                print(f"Warning: Failed to cleanup temporary directory: {e}")

    def __del__(self):
        """Destructor to ensure cleanup happens when project is deleted."""
        self.cleanup()
