"""
Command pattern implementation for undo/redo functionality
"""

import os
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
from pyPhotoAlbum.models import BaseLayoutElement, ImageData, PlaceholderData, TextBoxData


def _normalize_asset_path(image_path: str, asset_manager) -> str:
    """
    Convert absolute path to relative for asset manager.

    Args:
        image_path: Image path (absolute or relative)
        asset_manager: AssetManager instance

    Returns:
        Relative path suitable for asset manager
    """
    if not asset_manager or not image_path:
        return image_path

    if os.path.isabs(image_path):
        return os.path.relpath(image_path, asset_manager.project_folder)
    return image_path


def _deserialize_element(elem_data: Dict[str, Any]) -> BaseLayoutElement:
    """
    Deserialize element data into the appropriate element type.

    Args:
        elem_data: Dictionary containing serialized element data with 'type' key

    Returns:
        Deserialized element instance (ImageData, PlaceholderData, or TextBoxData)

    Raises:
        ValueError: If element type is unknown
    """
    elem_type = elem_data.get("type")

    element: BaseLayoutElement
    if elem_type == "image":
        element = ImageData()
    elif elem_type == "placeholder":
        element = PlaceholderData()
    elif elem_type == "textbox":
        element = TextBoxData()
    else:
        raise ValueError(f"Unknown element type: {elem_type}")

    element.deserialize(elem_data)
    return element


class Command(ABC):
    """Abstract base class for all commands"""

    @abstractmethod
    def execute(self):
        """Execute the command"""
        pass

    @abstractmethod
    def undo(self):
        """Undo the command"""
        pass

    @abstractmethod
    def redo(self):
        """Redo the command (default implementation calls execute)"""
        self.execute()

    @abstractmethod
    def serialize(self) -> Dict[str, Any]:
        """Serialize command to dictionary for saving"""
        pass

    @staticmethod
    @abstractmethod
    def deserialize(data: Dict[str, Any], project) -> "Command":
        """Deserialize command from dictionary"""
        pass


class AddElementCommand(Command):
    """Command for adding an element to a page"""

    def __init__(self, page_layout, element: BaseLayoutElement, asset_manager=None):
        self.page_layout = page_layout
        self.element = element
        self.executed = False
        self.asset_manager = asset_manager

        # Acquire reference to asset when command is created
        if self.asset_manager and isinstance(self.element, ImageData) and self.element.image_path:
            rel_path = _normalize_asset_path(self.element.image_path, self.asset_manager)
            self.asset_manager.acquire_reference(rel_path)

    def execute(self):
        """Add the element to the page"""
        if not self.executed:
            self.page_layout.add_element(self.element)
            self.executed = True

    def undo(self):
        """Remove the element from the page"""
        if self.executed:
            self.page_layout.remove_element(self.element)
            self.executed = False

    def redo(self):
        """Re-add the element"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {"type": "add_element", "element": self.element.serialize(), "executed": self.executed}

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "AddElementCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        # Note: page_layout will be handled by the CommandHistory deserializer
        cmd = AddElementCommand(None, element)
        cmd.executed = data.get("executed", False)
        return cmd


class DeleteElementCommand(Command):
    """Command for deleting an element from a page"""

    def __init__(self, page_layout, element: BaseLayoutElement, asset_manager=None):
        self.page_layout = page_layout
        self.element = element
        self.executed = False
        self.asset_manager = asset_manager

        # Acquire reference to asset to keep it alive while in undo history
        if self.asset_manager and isinstance(self.element, ImageData) and self.element.image_path:
            rel_path = _normalize_asset_path(self.element.image_path, self.asset_manager)
            self.asset_manager.acquire_reference(rel_path)

    def execute(self):
        """Remove the element from the page"""
        if not self.executed:
            self.page_layout.remove_element(self.element)
            self.executed = True

    def undo(self):
        """Re-add the element to the page"""
        if self.executed:
            self.page_layout.add_element(self.element)
            self.executed = False

    def redo(self):
        """Re-remove the element"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {"type": "delete_element", "element": self.element.serialize(), "executed": self.executed}

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "DeleteElementCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        cmd = DeleteElementCommand(None, element)
        cmd.executed = data.get("executed", False)
        return cmd


