"""
Conflict Operations Mixin for ContextStore.

Provides conflict detection and resolution functionality:
- _calculate_confidence: Determine conflict confidence level
- _create_conflict: Create a conflict record
- _check_similarity: Check for duplicates/conflicts
- list_conflicts: List conflicts with filtering
- count_pending_conflicts: Count pending conflicts
- _update_conflict: Update conflict record
- get_conflict_by_id: Get single conflict
- resolve_conflict: Resolve a conflict
"""

import json
import uuid
from typing import Dict, List, Optional

from ..utils.timestamps import utc_now_iso
from .utils import parse_files
from ..constants import (
    DUPLICATE_THRESHOLD,
    CONFLICT_CEILING,
    CONFIDENCE_DISTANCE_VERY_SIMILAR,
    CONFIDENCE_DISTANCE_AMBIGUOUS,
    CONFIDENCE_MIN_SUBSTANTIAL_LENGTH,
    CONFIDENCE_HIGH_DETAIL_LENGTH,
    CONFIDENCE_LENGTH_RATIO_HIGH
)
from ..logging_config import get_logger

logger = get_logger(__name__)


class ConflictMixin:
    """
    Mixin class providing conflict detection and resolution operations.

    Requires the base class to have:
    - self.db: LanceDB connection
    - self.table: LanceDB memories table (or None)
    - self.conflicts_table: LanceDB conflicts table (or None)
    - self.conflicts_table_name: str
    - self.project_name: str
    - self.embedder: Embedder instance
    - self.add(): Method to add a memory
    - self.delete_memory(): Method to delete a memory
    - self.get_memory_by_id(): Method to get a memory
    - self.count(): Method to count memories
    """

    def _calculate_confidence(
        self,
        distance: float,
        new_snapshot: dict,
        existing_snapshot: dict
    ) -> str:
        """
        Calculate conflict confidence level for ranking/auto-resolution.

        Returns:
            "auto" - Can be auto-resolved without LLM
            "high" - High confidence, easy decision
            "medium" - Medium confidence
            "low" - Low confidence, needs careful LLM review
        """
        new_fact = new_snapshot.get("fact", "") or ""
        existing_fact = existing_snapshot.get("fact", "") or ""
        new_files = set(new_snapshot.get("files", []) or [])
        existing_files = set(existing_snapshot.get("files", []) or [])

        # Check for minimal existing content (obvious decision: keep_new)
        if not existing_fact or len(existing_fact) < 10:
            return "auto"  # Auto-resolve as keep_new - existing lacks detail

        # Calculate metrics
        file_overlap = len(new_files & existing_files)
        total_files = len(new_files | existing_files)
        length_ratio = len(new_fact) / max(len(existing_fact), 1)

        # High confidence: very similar (low distance) = clear decision
        if distance < CONFIDENCE_DISTANCE_VERY_SIMILAR:
            return "high"

        # High confidence: no file overlap + both substantial = likely keep_both
        if file_overlap == 0 and len(new_fact) > CONFIDENCE_MIN_SUBSTANTIAL_LENGTH and len(existing_fact) > CONFIDENCE_MIN_SUBSTANTIAL_LENGTH:
            return "high"

        # High confidence: NEW is much more detailed = likely keep_new
        if length_ratio > CONFIDENCE_LENGTH_RATIO_HIGH and len(new_fact) > CONFIDENCE_HIGH_DETAIL_LENGTH:
            return "high"

        # Medium confidence
        if distance < CONFIDENCE_DISTANCE_AMBIGUOUS:
            return "medium"

        # Low confidence: ambiguous cases
        return "low"

    def _create_conflict(
        self,
        new_memory: dict,
        existing_memory: dict,
        distance: float = 0.5
    ) -> str:
        """
        Create a conflict record with snapshots of both memories.

        Stores self-contained data so conflicts survive memory deletion.
        Includes distance and confidence for ranking/auto-resolution.

        Args:
            new_memory: The newly added memory data
            existing_memory: The existing memory it conflicts with
            distance: L2 distance between the memories (0.35-0.65)

        Returns:
            The conflict ID
        """
        conflict_id = f"conflict_{uuid.uuid4().hex[:12]}"
        now = utc_now_iso()

        # Parse files for both snapshots
        new_files = parse_files(new_memory.get("files"))
        existing_files = parse_files(existing_memory.get("files"))

        # Build snapshots
        new_snapshot = {
            "type": new_memory.get("type"),
            "title": new_memory.get("title"),
            "fact": new_memory.get("fact"),
            "files": new_files,
            "created_at": new_memory.get("created_at")
        }
        existing_snapshot = {
            "type": existing_memory.get("type"),
            "title": existing_memory.get("title"),
            "fact": existing_memory.get("fact"),
            "files": existing_files,
            "created_at": existing_memory.get("created_at")
        }

        # Calculate ranking metrics
        file_overlap = len(set(new_files) & set(existing_files))
        new_fact_len = len(new_snapshot.get("fact", "") or "")
        existing_fact_len = len(existing_snapshot.get("fact", "") or "")
        content_length_diff = new_fact_len - existing_fact_len

        # Calculate confidence level
        confidence = self._calculate_confidence(distance, new_snapshot, existing_snapshot)

        record = {
            "id": conflict_id,
            "status": "pending",
            "detected_at": now,
            "resolved_at": "",
            "resolution": "",

            # References (for linking back if memories still exist)
            "new_memory_id": new_memory.get("id", ""),
            "existing_memory_id": existing_memory.get("id", ""),

            # Snapshots (self-contained, survives memory deletion)
            "new_snapshot": json.dumps(new_snapshot),
            "existing_snapshot": json.dumps(existing_snapshot),

            # Ranking metadata
            "distance": distance,
            "file_overlap": file_overlap,
            "content_length_diff": content_length_diff,
            "confidence": confidence
        }

        if self.conflicts_table is None:
            self.conflicts_table = self.db.create_table(self.conflicts_table_name, [record])
        else:
            self.conflicts_table.add([record])

        logger.info(f"[Store:{self.project_name}] Created conflict {conflict_id} (confidence={confidence}, distance={distance:.3f})")
        return conflict_id

    def _check_similarity(
        self,
        embedding: list,
        dup_threshold: float = DUPLICATE_THRESHOLD,
        conflict_threshold: float = CONFLICT_CEILING
    ) -> Dict:
        """
        Check for duplicate/conflict against existing memories.

        Distance thresholds (L2):
        - <= DUPLICATE_THRESHOLD: Duplicate (reject) - near-identical
        - DUPLICATE_THRESHOLD to CONFLICT_CEILING: Conflict (flag for review)
        - > CONFLICT_CEILING: OK (add normally) - sufficiently different
        """
        if self.table is None or self.count() == 0:
            return {"status": "ok", "match": None, "distance": None}

        results = self.table.search(embedding).limit(1).to_list()
        if not results:
            return {"status": "ok", "match": None, "distance": None}

        nearest = results[0]
        distance = nearest.get("_distance", float('inf'))

        match_info = {
            "id": nearest.get("id"),
            "type": nearest.get("type"),
            "title": nearest.get("title"),
            "fact": nearest.get("fact"),
            "files": parse_files(nearest.get("files")),
            "created_at": nearest.get("created_at")
        }

        if distance <= dup_threshold:
            return {"status": "duplicate", "match": match_info, "distance": distance}
        elif distance <= conflict_threshold:
            return {"status": "conflict", "match": match_info, "distance": distance}
        else:
            return {"status": "ok", "match": match_info, "distance": distance}

    def list_conflicts(self, status: str = "pending", sort_by_priority: bool = True) -> List[Dict]:
        """
        List conflicts from the dedicated conflicts table.

        Args:
            status: "pending", "resolved", or "all"
            sort_by_priority: If True, sort by confidence (auto first) then recency

        Returns:
            List of conflict records with snapshots and ranking metadata
        """
        if self.conflicts_table is None:
            return []

        try:
            df = self.conflicts_table.to_pandas()

            # Filter by status
            if status != "all":
                df = df[df["status"] == status]

            # Priority sorting: confidence level (auto > high > medium > low), then by detected_at
            if sort_by_priority and "confidence" in df.columns:
                # Map confidence to numeric priority (lower = higher priority)
                confidence_priority = {"auto": 0, "high": 1, "medium": 2, "low": 3}
                df["_priority"] = df["confidence"].map(confidence_priority).fillna(4)
                df = df.sort_values(
                    by=["_priority", "detected_at"],
                    ascending=[True, False],
                    na_position="last"
                )
                df = df.drop(columns=["_priority"])
            else:
                # Fallback: sort by detected_at descending (newest first)
                df = df.sort_values(by="detected_at", ascending=False, na_position="last")

            results = []
            for _, r in df.iterrows():
                # Parse the snapshot JSON strings
                new_snapshot = json.loads(r["new_snapshot"]) if r.get("new_snapshot") else {}
                existing_snapshot = json.loads(r["existing_snapshot"]) if r.get("existing_snapshot") else {}

                results.append({
                    # Conflict record fields
                    "id": r.get("id"),
                    "conflict_status": r.get("status"),  # Map to old field name for template compat
                    "detected_at": r.get("detected_at"),
                    "resolved_at": r.get("resolved_at") if r.get("resolved_at") else None,
                    "conflict_resolution": r.get("resolution") if r.get("resolution") else None,

                    # References to original memories
                    "new_memory_id": r.get("new_memory_id"),
                    "existing_memory_id": r.get("existing_memory_id"),

                    # Snapshots (self-contained data)
                    "new_snapshot": new_snapshot,
                    "existing_snapshot": existing_snapshot,

                    # Ranking metadata
                    "distance": r.get("distance"),
                    "confidence": r.get("confidence", "medium"),
                    "file_overlap": r.get("file_overlap", 0),
                    "content_length_diff": r.get("content_length_diff", 0),

                    # For backward compatibility with template (map from new_snapshot)
                    "type": new_snapshot.get("type"),
                    "title": new_snapshot.get("title"),
                    "fact": new_snapshot.get("fact"),
                    "files": new_snapshot.get("files", []),
                    "created_at": new_snapshot.get("created_at"),

                    # Map existing_snapshot to conflict_with for template backward compat
                    "conflict_with": existing_snapshot if existing_snapshot else None,
                    "conflict_with_id": r.get("existing_memory_id"),
                    "conflict_detected_at": r.get("detected_at")
                })

            return results
        except Exception as e:
            logger.error(f"[Store:{self.project_name}] Error listing conflicts: {e}")
            return []

    def count_pending_conflicts(self) -> int:
        """Count pending conflicts in the conflicts table."""
        if self.conflicts_table is None:
            return 0

        try:
            df = self.conflicts_table.to_pandas()
            return int((df["status"] == "pending").sum())
        except Exception:
            return 0

    def _update_conflict(self, conflict_id: str, updates: Dict) -> bool:
        """
        Update fields on a conflict record.

        Note: LanceDB doesn't support direct updates, so we delete and re-add.
        """
        if self.conflicts_table is None:
            return False

        try:
            df = self.conflicts_table.to_pandas()
            match = df[df["id"] == conflict_id]
            if match.empty:
                return False

            record = match.iloc[0].to_dict()

            # Apply updates
            for key, value in updates.items():
                if key in record:
                    record[key] = value

            # Delete old record
            self.conflicts_table.delete(f'id = "{conflict_id}"')

            # Re-add with updates
            self.conflicts_table.add([record])
            return True
        except Exception as e:
            logger.error(f"[Store:{self.project_name}] Error updating conflict: {e}")
            return False

    def get_conflict_by_id(self, conflict_id: str) -> Optional[Dict]:
        """Get a single conflict record by ID."""
        if self.conflicts_table is None:
            return None

        try:
            df = self.conflicts_table.to_pandas()
            match = df[df["id"] == conflict_id]
            if match.empty:
                return None

            r = match.iloc[0]
            new_snapshot = json.loads(r["new_snapshot"]) if r.get("new_snapshot") else {}
            existing_snapshot = json.loads(r["existing_snapshot"]) if r.get("existing_snapshot") else {}

            return {
                "id": r.get("id"),
                "status": r.get("status"),
                "detected_at": r.get("detected_at"),
                "resolved_at": r.get("resolved_at") if r.get("resolved_at") else None,
                "resolution": r.get("resolution") if r.get("resolution") else None,
                "new_memory_id": r.get("new_memory_id"),
                "existing_memory_id": r.get("existing_memory_id"),
                "new_snapshot": new_snapshot,
                "existing_snapshot": existing_snapshot
            }
        except Exception as e:
            logger.error(f"[Store:{self.project_name}] Error getting conflict: {e}")
            return None

    def resolve_conflict(
        self,
        conflict_id: str,
        resolution: str,
        merged_fact: Optional[str] = None
    ) -> Dict:
        """
        Resolve a conflict from the conflicts table.

        Args:
            conflict_id: ID of the conflict record (e.g., "conflict_abc123")
            resolution: "keep_new" | "keep_existing" | "keep_both" | "merge"
            merged_fact: Combined fact text (required if resolution is "merge")

        Returns:
            Dict with status and details
        """
        # Get the conflict record
        conflict = self.get_conflict_by_id(conflict_id)
        if not conflict:
            return {"success": False, "error": f"Conflict not found: {conflict_id}"}

        if conflict.get("status") != "pending":
            return {"success": False, "error": "Conflict already resolved"}

        new_memory_id = conflict.get("new_memory_id")
        existing_memory_id = conflict.get("existing_memory_id")
        new_snapshot = conflict.get("new_snapshot", {})
        existing_snapshot = conflict.get("existing_snapshot", {})

        # Get actual memories (may be None if deleted)
        new_memory = self.get_memory_by_id(new_memory_id) if new_memory_id else None
        existing_memory = self.get_memory_by_id(existing_memory_id) if existing_memory_id else None

        now = utc_now_iso()

        if resolution == "keep_new":
            # Delete existing (if still exists), keep new
            if existing_memory:
                self.delete_memory(
                    existing_memory_id,
                    archive=True,
                    deletion_reason="conflict_resolution",
                    deletion_source="llm_resolve",
                    replaced_by_id=new_memory_id,
                    conflict_id=conflict_id,
                    reasoning="Kept newer memory"
                )

            # Mark conflict as resolved
            self._update_conflict(conflict_id, {
                "status": "resolved",
                "resolved_at": now,
                "resolution": "keep_new"
            })
            return {"success": True, "action": "kept_new", "deleted": existing_memory_id}

        elif resolution == "keep_existing":
            # Delete new (if still exists), keep existing
            if new_memory:
                self.delete_memory(
                    new_memory_id,
                    archive=True,
                    deletion_reason="conflict_resolution",
                    deletion_source="llm_resolve",
                    replaced_by_id=existing_memory_id,
                    conflict_id=conflict_id,
                    reasoning="Kept existing memory"
                )

            # Mark conflict as resolved
            self._update_conflict(conflict_id, {
                "status": "resolved",
                "resolved_at": now,
                "resolution": "keep_existing"
            })
            return {"success": True, "action": "kept_existing", "deleted": new_memory_id}

        elif resolution == "keep_both":
            # Keep both memories, just mark conflict resolved
            self._update_conflict(conflict_id, {
                "status": "resolved",
                "resolved_at": now,
                "resolution": "keep_both"
            })
            return {"success": True, "action": "kept_both"}

        elif resolution == "merge":
            if not merged_fact:
                return {"success": False, "error": "merged_fact required for merge resolution"}

            # Use snapshot data for title/type (self-contained, doesn't require memories to exist)
            merged_title = new_snapshot.get('title', 'Untitled')
            merged_type = new_snapshot.get("type", "discovery")

            # Keep the newest timestamp from originals
            new_created = new_snapshot.get("created_at", "")
            existing_created = existing_snapshot.get("created_at", "")
            merged_created_at = max(new_created, existing_created) if new_created and existing_created else (new_created or existing_created or None)

            # Combine files from snapshots
            new_files = new_snapshot.get("files", [])
            existing_files = existing_snapshot.get("files", [])
            merged_files = list(set(new_files + existing_files))  # Deduplicate

            # Add merged memory with preserved timestamp
            result = self.add(
                obs_type=merged_type,
                title=merged_title,
                fact=merged_fact,
                files=merged_files,
                created_at=merged_created_at
            )

            # Delete both originals (if they still exist)
            deleted = []
            merged_memory_id = result.get("id")
            if new_memory:
                self.delete_memory(
                    new_memory_id,
                    archive=True,
                    deletion_reason="conflict_resolution",
                    deletion_source="llm_resolve",
                    replaced_by_id=merged_memory_id,
                    conflict_id=conflict_id,
                    reasoning="Merged into new memory"
                )
                deleted.append(new_memory_id)
            if existing_memory:
                self.delete_memory(
                    existing_memory_id,
                    archive=True,
                    deletion_reason="conflict_resolution",
                    deletion_source="llm_resolve",
                    replaced_by_id=merged_memory_id,
                    conflict_id=conflict_id,
                    reasoning="Merged into new memory"
                )
                deleted.append(existing_memory_id)

            # Mark conflict as resolved
            self._update_conflict(conflict_id, {
                "status": "resolved",
                "resolved_at": now,
                "resolution": "merge"
            })

            return {
                "success": True,
                "action": "merged",
                "new_id": result.get("id"),
                "deleted": deleted
            }

        else:
            return {"success": False, "error": f"Unknown resolution: {resolution}"}
