# app/services/memory/semantic/verification.py
"""
Fact Verification.

Provides utilities for verifying facts, detecting conflicts,
and managing fact consistency.
"""

import logging
from dataclasses import dataclass, field
from typing import Any, ClassVar
from uuid import UUID

from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.memory.fact import Fact as FactModel
from app.services.memory.types import Fact

logger = logging.getLogger(__name__)


@dataclass
class VerificationResult:
    """Result of fact verification."""

    is_valid: bool
    confidence_adjustment: float = 0.0
    conflicts: list["FactConflict"] = field(default_factory=list)
    supporting_facts: list[Fact] = field(default_factory=list)
    messages: list[str] = field(default_factory=list)


@dataclass
class FactConflict:
    """Represents a conflict between two facts."""

    fact_a_id: UUID
    fact_b_id: UUID
    conflict_type: str  # "contradiction", "superseded", "partial_overlap"
    description: str
    suggested_resolution: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "fact_a_id": str(self.fact_a_id),
            "fact_b_id": str(self.fact_b_id),
            "conflict_type": self.conflict_type,
            "description": self.description,
            "suggested_resolution": self.suggested_resolution,
        }


class FactVerifier:
    """
    Verifies facts and detects conflicts.

    Provides methods to:
    - Check if a fact conflicts with existing facts
    - Find supporting evidence for a fact
    - Detect contradictions in the fact base
    """

    # Predicates that are opposites/contradictions
    CONTRADICTORY_PREDICATES: ClassVar[set[tuple[str, str]]] = {
        ("uses", "does_not_use"),
        ("requires", "does_not_require"),
        ("is_a", "is_not_a"),
        ("causes", "prevents"),
        ("allows", "prevents"),
        ("supports", "does_not_support"),
        ("best_practice", "anti_pattern"),
    }

    def __init__(self, session: AsyncSession) -> None:
        """Initialize verifier with database session."""
        self._session = session

    async def verify_fact(
        self,
        subject: str,
        predicate: str,
        obj: str,
        project_id: UUID | None = None,
    ) -> VerificationResult:
        """
        Verify a fact against existing facts.

        Args:
            subject: Fact subject
            predicate: Fact predicate
            obj: Fact object
            project_id: Optional project scope

        Returns:
            VerificationResult with verification details
        """
        result = VerificationResult(is_valid=True)

        # Check for direct contradictions
        conflicts = await self._find_contradictions(
            subject=subject,
            predicate=predicate,
            obj=obj,
            project_id=project_id,
        )
        result.conflicts = conflicts

        if conflicts:
            result.is_valid = False
            result.messages.append(f"Found {len(conflicts)} conflicting fact(s)")
            # Reduce confidence based on conflicts
            result.confidence_adjustment = -0.1 * len(conflicts)

        # Find supporting facts
        supporting = await self._find_supporting_facts(
            subject=subject,
            predicate=predicate,
            project_id=project_id,
        )
        result.supporting_facts = supporting

        if supporting:
            result.messages.append(f"Found {len(supporting)} supporting fact(s)")
            # Boost confidence based on support
            result.confidence_adjustment += 0.05 * min(len(supporting), 3)

        return result

    async def _find_contradictions(
        self,
        subject: str,
        predicate: str,
        obj: str,
        project_id: UUID | None = None,
    ) -> list[FactConflict]:
        """Find facts that contradict the given fact."""
        conflicts: list[FactConflict] = []

        # Find opposite predicates
        opposite_predicates = self._get_opposite_predicates(predicate)

        if not opposite_predicates:
            return conflicts

        # Search for contradicting facts
        query = select(FactModel).where(
            and_(
                FactModel.subject == subject,
                FactModel.predicate.in_(opposite_predicates),
            )
        )

        if project_id is not None:
            query = query.where(
                or_(
                    FactModel.project_id == project_id,
                    FactModel.project_id.is_(None),
                )
            )

        result = await self._session.execute(query)
        models = list(result.scalars().all())

        for model in models:
            conflicts.append(
                FactConflict(
                    fact_a_id=model.id,  # type: ignore[arg-type]
                    fact_b_id=UUID(
                        "00000000-0000-0000-0000-000000000000"
                    ),  # Placeholder for new fact
                    conflict_type="contradiction",
                    description=(
                        f"'{subject} {predicate} {obj}' contradicts "
                        f"'{model.subject} {model.predicate} {model.object}'"
                    ),
                    suggested_resolution="Keep fact with higher confidence",
                )
            )

        return conflicts

    def _get_opposite_predicates(self, predicate: str) -> list[str]:
        """Get predicates that are opposite to the given predicate."""
        opposites: list[str] = []

        for pair in self.CONTRADICTORY_PREDICATES:
            if predicate in pair:
                opposites.extend(p for p in pair if p != predicate)

        return opposites

    async def _find_supporting_facts(
        self,
        subject: str,
        predicate: str,
        project_id: UUID | None = None,
    ) -> list[Fact]:
        """Find facts that support the given fact."""
        # Find facts with same subject and predicate
        query = (
            select(FactModel)
            .where(
                and_(
                    FactModel.subject == subject,
                    FactModel.predicate == predicate,
                    FactModel.confidence >= 0.5,
                )
            )
            .limit(10)
        )

        if project_id is not None:
            query = query.where(
                or_(
                    FactModel.project_id == project_id,
                    FactModel.project_id.is_(None),
                )
            )

        result = await self._session.execute(query)
        models = list(result.scalars().all())

        return [self._model_to_fact(m) for m in models]

    async def find_all_conflicts(
        self,
        project_id: UUID | None = None,
    ) -> list[FactConflict]:
        """
        Find all conflicts in the fact base.

        Args:
            project_id: Optional project scope

        Returns:
            List of all detected conflicts
        """
        conflicts: list[FactConflict] = []

        # Get all facts
        query = select(FactModel)
        if project_id is not None:
            query = query.where(
                or_(
                    FactModel.project_id == project_id,
                    FactModel.project_id.is_(None),
                )
            )

        result = await self._session.execute(query)
        models = list(result.scalars().all())

        # Check each pair for conflicts
        for i, fact_a in enumerate(models):
            for fact_b in models[i + 1 :]:
                conflict = self._check_pair_conflict(fact_a, fact_b)
                if conflict:
                    conflicts.append(conflict)

        logger.info(f"Found {len(conflicts)} conflicts in fact base")

        return conflicts

    def _check_pair_conflict(
        self,
        fact_a: FactModel,
        fact_b: FactModel,
    ) -> FactConflict | None:
        """Check if two facts conflict."""
        # Same subject?
        if fact_a.subject != fact_b.subject:
            return None

        # Contradictory predicates?
        opposite = self._get_opposite_predicates(fact_a.predicate)  # type: ignore[arg-type]
        if fact_b.predicate not in opposite:
            return None

        return FactConflict(
            fact_a_id=fact_a.id,  # type: ignore[arg-type]
            fact_b_id=fact_b.id,  # type: ignore[arg-type]
            conflict_type="contradiction",
            description=(
                f"'{fact_a.subject} {fact_a.predicate} {fact_a.object}' "
                f"contradicts '{fact_b.subject} {fact_b.predicate} {fact_b.object}'"
            ),
            suggested_resolution="Deprecate fact with lower confidence",
        )

    async def get_fact_reliability_score(
        self,
        fact_id: UUID,
    ) -> float:
        """
        Calculate a reliability score for a fact.

        Based on:
        - Confidence score
        - Number of reinforcements
        - Number of supporting facts
        - Absence of conflicts

        Args:
            fact_id: Fact to score

        Returns:
            Reliability score (0.0 to 1.0)
        """
        query = select(FactModel).where(FactModel.id == fact_id)
        result = await self._session.execute(query)
        model = result.scalar_one_or_none()

        if model is None:
            return 0.0

        # Base score from confidence - explicitly typed to avoid Column type issues
        score: float = float(model.confidence)

        # Boost for reinforcements (diminishing returns)
        reinforcement_boost = min(0.2, float(model.reinforcement_count) * 0.02)
        score += reinforcement_boost

        # Find supporting facts
        supporting = await self._find_supporting_facts(
            subject=model.subject,  # type: ignore[arg-type]
            predicate=model.predicate,  # type: ignore[arg-type]
            project_id=model.project_id,  # type: ignore[arg-type]
        )
        support_boost = min(0.1, len(supporting) * 0.02)
        score += support_boost

        # Check for conflicts
        conflicts = await self._find_contradictions(
            subject=model.subject,  # type: ignore[arg-type]
            predicate=model.predicate,  # type: ignore[arg-type]
            obj=model.object,  # type: ignore[arg-type]
            project_id=model.project_id,  # type: ignore[arg-type]
        )
        conflict_penalty = min(0.3, len(conflicts) * 0.1)
        score -= conflict_penalty

        # Clamp to valid range
        return max(0.0, min(1.0, score))

    def _model_to_fact(self, model: FactModel) -> Fact:
        """Convert SQLAlchemy model to Fact dataclass."""
        return Fact(
            id=model.id,  # type: ignore[arg-type]
            project_id=model.project_id,  # type: ignore[arg-type]
            subject=model.subject,  # type: ignore[arg-type]
            predicate=model.predicate,  # type: ignore[arg-type]
            object=model.object,  # type: ignore[arg-type]
            confidence=model.confidence,  # type: ignore[arg-type]
            source_episode_ids=model.source_episode_ids or [],  # type: ignore[arg-type]
            first_learned=model.first_learned,  # type: ignore[arg-type]
            last_reinforced=model.last_reinforced,  # type: ignore[arg-type]
            reinforcement_count=model.reinforcement_count,  # type: ignore[arg-type]
            embedding=None,
            created_at=model.created_at,  # type: ignore[arg-type]
            updated_at=model.updated_at,  # type: ignore[arg-type]
        )
