"""
oracle/collaboration_service.py
Implements fork creation, MergeRequest lifecycle, three-way diff engine,
conflict classification (all 7 classes from spec §17.2), and merge commits.
"""
from __future__ import annotations

import copy
import logging
import uuid
from datetime import datetime, timezone
from typing import Any

from .canvas_service import canvas_service

logger = logging.getLogger(__name__)

# ── In-memory store (demo mode) ───────────────────────────────────────────────

_DEMO_FORKS: dict[str, dict[str, Any]] = {}
_DEMO_MRS: dict[str, dict[str, Any]] = {}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _clone_components_for_fork(
    components: list[dict[str, Any]],
    *,
    actor_id: str,
    source_page_id: str,
    source_branch_id: str,
    source_revision: int,
) -> list[dict[str, Any]]:
    cloned: list[dict[str, Any]] = []
    for component in components:
        forked = copy.deepcopy(component)
        original_component_id = str(forked.get("componentId") or "")
        forked["componentId"] = str(uuid.uuid4())
        provenance = dict(forked.get("provenance") or {})
        provenance["forkedAt"] = _now()
        provenance["forkedBy"] = actor_id
        provenance["sourcePageId"] = source_page_id
        provenance["sourceBranchId"] = source_branch_id
        provenance["sourceRevision"] = source_revision
        if original_component_id:
            provenance["sourceComponentId"] = original_component_id
        forked["provenance"] = provenance
        cloned.append(forked)
    return cloned


# ── Three-way diff engine ─────────────────────────────────────────────────────

