"""
Merge manager for handling project merge conflicts

This module provides functionality for:
- Detecting when two projects should be merged vs. concatenated
- Finding conflicts between two project versions
- Resolving conflicts based on user input or automatic strategies
"""

import copy
from typing import Dict, Any, List, Optional, Tuple
from enum import Enum
from dataclasses import dataclass
from datetime import datetime, timezone


class ConflictType(Enum):
    """Types of merge conflicts"""

    # Page-level conflicts
    PAGE_MODIFIED_BOTH = "page_modified_both"  # Page modified in both versions
    PAGE_DELETED_ONE = "page_deleted_one"  # Page deleted in one version, modified in other
    PAGE_ADDED_BOTH = "page_added_both"  # Same page number added in both (rare)

    # Element-level conflicts
    ELEMENT_MODIFIED_BOTH = "element_modified_both"  # Element modified in both versions
    ELEMENT_DELETED_ONE = "element_deleted_one"  # Element deleted in one, modified in other

    # Project-level conflicts
    SETTINGS_MODIFIED_BOTH = "settings_modified_both"  # Project settings changed in both


class MergeStrategy(Enum):
    """Automatic merge resolution strategies"""

    LATEST_WINS = "latest_wins"  # Most recent last_modified wins
    OURS = "ours"  # Always use our version
    THEIRS = "theirs"  # Always use their version
    MANUAL = "manual"  # Require manual resolution


@dataclass
class ConflictInfo:
    """Information about a single merge conflict"""

    conflict_type: ConflictType
    page_uuid: Optional[str]  # UUID of the page (if page-level conflict)
    element_uuid: Optional[str]  # UUID of the element (if element-level conflict)
    our_version: Any  # Our version of the conflicted item
    their_version: Any  # Their version of the conflicted item
    description: str  # Human-readable description