class MoveElementCommand(Command):
    """Command for moving an element"""

    def __init__(self, element: BaseLayoutElement, old_position: tuple, new_position: tuple):
        self.element = element
        self.old_position = old_position
        self.new_position = new_position

    def execute(self):
        """Move element to new position"""
        self.element.position = self.new_position

    def undo(self):
        """Move element back to old position"""
        self.element.position = self.old_position

    def redo(self):
        """Move element to new position again"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "move_element",
            "element": self.element.serialize(),
            "old_position": self.old_position,
            "new_position": self.new_position,
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "MoveElementCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        return MoveElementCommand(element, tuple(data["old_position"]), tuple(data["new_position"]))


class ResizeElementCommand(Command):
    """Command for resizing an element"""

    def __init__(
        self, element: BaseLayoutElement, old_position: tuple, old_size: tuple, new_position: tuple, new_size: tuple
    ):
        self.element = element
        self.old_position = old_position
        self.old_size = old_size
        self.new_position = new_position
        self.new_size = new_size

    def execute(self):
        """Resize element to new size"""
        self.element.position = self.new_position
        self.element.size = self.new_size

    def undo(self):
        """Resize element back to old size"""
        self.element.position = self.old_position
        self.element.size = self.old_size

    def redo(self):
        """Resize element to new size again"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "resize_element",
            "element": self.element.serialize(),
            "old_position": self.old_position,
            "old_size": self.old_size,
            "new_position": self.new_position,
            "new_size": self.new_size,
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "ResizeElementCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        return ResizeElementCommand(
            element,
            tuple(data["old_position"]),
            tuple(data["old_size"]),
            tuple(data["new_position"]),
            tuple(data["new_size"]),
        )


class RotateElementCommand(Command):
    """Command for rotating an element"""

    def __init__(self, element: BaseLayoutElement, old_rotation: float, new_rotation: float):
        self.element = element
        self.old_rotation = old_rotation
        self.new_rotation = new_rotation

        # Store old position, size, and PIL rotation state
        self.old_position = element.position
        self.old_size = element.size

        # For ImageData, store the old PIL rotation state
        if hasattr(element, "pil_rotation_90"):
            self.old_pil_rotation = element.pil_rotation_90
        else:
            self.old_pil_rotation = None

    def execute(self):
        """Rotate element by physically rotating the PIL image data"""
        from pyPhotoAlbum.models import ImageData

        # Calculate rotation delta
        delta = (self.new_rotation - self.old_rotation) % 360

        # For ImageData, rotate the actual PIL image
        if isinstance(self.element, ImageData):
            # Update PIL rotation counter
            if delta == 90:
                self.element.pil_rotation_90 = (self.element.pil_rotation_90 + 1) % 4
            elif delta == 270:
                self.element.pil_rotation_90 = (self.element.pil_rotation_90 + 3) % 4
            elif delta == 180:
                self.element.pil_rotation_90 = (self.element.pil_rotation_90 + 2) % 4

            # For 90° or 270° rotations, swap dimensions
            if delta == 90 or delta == 270:
                w, h = self.element.size
                x, y = self.element.position

                # Swap dimensions
                self.element.size = (h, w)

                # Adjust position to keep center in same place
                center_x = x + w / 2
                center_y = y + h / 2
                self.element.position = (center_x - h / 2, center_y - w / 2)

            # Clear the texture so it will be reloaded with the new rotation
            if hasattr(self.element, "_texture_id"):
                del self.element._texture_id
            if hasattr(self.element, "_async_load_requested"):
                self.element._async_load_requested = False

            # Keep visual rotation at 0
            self.element.rotation = 0
        else:
            # For non-image elements, use old visual rotation
            if delta == 90 or delta == 270:
                w, h = self.element.size
                x, y = self.element.position
                self.element.size = (h, w)
                center_x = x + w / 2
                center_y = y + h / 2
                self.element.position = (center_x - h / 2, center_y - w / 2)
                self.element.rotation = 0
            else:
                self.element.rotation = self.new_rotation

    def undo(self):
        """Restore element back to old state"""
        from pyPhotoAlbum.models import ImageData

        # Restore original rotation, position, and size
        self.element.rotation = self.old_rotation
        self.element.position = self.old_position
        self.element.size = self.old_size

        # For ImageData, restore PIL rotation and clear texture
        if isinstance(self.element, ImageData) and self.old_pil_rotation is not None:
            self.element.pil_rotation_90 = self.old_pil_rotation
            if hasattr(self.element, "_texture_id"):
                self.element._texture_id = None
                self.element._async_load_requested = False

    def redo(self):
        """Rotate element to new angle again"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "rotate_element",
            "element": self.element.serialize(),
            "old_rotation": self.old_rotation,
            "new_rotation": self.new_rotation,
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "RotateElementCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        return RotateElementCommand(element, data["old_rotation"], data["new_rotation"])


class AdjustImageCropCommand(Command):
    """Command for adjusting image crop/pan within frame"""

    def __init__(self, element: ImageData, old_crop_info: tuple, new_crop_info: tuple):
        self.element = element
        self.old_crop_info = old_crop_info
        self.new_crop_info = new_crop_info

    def execute(self):
        """Apply new crop info"""
        self.element.crop_info = self.new_crop_info

    def undo(self):
        """Restore old crop info"""
        self.element.crop_info = self.old_crop_info

    def redo(self):
        """Apply new crop info again"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "adjust_image_crop",
            "element": self.element.serialize(),
            "old_crop_info": self.old_crop_info,
            "new_crop_info": self.new_crop_info,
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "AdjustImageCropCommand":
        """Deserialize from dictionary"""
        elem_data = data["element"]
        element = ImageData()
        element.deserialize(elem_data)

        return AdjustImageCropCommand(element, tuple(data["old_crop_info"]), tuple(data["new_crop_info"]))