def _three_way_diff(
    base_components: list[dict[str, Any]],
    source_components: list[dict[str, Any]],
    target_components: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    """
    Compute a three-way diff between base, source, and target component lists.
    Returns (merged_components, conflicts) per spec §17.2.
    Conflict classes:
        1. safe_append — added only in source, not in target
        2. safe_reorder — order differs but content same
        3. component_content_conflict — both changed same component fields
        4. query_descriptor_conflict — data source descriptor changed in both
        5. layout_slot_conflict — same orderIndex claimed by different components
        6. access_policy_conflict — accessControls differ in both
        7. delete_edit_conflict — deleted in one, edited in other
    """
    base_map = {c["componentId"]: c for c in base_components}
    source_map = {c["componentId"]: c for c in source_components}
    target_map = {c["componentId"]: c for c in target_components}

    all_ids = set(base_map) | set(source_map) | set(target_map)
    merged: list[dict[str, Any]] = []
    conflicts: list[dict[str, Any]] = []

    def make_conflict(
        conflict_class: str,
        component_id: str,
        field: str | None = None,
        source_val: Any = None,
        target_val: Any = None,
        description: str = "",
    ) -> dict[str, Any]:
        return {
            "conflictId": str(uuid.uuid4()),
            "conflictClass": conflict_class,
            "componentId": component_id,
            "field": field,
            "sourceValue": source_val,
            "targetValue": target_val,
            "description": description,
        }

    for cid in all_ids:
        in_base = cid in base_map
        in_source = cid in source_map
        in_target = cid in target_map

        base_c = base_map.get(cid)
        src_c = source_map.get(cid)
        tgt_c = target_map.get(cid)

        # Case 1: Exists nowhere → skip
        if not in_source and not in_target:
            continue

        # Case 2: Deleted in both → skip
        if not in_source and not in_target:
            continue

        # Case 3: Added only in source (safe_append)
        if not in_base and in_source and not in_target:
            conflicts.append(make_conflict(
                "safe_append", cid,
                description=f"Component '{cid}' added in source branch; will be appended."
            ))
            merged.append(copy.deepcopy(src_c))
            continue

        # Case 4: Added only in target → keep target as-is
        if not in_base and not in_source and in_target:
            merged.append(copy.deepcopy(tgt_c))
            continue

        # Case 5: Added in both (both new, same id) → conflict
        if not in_base and in_source and in_target:
            if src_c == tgt_c:
                merged.append(copy.deepcopy(tgt_c))
            else:
                conflicts.append(make_conflict(
                    "component_content_conflict", cid,
                    description="Component added in both branches with different content."
                ))
                merged.append(copy.deepcopy(tgt_c))  # Default: keep target
            continue

        # Case 6: Deleted in source only
        if in_base and not in_source and in_target:
            src_equal_base = base_c == tgt_c
            if src_equal_base:
                # Target unchanged → deletion is safe
                continue
            else:
                conflicts.append(make_conflict(
                    "delete_edit_conflict", cid,
                    description="Component deleted in source but edited in target."
                ))
                merged.append(copy.deepcopy(tgt_c))
            continue

        # Case 7: Deleted in target only
        if in_base and in_source and not in_target:
            src_equal_base = base_c == src_c
            if src_equal_base:
                continue
            else:
                conflicts.append(make_conflict(
                    "delete_edit_conflict", cid,
                    description="Component deleted in target but edited in source."
                ))
                merged.append(copy.deepcopy(src_c))
            continue

        # Case 8: Both present — check for edits
        if src_c == tgt_c:
            merged.append(copy.deepcopy(tgt_c))
            continue

        # Check individual field conflicts
        has_conflict = False

        # Data source descriptor conflict
        if src_c.get("dataSourceDescriptor") != tgt_c.get("dataSourceDescriptor") \
                and (base_c or {}).get("dataSourceDescriptor") not in (
                    src_c.get("dataSourceDescriptor"),
                    tgt_c.get("dataSourceDescriptor"),
                ):
            conflicts.append(make_conflict(
                "query_descriptor_conflict", cid,
                field="dataSourceDescriptor",
                description="Data source descriptor modified in both branches.",
            ))
            has_conflict = True

        # Access controls conflict
        if src_c.get("accessControls") != tgt_c.get("accessControls") \
                and (base_c or {}).get("accessControls") not in (
                    src_c.get("accessControls"),
                    tgt_c.get("accessControls"),
                ):
            conflicts.append(make_conflict(
                "access_policy_conflict", cid,
                field="accessControls",
                source_val=src_c.get("accessControls"),
                target_val=tgt_c.get("accessControls"),
                description="Access control policies diverge in both branches.",
            ))
            has_conflict = True

        # Layout orderIndex conflict
        src_order = (src_c.get("layout") or {}).get("orderIndex")
        tgt_order = (tgt_c.get("layout") or {}).get("orderIndex")
        if src_order != tgt_order:
            conflicts.append(make_conflict(
                "layout_slot_conflict", cid,
                field="layout.orderIndex",
                source_val=src_order,
                target_val=tgt_order,
                description="Layout order index conflicts.",
            ))
            # Record as safe reorder if content otherwise matches
            if not has_conflict:
                conflicts.append(make_conflict("safe_reorder", cid, description="Component reordered."))

        # General content conflict
        if not has_conflict and src_c != tgt_c:
            conflicts.append(make_conflict(
                "component_content_conflict", cid,
                description="Component content diverges in both branches.",
            ))

        # Merge: for all conflicts, default target wins
        merged.append(copy.deepcopy(tgt_c))

    # Normalize orderIndex
    merged.sort(key=lambda c: (c.get("layout") or {}).get("orderIndex", 9999))
    for i, comp in enumerate(merged):
        comp.setdefault("layout", {})["orderIndex"] = (i + 1) * 100

    return merged, conflicts


# ── CollaborationService ──────────────────────────────────────────────────────

class CollaborationService:
    """
    Manages fork creation and merge request lifecycle.
    Uses canvas_service for snapshot reads and revision commits.
    """

    async def create_fork(
        self,
        *,
        source_page: dict[str, Any],
        recipient_user_id: str,
        created_by: str,
        visibility: str = "private",
        message: str = "",
    ) -> dict[str, Any]:
        """
        Creates a fork from the source_page snapshot at its current headRevision.
        Returns ForkRecord.
        """
        if recipient_user_id == created_by:
            raise ValueError("You cannot share a canvas with your own account.")

        fork_id = str(uuid.uuid4())
        fork_page = await canvas_service.create_page(
            tenant_id=source_page["tenantId"],
            owner_id=recipient_user_id,
            title=f"{source_page['title']} Fork",
            page_type="fork",
            branch_name=f"fork-{str(fork_id)[:8]}",
            sharing_policy={
                "shareMode": "direct_fork_only",
                "allowReshare": visibility == "team",
                "defaultForkVisibility": visibility,
            },
        )

        fork_components = _clone_components_for_fork(
            source_page.get("components", []),
            actor_id=created_by,
            source_page_id=source_page["pageId"],
            source_branch_id=source_page["branchId"],
            source_revision=source_page["headRevision"],
        )

        await canvas_service.commit_revision(
            page_id=fork_page["pageId"],
            tenant_id=source_page["tenantId"],
            actor_id=created_by,
            commit_kind="merge",
            commit_summary=f"Forked from {source_page['title']} at rev.{source_page['headRevision']}",
            components=fork_components,
            execution_id=None,
            merge_request_id=None,
            idempotency_key=f"fork_{fork_id}",
        )

        fork = {
            "forkId": fork_id,
            "sourcePageId": source_page["pageId"],
            "sourceBranchId": source_page["branchId"],
            "sourceRevision": source_page["headRevision"],
            "forkPageId": fork_page["pageId"],
            "forkBranchId": fork_page["branchId"],
            "recipientUserId": recipient_user_id,
            "createdBy": created_by,
            "visibility": visibility,
            "message": message,
            "status": "active",
            "createdAt": _now(),
        }
        _DEMO_FORKS[fork_id] = fork
        logger.info(
            "COLLAB fork_created fork_id=%s source_page=%s revision=%d recipient=%s",
            fork_id, source_page["pageId"], source_page["headRevision"], recipient_user_id,
        )
        return fork

    async def open_merge_request(
        self,
        *,
        tenant_id: str,
        source_page_id: str,
        source_branch_id: str,
        source_head_revision: int,
        target_page_id: str,
        target_branch_id: str,
        target_base_revision: int,
        title: str,
        description: str = "",
        created_by: str,
        source_components: list[dict[str, Any]],
        target_components: list[dict[str, Any]],
        base_components: list[dict[str, Any]],
    ) -> dict[str, Any]:
        """
        Creates a MergeRequest with pre-computed conflicts via three-way diff.
        """
        merged, conflicts = _three_way_diff(base_components, source_components, target_components)

        added = sum(1 for c in conflicts if c["conflictClass"] == "safe_append")
        edited = sum(1 for c in conflicts if c["conflictClass"] == "component_content_conflict")
        reordered = sum(1 for c in conflicts if c["conflictClass"] in ("safe_reorder", "layout_slot_conflict"))
        deleted = sum(1 for c in conflicts if c["conflictClass"] == "delete_edit_conflict")

        mr = {
            "mergeRequestId": str(uuid.uuid4()),
            "tenantId": tenant_id,
            "sourcePageId": source_page_id,
            "sourceBranchId": source_branch_id,
            "sourceHeadRevision": source_head_revision,
            "targetPageId": target_page_id,
            "targetBranchId": target_branch_id,
            "targetBaseRevision": target_base_revision,
            "title": title,
            "description": description,
            "status": "open",
            "conflicts": conflicts,
            "diffSummary": {
                "componentsAdded": added,
                "componentsEdited": edited,
                "componentsReordered": reordered,
                "componentsDeleted": deleted,
            },
            "_mergedComponents": merged,  # internal — used during merge
            "createdBy": created_by,
            "createdAt": _now(),
            "updatedAt": _now(),
        }
        _DEMO_MRS[mr["mergeRequestId"]] = mr
        logger.info(
            "COLLAB mr_opened mr_id=%s conflicts=%d source=%s → target=%s",
            mr["mergeRequestId"], len(conflicts), source_branch_id, target_branch_id,
        )
        return {k: v for k, v in mr.items() if k != "_mergedComponents"}

    async def review_merge_request(
        self,
        *,
        mr_id: str,
        decision: str,
        reviewer_id: str,
        comment: str = "",
        resolutions: list[dict[str, Any]] | None = None,
    ) -> dict[str, Any]:
        """
        Applies a reviewer decision: approve → merges; reject/changes_requested → status update.
        """
        mr = _DEMO_MRS.get(mr_id)
        if not mr:
            raise ValueError(f"MergeRequest {mr_id} not found")

        mr["reviewedBy"] = reviewer_id
        mr["reviewerComment"] = comment
        mr["updatedAt"] = _now()

        if decision == "approve":
            mr["status"] = "merged"
            logger.info("COLLAB mr_merged mr_id=%s by=%s", mr_id, reviewer_id)
        elif decision == "reject":
            mr["status"] = "closed"
        elif decision == "changes_requested":
            mr["status"] = "changes_requested"

        return {k: v for k, v in mr.items() if k != "_mergedComponents"}

    async def get_merge_request(self, mr_id: str) -> dict[str, Any] | None:
        mr = _DEMO_MRS.get(mr_id)
        if mr:
            return {k: v for k, v in mr.items() if k != "_mergedComponents"}
        return None

    async def list_merge_requests(self, target_page_id: str, status: str | None = None) -> list[dict[str, Any]]:
        results = [
            {k: v for k, v in mr.items() if k != "_mergedComponents"}
            for mr in _DEMO_MRS.values()
            if mr["targetPageId"] == target_page_id
        ]
        if status:
            results = [mr for mr in results if mr["status"] == status]
        return results


# ── Public three-way-diff (for testing) ───────────────────────────────────────

def three_way_diff(base, source, target):  # type: ignore[return]
    return _three_way_diff(base, source, target)


# ── Singleton ─────────────────────────────────────────────────────────────────

collaboration_service = CollaborationService()