class MergeManager:
    """Manages merge operations between two project versions"""

    def __init__(self):
        self.conflicts: List[ConflictInfo] = []

    def should_merge_projects(self, project_a_data: Dict[str, Any], project_b_data: Dict[str, Any]) -> bool:
        """
        Determine if two projects should be merged or concatenated.

        Projects with the same project_id should be merged (conflict resolution).
        Projects with different project_ids should be concatenated (combine content).

        Args:
            project_a_data: First project's serialized data
            project_b_data: Second project's serialized data

        Returns:
            True if projects should be merged, False if concatenated
        """
        project_a_id = project_a_data.get("project_id")
        project_b_id = project_b_data.get("project_id")

        # If either project lacks a project_id (v2.0 or earlier), assume different projects
        if not project_a_id or not project_b_id:
            print("MergeManager: One or both projects lack project_id, assuming concatenation")
            return False

        return bool(project_a_id == project_b_id)

    def detect_conflicts(
        self, our_project_data: Dict[str, Any], their_project_data: Dict[str, Any]
    ) -> List[ConflictInfo]:
        """
        Detect conflicts between two versions of the same project.

        Args:
            our_project_data: Our version of the project (serialized)
            their_project_data: Their version of the project (serialized)

        Returns:
            List of conflicts found
        """
        self.conflicts = []

        # Detect project-level conflicts
        self._detect_project_settings_conflicts(our_project_data, their_project_data)

        # Detect page-level conflicts
        self._detect_page_conflicts(our_project_data, their_project_data)

        return self.conflicts

    def _detect_project_settings_conflicts(self, our_data: Dict[str, Any], their_data: Dict[str, Any]):
        """Detect conflicts in project-level settings."""
        # Settings that can conflict
        settings_keys = [
            "name",
            "page_size_mm",
            "working_dpi",
            "export_dpi",
            "has_cover",
            "paper_thickness_mm",
            "cover_bleed_mm",
            "binding_type",
        ]

        our_modified = our_data.get("last_modified")
        their_modified = their_data.get("last_modified")

        for key in settings_keys:
            our_value = our_data.get(key)
            their_value = their_data.get(key)

            # If values differ, it's a conflict
            if our_value != their_value:
                self.conflicts.append(
                    ConflictInfo(
                        conflict_type=ConflictType.SETTINGS_MODIFIED_BOTH,
                        page_uuid=None,
                        element_uuid=None,
                        our_version={key: our_value, "last_modified": our_modified},
                        their_version={key: their_value, "last_modified": their_modified},
                        description=f"Project setting '{key}' modified in both versions",
                    )
                )

    def _detect_page_conflicts(self, our_data: Dict[str, Any], their_data: Dict[str, Any]):
        """Detect conflicts at page level."""
        our_pages = {page["uuid"]: page for page in our_data.get("pages", [])}
        their_pages = {page["uuid"]: page for page in their_data.get("pages", [])}

        # Check all pages that exist in our version
        for page_uuid, our_page in our_pages.items():
            their_page = their_pages.get(page_uuid)

            if their_page is None:
                # Page exists in ours but not theirs - check if deleted
                if our_page.get("deleted"):
                    continue  # Both deleted, no conflict
                # We have it, they don't (might have deleted it)
                # This could be a conflict if we modified it after they deleted it
                continue

            # Page exists in both - check for modifications
            self._detect_page_modification_conflicts(page_uuid, our_page, their_page)

        # Check for pages that exist only in their version
        for page_uuid, their_page in their_pages.items():
            if page_uuid not in our_pages:
                # They have a page we don't - this is fine, add it
                # Unless we deleted it
                pass

    def _detect_page_modification_conflicts(self, page_uuid: str, our_page: Dict[str, Any], their_page: Dict[str, Any]):
        """Detect conflicts in a specific page."""
        our_modified = our_page.get("last_modified")
        their_modified = their_page.get("last_modified")

        # Check if both deleted
        if our_page.get("deleted") and their_page.get("deleted"):
            return  # No conflict

        # Check if one deleted, one modified
        if our_page.get("deleted") != their_page.get("deleted"):
            self.conflicts.append(
                ConflictInfo(
                    conflict_type=ConflictType.PAGE_DELETED_ONE,
                    page_uuid=page_uuid,
                    element_uuid=None,
                    our_version=our_page,
                    their_version=their_page,
                    description=f"Page deleted in one version but modified in the other",
                )
            )
            return

        # Check page-level properties
        page_props = ["page_number", "is_cover", "is_double_spread"]
        page_modified = False
        for prop in page_props:
            if our_page.get(prop) != their_page.get(prop):
                page_modified = True
                break

        # Only flag as conflict if properties differ AND timestamps are identical
        # (See element conflict detection for detailed explanation of this strategy)
        if page_modified and our_modified == their_modified:
            self.conflicts.append(
                ConflictInfo(
                    conflict_type=ConflictType.PAGE_MODIFIED_BOTH,
                    page_uuid=page_uuid,
                    element_uuid=None,
                    our_version=our_page,
                    their_version=their_page,
                    description=f"Page properties modified with same timestamp (possible conflict)",
                )
            )

        # Check element-level conflicts
        self._detect_element_conflicts(page_uuid, our_page, their_page)

    def _detect_element_conflicts(self, page_uuid: str, our_page: Dict[str, Any], their_page: Dict[str, Any]):
        """Detect conflicts in elements within a page."""
        our_layout = our_page.get("layout", {})
        their_layout = their_page.get("layout", {})

        our_elements = {elem["uuid"]: elem for elem in our_layout.get("elements", [])}
        their_elements = {elem["uuid"]: elem for elem in their_layout.get("elements", [])}

        # Check all elements in our version
        for elem_uuid, our_elem in our_elements.items():
            their_elem = their_elements.get(elem_uuid)

            if their_elem is None:
                # Element exists in ours but not theirs
                if our_elem.get("deleted"):
                    continue  # Both deleted, no conflict
                # We have it, they don't
                continue

            # Element exists in both - check for modifications
            self._detect_element_modification_conflicts(page_uuid, elem_uuid, our_elem, their_elem)

    def _detect_element_modification_conflicts(
        self, page_uuid: str, elem_uuid: str, our_elem: Dict[str, Any], their_elem: Dict[str, Any]
    ):
        """Detect conflicts in a specific element."""
        our_modified = our_elem.get("last_modified")
        their_modified = their_elem.get("last_modified")

        # Check if both deleted
        if our_elem.get("deleted") and their_elem.get("deleted"):
            return  # No conflict

        # Check if one deleted, one modified
        if our_elem.get("deleted") != their_elem.get("deleted"):
            self.conflicts.append(
                ConflictInfo(
                    conflict_type=ConflictType.ELEMENT_DELETED_ONE,
                    page_uuid=page_uuid,
                    element_uuid=elem_uuid,
                    our_version=our_elem,
                    their_version=their_elem,
                    description=f"Element deleted in one version but modified in the other",
                )
            )
            return

        # Check element properties
        elem_props = ["position", "size", "rotation", "z_index"]

        # Add type-specific properties
        elem_type = our_elem.get("type")
        if elem_type == "image":
            elem_props.extend(["image_path", "crop_info", "pil_rotation_90"])
        elif elem_type == "textbox":
            elem_props.extend(["text_content", "font_settings", "alignment"])

        # Check if any properties differ
        props_modified = False
        for prop in elem_props:
            if our_elem.get(prop) != their_elem.get(prop):
                props_modified = True
                break

        # Without a 3-way merge (base version), we cannot reliably detect if BOTH versions
        # modified an element vs only ONE version modifying it.
        #
        # Strategy: Only flag as conflict when we have strong evidence of concurrent modification:
        # - Properties differ AND timestamps are identical → suspicious, possible conflict
        # - Properties differ AND timestamps differ → one version modified it, auto-merge by timestamp
        #
        # If timestamps differ, _merge_non_conflicting_changes will handle it by using the newer version.
        if props_modified and our_modified == their_modified:
            # Properties differ but timestamps match - this is unusual and might indicate
            # that both versions modified it at exactly the same time, or there's data corruption.
            # Flag as conflict to be safe.
            self.conflicts.append(
                ConflictInfo(
                    conflict_type=ConflictType.ELEMENT_MODIFIED_BOTH,
                    page_uuid=page_uuid,
                    element_uuid=elem_uuid,
                    our_version=our_elem,
                    their_version=their_elem,
                    description=f"Element modified with same timestamp (possible conflict)",
                )
            )

        # Note: If timestamps differ, we assume one version modified it and the other didn't.
        # The _merge_non_conflicting_changes method will automatically use the newer version.

    def auto_resolve_conflicts(self, strategy: MergeStrategy = MergeStrategy.LATEST_WINS) -> Dict[int, str]:
        """
        Automatically resolve conflicts based on a strategy.

        Args:
            strategy: The resolution strategy to use

        Returns:
            Dictionary mapping conflict index to resolution choice ("ours" or "theirs")
        """
        resolutions = {}

        for i, conflict in enumerate(self.conflicts):
            if strategy == MergeStrategy.LATEST_WINS:
                # Compare timestamps
                our_modified = self._get_timestamp(conflict.our_version)
                their_modified = self._get_timestamp(conflict.their_version)

                if our_modified and their_modified:
                    resolutions[i] = "ours" if our_modified >= their_modified else "theirs"
                else:
                    resolutions[i] = "ours"  # Default to ours if timestamps missing

            elif strategy == MergeStrategy.OURS:
                resolutions[i] = "ours"

            elif strategy == MergeStrategy.THEIRS:
                resolutions[i] = "theirs"

            # MANUAL strategy leaves resolutions empty

        return resolutions

    def _get_timestamp(self, version_data: Any) -> Optional[str]:
        """Extract timestamp from version data."""
        if isinstance(version_data, dict):
            return version_data.get("last_modified")
        return None

    def apply_resolutions(
        self, our_project_data: Dict[str, Any], their_project_data: Dict[str, Any], resolutions: Dict[int, str]
    ) -> Dict[str, Any]:
        """
        Apply conflict resolutions to create merged project.

        Args:
            our_project_data: Our version of the project
            their_project_data: Their version of the project
            resolutions: Dictionary mapping conflict index to choice ("ours" or "theirs")

        Returns:
            Merged project data
        """
        # Start with a copy of our project
        merged_data = copy.deepcopy(our_project_data)

        # Apply resolutions
        for conflict_idx, choice in resolutions.items():
            if conflict_idx >= len(self.conflicts):
                continue

            conflict = self.conflicts[conflict_idx]

            if choice == "theirs":
                # Apply their version
                self._apply_their_version(merged_data, conflict)
            # If choice is "ours", no need to do anything

        # Add pages/elements from their version that we don't have
        self._merge_non_conflicting_changes(merged_data, their_project_data)

        return merged_data

    def _apply_their_version(self, merged_data: Dict[str, Any], conflict: ConflictInfo):
        """Apply their version for a specific conflict."""
        if conflict.conflict_type == ConflictType.SETTINGS_MODIFIED_BOTH:
            # Update project setting
            for key, value in conflict.their_version.items():
                if key != "last_modified":
                    merged_data[key] = value

        elif conflict.conflict_type in [ConflictType.PAGE_MODIFIED_BOTH, ConflictType.PAGE_DELETED_ONE]:
            # Replace entire page
            for i, page in enumerate(merged_data.get("pages", [])):
                if page.get("uuid") == conflict.page_uuid:
                    merged_data["pages"][i] = conflict.their_version
                    break

        elif conflict.conflict_type in [ConflictType.ELEMENT_MODIFIED_BOTH, ConflictType.ELEMENT_DELETED_ONE]:
            # Replace element within page
            for page in merged_data.get("pages", []):
                if page.get("uuid") == conflict.page_uuid:
                    layout = page.get("layout", {})
                    for i, elem in enumerate(layout.get("elements", [])):
                        if elem.get("uuid") == conflict.element_uuid:
                            layout["elements"][i] = conflict.their_version
                            break
                    break

    def _merge_non_conflicting_changes(self, merged_data: Dict[str, Any], their_data: Dict[str, Any]):
        """Add non-conflicting pages and elements from their version."""
        self._add_missing_pages(merged_data, their_data)
        self._merge_page_elements(merged_data, their_data)

    def _add_missing_pages(self, merged_data: Dict[str, Any], their_data: Dict[str, Any]):
        """Add pages that exist only in their version."""
        our_page_uuids = {page["uuid"] for page in merged_data.get("pages", [])}

        for their_page in their_data.get("pages", []):
            if their_page["uuid"] not in our_page_uuids:
                merged_data["pages"].append(their_page)

    def _merge_page_elements(self, merged_data: Dict[str, Any], their_data: Dict[str, Any]):
        """For pages that exist in both versions, merge their elements."""
        their_pages = {page["uuid"]: page for page in their_data.get("pages", [])}

        for our_page in merged_data.get("pages", []):
            their_page = their_pages.get(our_page["uuid"])
            if not their_page:
                continue

            our_elements = {elem["uuid"]: elem for elem in our_page.get("layout", {}).get("elements", [])}

            for their_elem in their_page.get("layout", {}).get("elements", []):
                self._merge_element(
                    our_page=our_page, page_uuid=our_page["uuid"], their_elem=their_elem, our_elements=our_elements
                )

    def _merge_element(
        self, our_page: Dict[str, Any], page_uuid: str, their_elem: Dict[str, Any], our_elements: Dict[str, Any]
    ):
        """Merge a single element from their version into our page."""
        elem_uuid = their_elem["uuid"]

        # Add new elements that we don't have
        if elem_uuid not in our_elements:
            our_page["layout"]["elements"].append(their_elem)
            return

        # Element exists in both - check if already resolved as conflict
        if self._is_element_in_conflict(elem_uuid, page_uuid):
            return

        # No conflict - use the more recently modified version
        self._merge_by_timestamp(our_page, elem_uuid, their_elem, our_elements[elem_uuid])

    def _is_element_in_conflict(self, elem_uuid: str, page_uuid: str) -> bool:
        """Check if element was part of a conflict that was already resolved."""
        return any(c.element_uuid == elem_uuid and c.page_uuid == page_uuid for c in self.conflicts)

    def _merge_by_timestamp(
        self, our_page: Dict[str, Any], elem_uuid: str, their_elem: Dict[str, Any], our_elem: Dict[str, Any]
    ):
        """Use the more recently modified version of an element."""
        our_modified = our_elem.get("last_modified")
        their_modified = their_elem.get("last_modified")

        # Their version is newer
        if not their_modified or (our_modified and their_modified <= our_modified):
            return

        # Replace with their newer version
        for i, elem in enumerate(our_page["layout"]["elements"]):
            if elem["uuid"] == elem_uuid:
                our_page["layout"]["elements"][i] = their_elem
                break


def concatenate_projects(project_a_data: Dict[str, Any], project_b_data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Concatenate two projects with different project_ids.

    This combines the pages from both projects into a single project.

    Args:
        project_a_data: First project data
        project_b_data: Second project data

    Returns:
        Combined project data
    """
    # Start with project A as base
    merged_data = copy.deepcopy(project_a_data)

    # Append all pages from project B
    merged_data["pages"].extend(copy.deepcopy(project_b_data.get("pages", [])))

    # Update project name to indicate merge
    merged_data["name"] = f"{project_a_data.get('name', 'Untitled')} + {project_b_data.get('name', 'Untitled')}"

    # Keep project A's ID and settings
    # Update last_modified to now
    merged_data["last_modified"] = datetime.now(timezone.utc).isoformat()

    print(
        f"Concatenated projects: {len(project_a_data.get('pages', []))} + {len(project_b_data.get('pages', []))} = {len(merged_data['pages'])} pages"
    )

    return merged_data