class AlignElementsCommand(Command):
    """Command for aligning multiple elements"""

    def __init__(self, changes: List[tuple]):
        """
        Args:
            changes: List of (element, old_position) tuples
        """
        self.changes = changes

    def execute(self):
        """Positions have already been set by AlignmentManager"""
        pass

    def undo(self):
        """Restore old positions"""
        for element, old_position in self.changes:
            element.position = old_position

    def redo(self):
        """Re-apply alignment (positions are stored in current state)"""
        # Store current positions and restore them
        new_positions = [(elem, elem.position) for elem, _ in self.changes]
        for element, old_position in self.changes:
            element.position = old_position
        # Then re-apply new positions
        for element, new_position in new_positions:
            element.position = new_position

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "align_elements",
            "changes": [{"element": elem.serialize(), "old_position": old_pos} for elem, old_pos in self.changes],
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "AlignElementsCommand":
        """Deserialize from dictionary"""
        changes = []
        for change_data in data.get("changes", []):
            try:
                element = _deserialize_element(change_data["element"])
                old_position = tuple(change_data["old_position"])
                changes.append((element, old_position))
            except ValueError:
                continue
        return AlignElementsCommand(changes)


class ResizeElementsCommand(Command):
    """Command for resizing multiple elements"""

    def __init__(self, changes: List[tuple]):
        """
        Args:
            changes: List of (element, old_position, old_size) tuples
        """
        self.changes = changes
        self.new_states = [(elem, elem.position, elem.size) for elem, _, _ in changes]

    def execute(self):
        """Sizes have already been set by AlignmentManager"""
        pass

    def undo(self):
        """Restore old positions and sizes"""
        for element, old_position, old_size in self.changes:
            element.position = old_position
            element.size = old_size

    def redo(self):
        """Re-apply new sizes"""
        for element, new_position, new_size in self.new_states:
            element.position = new_position
            element.size = new_size

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "resize_elements",
            "changes": [
                {"element": elem.serialize(), "old_position": old_pos, "old_size": old_size}
                for elem, old_pos, old_size in self.changes
            ],
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "ResizeElementsCommand":
        """Deserialize from dictionary"""
        changes = []
        for change_data in data.get("changes", []):
            try:
                element = _deserialize_element(change_data["element"])
                old_position = tuple(change_data["old_position"])
                old_size = tuple(change_data["old_size"])
                changes.append((element, old_position, old_size))
            except ValueError:
                continue
        return ResizeElementsCommand(changes)


