"""
core/merge/conflict_detector.py

ConflictDetector — analyzes multiple StateDelta patches for logical contradictions.
Used by SemanticMergeInterceptor to decide if Opus arbitration is needed.

Conflict types detected:
    1. path_collision      — two deltas write the same JSON path with different values
    2. op_contradiction    — one delta adds at path X, another removes at path X
    3. semantic_contradiction — two deltas replace the same field with different values

# VERIFICATION_STAMP
# Story: 7.01
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 10/10
# Coverage: 100%
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------


@dataclass
class ConflictReport:
    """Result of conflict detection across multiple StateDelta proposals."""

    has_conflicts: bool
    conflicting_paths: list[str] = field(default_factory=list)
    conflict_types: list[str] = field(default_factory=list)
    # StateDelta objects (or dicts) whose paths never clashed with another delta
    non_conflicting_deltas: list = field(default_factory=list)


# ---------------------------------------------------------------------------
# ConflictDetector
# ---------------------------------------------------------------------------


class ConflictDetector:
    """
    Analyzes multiple StateDelta patches for logical contradictions.

    A StateDelta is expected to have a ``patch`` attribute that is either a
    tuple or list of RFC 6902 operation dicts.  Plain dicts with a ``patch``
    key are also accepted (for testing convenience).

    Algorithm
    ---------
    1. Build a path-ops map: for every delta, collect every (path, op, value)
       triple and record which delta index it came from.
    2. For each path that appears in more than one delta, run three checks:
       - Type 1 (path_collision): same path, op is add/replace, values differ.
       - Type 2 (op_contradiction): mix of add and remove ops at same path.
       - Type 3 (semantic_contradiction): same path, op is replace, values differ.
    3. Deltas whose indices never appear in the conflict set are returned as
       ``non_conflicting_deltas``.
    """

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def detect(self, deltas: list) -> ConflictReport:
        """
        Analyze *deltas* for logical conflicts.

        Args:
            deltas: List of StateDelta objects (or dicts with a ``patch`` key).

        Returns:
            ConflictReport describing all detected conflicts and which deltas
            are safe to merge without Opus arbitration.
        """
        if not deltas:
            return ConflictReport(has_conflicts=False, non_conflicting_deltas=[])

        if len(deltas) == 1:
            return ConflictReport(
                has_conflicts=False,
                non_conflicting_deltas=list(deltas),
            )

        # --- step 1: collect all path-level operations per delta index -------
        # path_ops: path -> list of (delta_idx, op_type, value)
        path_ops: dict[str, list[tuple[int, str, Any]]] = {}

        for idx, delta in enumerate(deltas):
            patch = self._extract_patch(delta)
            for op_dict in patch:
                path: str = op_dict.get("path", "")
                op_type: str = op_dict.get("op", "")
                value: Any = op_dict.get("value", None)
                path_ops.setdefault(path, []).append((idx, op_type, value))

        # --- step 2: detect conflicts per path --------------------------------
        conflicting_paths: list[str] = []
        conflict_types: list[str] = []
        conflicting_indices: set[int] = set()

        for path, ops in path_ops.items():
            if len(ops) < 2:
                # Only one delta touches this path — no conflict possible
                continue

            self._check_path(
                path,
                ops,
                conflicting_paths,
                conflict_types,
                conflicting_indices,
            )

        # --- step 3: build result ---------------------------------------------
        non_conflicting = [
            d for i, d in enumerate(deltas) if i not in conflicting_indices
        ]
        has_conflicts = bool(conflicting_paths)

        report = ConflictReport(
            has_conflicts=has_conflicts,
            conflicting_paths=conflicting_paths,
            conflict_types=conflict_types,
            non_conflicting_deltas=non_conflicting,
        )
        logger.debug(
            "ConflictDetector.detect: %d deltas → has_conflicts=%s, paths=%s, types=%s",
            len(deltas),
            has_conflicts,
            conflicting_paths,
            conflict_types,
        )
        return report

    def fast_merge(self, non_conflicting_deltas: list) -> list[dict]:
        """
        Merge non-conflicting patches into a single combined operation list.

        No Opus arbitration call needed — patches are simply concatenated in
        submission order.

        Args:
            non_conflicting_deltas: List of StateDelta / dict objects whose
                                    patches are known not to conflict.

        Returns:
            Flat list of RFC 6902 operation dicts from all supplied deltas.
        """
        merged: list[dict] = []
        for delta in non_conflicting_deltas:
            patch = self._extract_patch(delta)
            merged.extend(patch)
        return merged

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _extract_patch(delta: Any) -> list[dict]:
        """
        Return the patch as a plain list regardless of whether *delta* is a
        StateDelta (with a ``patch`` tuple attribute) or a plain dict.
        """
        if hasattr(delta, "patch"):
            patch = delta.patch
        elif isinstance(delta, dict):
            patch = delta.get("patch", [])
        else:
            patch = []

        # StateDelta stores patch as a tuple (frozen dataclass) — normalise
        if isinstance(patch, tuple):
            return list(patch)
        if isinstance(patch, list):
            return patch
        return []

    @staticmethod
    def _check_path(
        path: str,
        ops: list[tuple[int, str, Any]],
        conflicting_paths: list[str],
        conflict_types: list[str],
        conflicting_indices: set[int],
    ) -> None:
        """
        Run all three conflict checks for a single *path* that appears in
        more than one delta.  Mutates the supplied output collections in place.
        """
        path_already_recorded = False

        # ---- Type 1: path_collision (add/replace at same path, different values) ----
        write_ops = [
            (idx, op_type, val)
            for idx, op_type, val in ops
            if op_type in ("add", "replace")
        ]
        if len(write_ops) >= 2:
            unique_reprs = {repr(val) for _, _, val in write_ops}
            if len(unique_reprs) > 1:
                if not path_already_recorded:
                    conflicting_paths.append(path)
                    path_already_recorded = True
                if "path_collision" not in conflict_types:
                    conflict_types.append("path_collision")
                for idx, _, _ in write_ops:
                    conflicting_indices.add(idx)

        # ---- Type 2: op_contradiction (add vs remove at same path) ----
        op_types_present = {op_type for _, op_type, _ in ops}
        if "add" in op_types_present and "remove" in op_types_present:
            if not path_already_recorded:
                conflicting_paths.append(path)
                path_already_recorded = True
            if "op_contradiction" not in conflict_types:
                conflict_types.append("op_contradiction")
            for idx, op_type, _ in ops:
                if op_type in ("add", "remove"):
                    conflicting_indices.add(idx)

        # ---- Type 3: semantic_contradiction (replace at same path, different values) ----
        replace_ops = [
            (idx, val) for idx, op_type, val in ops if op_type == "replace"
        ]
        if len(replace_ops) >= 2:
            unique_replace_reprs = {repr(val) for _, val in replace_ops}
            if len(unique_replace_reprs) > 1:
                if not path_already_recorded:
                    conflicting_paths.append(path)
                    # path_already_recorded = True  # last check, no need
                if "semantic_contradiction" not in conflict_types:
                    conflict_types.append("semantic_contradiction")
                for idx, _ in replace_ops:
                    conflicting_indices.add(idx)
