"""
Contradiction Detection
=======================

Detects semantic, temporal, and graph-based contradictions
between memories/episodes.

Supports:
- Semantic contradiction: Embedding-based conflict detection
- Temporal contradiction: Time-based conflicts
- Graph contradiction: Conflicting edges in knowledge graph
- Trust-based conflicts: High-trust vs low-trust disagreements

Resolution strategies:
- newest_wins: Most recent episode takes precedence
- highest_trust_wins: Highest trust score wins
- flag_for_review: Mark for human review
- merge: Attempt to merge conflicting information
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Set, Tuple

import numpy as np

logger = logging.getLogger(__name__)


class ContradictionType(Enum):
    """Types of contradictions that can be detected."""
    SEMANTIC = "semantic"           # "Alice is CEO" vs "Alice is intern"
    TEMPORAL = "temporal"           # "Meeting at 2pm" vs "Meeting at 3pm"
    GRAPH_CONFLICT = "graph"        # (A, works_at, B) vs (A, works_at, C)
    TRUST_CONFLICT = "trust"        # High-trust vs low-trust disagreement
    NEGATION = "negation"           # "X is true" vs "X is not true"


class ResolutionStrategy(Enum):
    """Strategies for resolving contradictions."""
    NEWEST_WINS = "newest_wins"
    HIGHEST_TRUST_WINS = "highest_trust_wins"
    FLAG_FOR_REVIEW = "flag_for_review"
    MERGE = "merge"
    KEEP_BOTH = "keep_both"


@dataclass
class Contradiction:
    """
    Represents a detected contradiction between episodes.

    Attributes:
        contradiction_id: Unique identifier
        type: Type of contradiction
        episode_a_id: First episode ID
        episode_b_id: Second episode ID (conflicting)
        confidence: Confidence score (0-1) that this is a real contradiction
        explanation: Human-readable explanation
        details: Additional context
        detected_at: When the contradiction was detected
        resolved: Whether it has been resolved
        resolution: How it was resolved (if applicable)
    """
    contradiction_id: str
    type: ContradictionType
    episode_a_id: str
    episode_b_id: str
    confidence: float
    explanation: str
    details: Dict[str, Any] = field(default_factory=dict)
    detected_at: datetime = field(default_factory=datetime.utcnow)
    resolved: bool = False
    resolution: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "contradiction_id": self.contradiction_id,
            "type": self.type.value,
            "episode_a_id": self.episode_a_id,
            "episode_b_id": self.episode_b_id,
            "confidence": self.confidence,
            "explanation": self.explanation,
            "details": self.details,
            "detected_at": self.detected_at.isoformat(),
            "resolved": self.resolved,
            "resolution": self.resolution,
        }


@dataclass
class ContradictionResult:
    """Result of contradiction detection on an insert."""
    episode_id: str
    contradictions: List[Contradiction] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)

    @property
    def has_contradictions(self) -> bool:
        return len(self.contradictions) > 0

    def highest_confidence(self) -> Optional[Contradiction]:
        """Get the highest-confidence contradiction."""
        if not self.contradictions:
            return None
        return max(self.contradictions, key=lambda c: c.confidence)


class ContradictionDetector:
    """
    Detects contradictions between episodes.

    Uses multiple strategies:
    1. Semantic similarity with opposing content
    2. Graph edge conflicts
    3. Temporal inconsistencies
    4. Negation pattern matching
    """

    # Negation patterns for detecting semantic negations
    NEGATION_PATTERNS = [
        (r"\b(is|are|was|were)\b", r"\1 not"),
        (r"\b(is|are|was|were) not\b", r"\1"),
        (r"\b(can|will|should|would|could)\b", r"\1 not"),
        (r"\b(can|will|should|would|could) not\b", r"\1"),
        (r"\b(has|have|had)\b", r"\1 not"),
        (r"\b(has|have|had) not\b", r"\1"),
    ]

    # Antonym pairs for semantic contradiction detection
    ANTONYM_PAIRS = {
        "true": "false",
        "yes": "no",
        "alive": "dead",
        "open": "closed",
        "active": "inactive",
        "enabled": "disabled",
        "success": "failure",
        "win": "lose",
        "start": "end",
        "begin": "finish",
    }

    def __init__(
        self,
        semantic_threshold: float = 0.85,
        trust_conflict_threshold: float = 0.3,
        enable_negation_detection: bool = True,
        enable_graph_detection: bool = True,
        custom_contradiction_patterns: Optional[List[Tuple[str, str]]] = None,
    ):
        """
        Initialize the contradiction detector.

        Args:
            semantic_threshold: Similarity threshold for semantic contradiction
            trust_conflict_threshold: Trust difference to consider a conflict
            enable_negation_detection: Enable negation pattern detection
            enable_graph_detection: Enable graph edge conflict detection
            custom_contradiction_patterns: Additional regex patterns for contradictions
        """
        self.semantic_threshold = semantic_threshold
        self.trust_conflict_threshold = trust_conflict_threshold
        self.enable_negation_detection = enable_negation_detection
        self.enable_graph_detection = enable_graph_detection
        self.custom_patterns = custom_contradiction_patterns or []

        # Build antonym lookup (bidirectional)
        self._antonyms = {}
        for a, b in self.ANTONYM_PAIRS.items():
            self._antonyms[a.lower()] = b.lower()
            self._antonyms[b.lower()] = a.lower()

    def detect_contradictions(
        self,
        new_episode: Any,  # Episode type
        existing_episodes: List[Any],  # List[Episode]
        similarity_fn: Optional[Callable[[np.ndarray, np.ndarray], float]] = None,
    ) -> ContradictionResult:
        """
        Detect contradictions between a new episode and existing ones.

        Args:
            new_episode: The new episode being inserted
            existing_episodes: List of existing episodes to check against
            similarity_fn: Optional custom similarity function

        Returns:
            ContradictionResult with detected contradictions
        """
        import uuid

        contradictions: List[Contradiction] = []
        warnings: List[str] = []

        new_text = self._get_text(new_episode)
        new_embedding = getattr(new_episode, 'e', None)
        new_graph = getattr(new_episode, 'G', None)
        new_trust = getattr(new_episode, 'trust', 1.0)

        for existing in existing_episodes:
            existing_text = self._get_text(existing)
            existing_embedding = getattr(existing, 'e', None)
            existing_graph = getattr(existing, 'G', None)
            existing_trust = getattr(existing, 'trust', 1.0)

            # 1. Semantic contradiction (high similarity + negation)
            if new_embedding is not None and existing_embedding is not None:
                sim = self._compute_similarity(
                    new_embedding, existing_embedding, similarity_fn
                )

                # Check for negation patterns if texts are similar
                if sim > self.semantic_threshold and new_text and existing_text:
                    negation = self._detect_negation(new_text, existing_text)
                    if negation:
                        contradictions.append(Contradiction(
                            contradiction_id=str(uuid.uuid4()),
                            type=ContradictionType.NEGATION,
                            episode_a_id=new_episode.id,
                            episode_b_id=existing.id,
                            confidence=sim * 0.9,  # Slightly reduce for negation
                            explanation=f"Negation detected: {negation}",
                            details={
                                "similarity": sim,
                                "new_text": new_text[:200],
                                "existing_text": existing_text[:200],
                            }
                        ))

                # Check for semantic opposition (antonyms)
                if sim > 0.7 and new_text and existing_text:
                    antonym = self._detect_antonym(new_text, existing_text)
                    if antonym:
                        contradictions.append(Contradiction(
                            contradiction_id=str(uuid.uuid4()),
                            type=ContradictionType.SEMANTIC,
                            episode_a_id=new_episode.id,
                            episode_b_id=existing.id,
                            confidence=sim * 0.85,
                            explanation=f"Semantic opposition: {antonym}",
                            details={
                                "similarity": sim,
                                "antonym_pair": antonym,
                            }
                        ))

            # 2. Graph contradiction (conflicting edges)
            if self.enable_graph_detection and new_graph and existing_graph:
                graph_conflicts = self._detect_graph_conflicts(
                    new_graph, existing_graph
                )
                for conflict in graph_conflicts:
                    contradictions.append(Contradiction(
                        contradiction_id=str(uuid.uuid4()),
                        type=ContradictionType.GRAPH_CONFLICT,
                        episode_a_id=new_episode.id,
                        episode_b_id=existing.id,
                        confidence=0.95,  # High confidence for graph conflicts
                        explanation=conflict["explanation"],
                        details=conflict,
                    ))

            # 3. Trust conflict (similar content, very different trust)
            if new_embedding is not None and existing_embedding is not None:
                sim = self._compute_similarity(
                    new_embedding, existing_embedding, similarity_fn
                )
                trust_diff = abs(new_trust - existing_trust)

                if sim > self.semantic_threshold and trust_diff > self.trust_conflict_threshold:
                    contradictions.append(Contradiction(
                        contradiction_id=str(uuid.uuid4()),
                        type=ContradictionType.TRUST_CONFLICT,
                        episode_a_id=new_episode.id,
                        episode_b_id=existing.id,
                        confidence=sim * (trust_diff / 1.0),
                        explanation=f"Trust conflict: {new_trust:.2f} vs {existing_trust:.2f}",
                        details={
                            "new_trust": new_trust,
                            "existing_trust": existing_trust,
                            "similarity": sim,
                        }
                    ))

        return ContradictionResult(
            episode_id=new_episode.id,
            contradictions=contradictions,
            warnings=warnings,
        )

    def find_contradictions(
        self,
        episodes: List[Any],
        similarity_fn: Optional[Callable[[np.ndarray, np.ndarray], float]] = None,
    ) -> List[Contradiction]:
        """
        Find all contradictions in a set of episodes.

        Args:
            episodes: List of episodes to analyze
            similarity_fn: Optional custom similarity function

        Returns:
            List of all detected contradictions
        """
        all_contradictions: List[Contradiction] = []
        seen_pairs: Set[Tuple[str, str]] = set()

        for i, ep_a in enumerate(episodes):
            for ep_b in episodes[i+1:]:
                pair = tuple(sorted([ep_a.id, ep_b.id]))
                if pair in seen_pairs:
                    continue
                seen_pairs.add(pair)

                result = self.detect_contradictions(
                    ep_a, [ep_b], similarity_fn
                )
                all_contradictions.extend(result.contradictions)

        return all_contradictions

    def _get_text(self, episode: Any) -> Optional[str]:
        """Extract text content from an episode."""
        meta = getattr(episode, 'meta', {}) or {}
        return (
            meta.get('text') or
            meta.get('content') or
            meta.get('content_preview') or
            None
        )

    def _compute_similarity(
        self,
        vec_a: np.ndarray,
        vec_b: np.ndarray,
        similarity_fn: Optional[Callable] = None,
    ) -> float:
        """Compute similarity between two vectors."""
        if similarity_fn:
            return similarity_fn(vec_a, vec_b)

        # Default: cosine similarity
        norm_a = np.linalg.norm(vec_a)
        norm_b = np.linalg.norm(vec_b)
        if norm_a == 0 or norm_b == 0:
            return 0.0
        return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))

    def _detect_negation(self, text_a: str, text_b: str) -> Optional[str]:
        """Detect if one text is a negation of the other."""
        if not self.enable_negation_detection:
            return None

        text_a_lower = text_a.lower()
        text_b_lower = text_b.lower()

        for pattern, replacement in self.NEGATION_PATTERNS:
            # Check if negating text_a produces text_b
            negated = re.sub(pattern, replacement, text_a_lower)
            if self._texts_similar(negated, text_b_lower):
                return f"'{text_a[:50]}' negates '{text_b[:50]}'"

            # Check reverse
            negated = re.sub(pattern, replacement, text_b_lower)
            if self._texts_similar(negated, text_a_lower):
                return f"'{text_b[:50]}' negates '{text_a[:50]}'"

        return None

    def _detect_antonym(self, text_a: str, text_b: str) -> Optional[Tuple[str, str]]:
        """Detect if texts contain antonym pairs."""
        words_a = set(text_a.lower().split())
        words_b = set(text_b.lower().split())

        for word in words_a:
            if word in self._antonyms:
                antonym = self._antonyms[word]
                if antonym in words_b:
                    return (word, antonym)

        return None

    def _texts_similar(self, text_a: str, text_b: str, threshold: float = 0.9) -> bool:
        """Check if two texts are similar (simple word overlap)."""
        words_a = set(text_a.split())
        words_b = set(text_b.split())

        if not words_a or not words_b:
            return False

        intersection = len(words_a & words_b)
        union = len(words_a | words_b)

        return (intersection / union) > threshold if union > 0 else False

    def _detect_graph_conflicts(
        self,
        graph_a: Any,  # nx.DiGraph
        graph_b: Any,  # nx.DiGraph
    ) -> List[Dict[str, Any]]:
        """Detect conflicting edges between two graphs."""
        conflicts: List[Dict[str, Any]] = []

        # Functional relations that should be unique
        FUNCTIONAL_RELATIONS = {
            "works_at", "lives_in", "married_to", "ceo_of",
            "capital_of", "president_of", "located_in",
        }

        for u, v, data_a in graph_a.edges(data=True):
            rel_a = data_a.get("relation") or data_a.get("label") or data_a.get("type")
            if not rel_a:
                continue

            rel_a_lower = rel_a.lower()

            # Check for functional relation conflicts
            if rel_a_lower in FUNCTIONAL_RELATIONS:
                for u2, v2, data_b in graph_b.edges(data=True):
                    rel_b = data_b.get("relation") or data_b.get("label") or data_b.get("type")
                    if not rel_b:
                        continue

                    # Same subject, same relation, different object
                    if u == u2 and rel_a_lower == rel_b.lower() and v != v2:
                        conflicts.append({
                            "subject": u,
                            "relation": rel_a,
                            "object_a": v,
                            "object_b": v2,
                            "explanation": f"Conflicting relation: ({u})-[{rel_a}]->({v}) vs ({u})-[{rel_b}]->({v2})",
                        })

        return conflicts


class ContradictionResolver:
    """
    Resolves contradictions using configurable strategies.
    """

    def __init__(self, default_strategy: ResolutionStrategy = ResolutionStrategy.FLAG_FOR_REVIEW):
        """
        Initialize the resolver.

        Args:
            default_strategy: Default resolution strategy
        """
        self.default_strategy = default_strategy
        self._type_strategies: Dict[ContradictionType, ResolutionStrategy] = {}

    def set_strategy(
        self,
        contradiction_type: ContradictionType,
        strategy: ResolutionStrategy,
    ) -> None:
        """Set resolution strategy for a specific contradiction type."""
        self._type_strategies[contradiction_type] = strategy

    def resolve(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """
        Resolve a contradiction.

        Args:
            contradiction: The contradiction to resolve
            episode_a: First episode
            episode_b: Second episode

        Returns:
            Resolution result with action to take
        """
        strategy = self._type_strategies.get(
            contradiction.type, self.default_strategy
        )

        if strategy == ResolutionStrategy.NEWEST_WINS:
            return self._resolve_newest_wins(contradiction, episode_a, episode_b)
        elif strategy == ResolutionStrategy.HIGHEST_TRUST_WINS:
            return self._resolve_highest_trust(contradiction, episode_a, episode_b)
        elif strategy == ResolutionStrategy.FLAG_FOR_REVIEW:
            return self._resolve_flag(contradiction, episode_a, episode_b)
        elif strategy == ResolutionStrategy.KEEP_BOTH:
            return self._resolve_keep_both(contradiction, episode_a, episode_b)
        elif strategy == ResolutionStrategy.MERGE:
            return self._resolve_merge(contradiction, episode_a, episode_b)
        else:
            return self._resolve_flag(contradiction, episode_a, episode_b)

    def _resolve_newest_wins(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """Newer episode takes precedence."""
        time_a = getattr(episode_a, 'tocc', datetime.min)
        time_b = getattr(episode_b, 'tocc', datetime.min)

        winner = episode_a if time_a >= time_b else episode_b
        loser = episode_b if time_a >= time_b else episode_a

        return {
            "action": "supersede",
            "winner_id": winner.id,
            "loser_id": loser.id,
            "strategy": "newest_wins",
            "explanation": f"Episode {winner.id} is newer and supersedes {loser.id}",
        }

    def _resolve_highest_trust(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """Higher trust episode takes precedence."""
        trust_a = getattr(episode_a, 'trust', 0.5)
        trust_b = getattr(episode_b, 'trust', 0.5)

        winner = episode_a if trust_a >= trust_b else episode_b
        loser = episode_b if trust_a >= trust_b else episode_a

        return {
            "action": "supersede",
            "winner_id": winner.id,
            "loser_id": loser.id,
            "strategy": "highest_trust_wins",
            "explanation": f"Episode {winner.id} has higher trust ({max(trust_a, trust_b):.2f})",
        }

    def _resolve_flag(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """Flag for manual review."""
        return {
            "action": "flag",
            "episode_ids": [episode_a.id, episode_b.id],
            "strategy": "flag_for_review",
            "explanation": f"Contradiction flagged for review: {contradiction.explanation}",
        }

    def _resolve_keep_both(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """Keep both episodes but mark them as contradicting."""
        return {
            "action": "link",
            "episode_ids": [episode_a.id, episode_b.id],
            "edge_type": "contradicts",
            "strategy": "keep_both",
            "explanation": "Both episodes kept with 'contradicts' edge",
        }

    def _resolve_merge(
        self,
        contradiction: Contradiction,
        episode_a: Any,
        episode_b: Any,
    ) -> Dict[str, Any]:
        """
        Merge conflicting episodes into a unified representation.

        Creates a merged episode that combines information from both,
        preferring higher-trust or newer data where conflicts exist.
        """
        # Determine which episode has higher trust/recency for conflict resolution
        trust_a = getattr(episode_a, 'trust', 0.5)
        trust_b = getattr(episode_b, 'trust', 0.5)
        time_a = getattr(episode_a, 'tocc', datetime.min)
        time_b = getattr(episode_b, 'tocc', datetime.min)

        # Primary episode is the one with higher trust, or newer if equal trust
        if trust_a > trust_b:
            primary, secondary = episode_a, episode_b
        elif trust_b > trust_a:
            primary, secondary = episode_b, episode_a
        else:
            # Equal trust - prefer newer
            primary = episode_a if time_a >= time_b else episode_b
            secondary = episode_b if time_a >= time_b else episode_a

        # Merge metadata from both episodes (primary overwrites secondary)
        primary_meta = getattr(primary, 'meta', {}) or {}
        secondary_meta = getattr(secondary, 'meta', {}) or {}
        merged_meta = {**secondary_meta, **primary_meta}

        # Track provenance of merged data
        merged_meta['_merged_from'] = [episode_a.id, episode_b.id]
        merged_meta['_merge_primary'] = primary.id
        merged_meta['_contradiction_id'] = contradiction.contradiction_id

        return {
            "action": "merge",
            "primary_id": primary.id,
            "secondary_id": secondary.id,
            "merged_meta": merged_meta,
            "strategy": "merge",
            "explanation": (
                f"Episodes merged with {primary.id} as primary "
                f"(trust: {getattr(primary, 'trust', 0.5):.2f})"
            ),
        }