class ChangeZOrderCommand(Command):
    """Command for changing element z-order (list position)"""

    def __init__(self, page_layout, element: BaseLayoutElement, old_index: int, new_index: int):
        self.page_layout = page_layout
        self.element = element
        self.old_index = old_index
        self.new_index = new_index

    def execute(self):
        """Move element to new position in list"""
        elements = self.page_layout.elements
        if self.element in elements:
            elements.remove(self.element)
            elements.insert(self.new_index, self.element)

    def undo(self):
        """Move element back to old position in list"""
        elements = self.page_layout.elements
        if self.element in elements:
            elements.remove(self.element)
            elements.insert(self.old_index, self.element)

    def redo(self):
        """Move element to new position again"""
        self.execute()

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        return {
            "type": "change_zorder",
            "element": self.element.serialize(),
            "old_index": self.old_index,
            "new_index": self.new_index,
        }

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "ChangeZOrderCommand":
        """Deserialize from dictionary"""
        element = _deserialize_element(data["element"])
        return ChangeZOrderCommand(
            None, element, data["old_index"], data["new_index"]  # page_layout will be set by CommandHistory
        )


class StateChangeCommand(Command):
    """
    Generic command for operations that change state.

    This command captures before/after snapshots of state and can restore them.
    Used by the @undoable_operation decorator.
    """

    def __init__(self, description: str, restore_func, before_state: Any, after_state: Any = None):
        """
        Args:
            description: Human-readable description of the operation
            restore_func: Function to restore state: restore_func(state)
            before_state: State before the operation
            after_state: State after the operation (captured during execute)
        """
        self.description = description
        self.restore_func = restore_func
        self.before_state = before_state
        self.after_state = after_state

    def execute(self):
        """State is already applied, just store after_state if not set"""
        # After state is captured by decorator after operation runs
        pass

    def undo(self):
        """Restore to before state"""
        self.restore_func(self.before_state)

    def redo(self):
        """Restore to after state"""
        self.restore_func(self.after_state)

    def serialize(self) -> Dict[str, Any]:
        """Serialize to dictionary"""
        # For now, state change commands are not serialized
        # This could be enhanced later if needed
        return {"type": "state_change", "description": self.description}

    @staticmethod
    def deserialize(data: Dict[str, Any], project) -> "StateChangeCommand":
        """Deserialize from dictionary"""
        # Not implemented - would need to serialize state
        raise NotImplementedError("StateChangeCommand deserialization not yet supported")


class CommandHistory:
    """Manages undo/redo command history"""

    def __init__(self, max_history: int = 100, asset_manager=None, project=None):
        self.undo_stack: List[Command] = []
        self.redo_stack: List[Command] = []
        self.max_history = max_history
        self.asset_manager = asset_manager
        self.project = project  # Reference to project for dirty flag tracking

    def execute(self, command: Command):
        """Execute a command and add it to history"""
        command.execute()

        # When clearing redo stack, release asset references
        for cmd in self.redo_stack:
            self._release_command_assets(cmd)
        self.redo_stack.clear()

        self.undo_stack.append(command)

        # Mark project as dirty
        if self.project:
            self.project.mark_dirty()

        # Limit history size - release assets from old commands
        if len(self.undo_stack) > self.max_history:
            old_cmd = self.undo_stack.pop(0)
            self._release_command_assets(old_cmd)

    def _release_command_assets(self, command: Command):
        """Release asset references held by a command"""
        if not self.asset_manager:
            return

        # Release asset references for commands that hold them
        if isinstance(command, (AddElementCommand, DeleteElementCommand)):
            if isinstance(command.element, ImageData) and command.element.image_path:
                # Convert absolute path to relative for asset manager
                asset_path = command.element.image_path
                if os.path.isabs(asset_path):
                    asset_path = os.path.relpath(asset_path, self.asset_manager.project_folder)
                self.asset_manager.release_reference(asset_path)

    def undo(self) -> bool:
        """Undo the last command"""
        if not self.can_undo():
            return False

        command = self.undo_stack.pop()
        command.undo()
        self.redo_stack.append(command)

        # Mark project as dirty
        if self.project:
            self.project.mark_dirty()

        return True

    def redo(self) -> bool:
        """Redo the last undone command"""
        if not self.can_redo():
            return False

        command = self.redo_stack.pop()
        command.redo()
        self.undo_stack.append(command)

        # Mark project as dirty
        if self.project:
            self.project.mark_dirty()

        return True

    def can_undo(self) -> bool:
        """Check if undo is available"""
        return len(self.undo_stack) > 0

    def can_redo(self) -> bool:
        """Check if redo is available"""
        return len(self.redo_stack) > 0

    def clear(self):
        """Clear all history and release asset references"""
        # Release all asset references
        for cmd in self.undo_stack:
            self._release_command_assets(cmd)
        for cmd in self.redo_stack:
            self._release_command_assets(cmd)

        self.undo_stack.clear()
        self.redo_stack.clear()

    def serialize(self) -> Dict[str, Any]:
        """Serialize history to dictionary"""
        return {
            "undo_stack": [cmd.serialize() for cmd in self.undo_stack],
            "redo_stack": [cmd.serialize() for cmd in self.redo_stack],
            "max_history": self.max_history,
        }

    def deserialize(self, data: Dict[str, Any], project):
        """Deserialize history from dictionary"""
        self.max_history = data.get("max_history", 100)

        # Deserialize undo stack
        self.undo_stack = []
        for cmd_data in data.get("undo_stack", []):
            cmd = self._deserialize_command(cmd_data, project)
            if cmd:
                # Fix up page_layout references for commands that need them
                self._fixup_page_layout(cmd, project)
                self.undo_stack.append(cmd)

        # Deserialize redo stack
        self.redo_stack = []
        for cmd_data in data.get("redo_stack", []):
            cmd = self._deserialize_command(cmd_data, project)
            if cmd:
                # Fix up page_layout references for commands that need them
                self._fixup_page_layout(cmd, project)
                self.redo_stack.append(cmd)

    def _fixup_page_layout(self, cmd: Command, project):
        """
        Fix up page_layout references after deserialization.

        Commands like AddElementCommand store page_layout as None during
        deserialization because the page_layout object doesn't exist yet.
        This method finds the correct page_layout based on the element.
        """
        # Check if command has a page_layout attribute that's None
        if not hasattr(cmd, "page_layout") or cmd.page_layout is not None:
            return

        # Try to find the page containing this element
        if hasattr(cmd, "element") and cmd.element:
            element = cmd.element
            for page in project.pages:
                if element in page.layout.elements:
                    cmd.page_layout = page.layout
                    return
            # Element not found in any page - use first page as fallback
            # This can happen for newly added elements not yet in a page
            if project.pages:
                cmd.page_layout = project.pages[0].layout

    # Command type registry for deserialization
    _COMMAND_DESERIALIZERS = {
        "add_element": AddElementCommand.deserialize,
        "delete_element": DeleteElementCommand.deserialize,
        "move_element": MoveElementCommand.deserialize,
        "resize_element": ResizeElementCommand.deserialize,
        "rotate_element": RotateElementCommand.deserialize,
        "align_elements": AlignElementsCommand.deserialize,
        "resize_elements": ResizeElementsCommand.deserialize,
        "change_zorder": ChangeZOrderCommand.deserialize,
        "adjust_image_crop": AdjustImageCropCommand.deserialize,
    }

    def _deserialize_command(self, data: Dict[str, Any], project) -> Optional[Command]:
        """Deserialize a single command using registry pattern"""
        cmd_type = data.get("type")
        if cmd_type is None:
            return None

        deserializer = self._COMMAND_DESERIALIZERS.get(cmd_type)
        if not deserializer:
            print(f"Warning: Unknown command type: {cmd_type}")
            return None

        try:
            return deserializer(data, project)
        except Exception as e:
            print(f"Error deserializing command: {e}")
            return None
