"""
cortex/core/reconciliation/actions.py — Action application methods.

Mixin class providing _apply_action, _supersede_claim, and _delete_claim
methods for ReconciliationEngine.

Extracted from the monolithic reconciliation.py (#946).
"""

from __future__ import annotations

import logging
from typing import Optional, TYPE_CHECKING

from cortex.core.contradiction_store import insert_contradiction as _persist_contradiction
from cortex.core.reconciliation.models import _now
from cortex.core.reconciliation.models import (
    ReconciliationAction,
    ReconciliationResult,
    RelationshipKind,
)
from cortex.core.reconciliation.storage_helpers import StorageHelpersMixin

if TYPE_CHECKING:
    from cortex.storage.adapter import SemanticClaim

logger = logging.getLogger(__name__)


class ActionsMixin(StorageHelpersMixin):
    """Mixin providing action application methods for ReconciliationEngine."""

    # ------------------------------------------------------------------
    # Real-time supersession (#514)
    # ------------------------------------------------------------------

    async def _supersede_claim(
        self,
        candidate: "CandidateClaim",
        existing: "SemanticClaim",
        session_id: str,
        owner_id: str,
        subject_entity_id: Optional[str],
        reason: str = "real-time supersession during ingestion",
    ) -> ReconciliationResult:
        """Supersede an existing active claim with the incoming candidate.

        Used when the extraction pipeline signals ``action=UPDATE`` on a
        candidate, indicating an authoritative in-session override that should
        take effect immediately without waiting for dream-cycle reconciliation
        or spending an LLM classification call.
        """
        # Anti-compression guard: don't lose rich detail for terse updates.
        ok = await self._check_anti_compression(candidate, existing)
        if not ok:
            logger.info(
                "Real-time supersession blocked by anti-compression guard for %s/%s",
                candidate.subject, candidate.predicate,
            )
            return ReconciliationResult(
                candidate_subject=candidate.subject,
                candidate_predicate=candidate.predicate,
                candidate_value=candidate.object_value,
                action=ReconciliationAction.NOOP,
                existing_claim_id=existing.id,
                relationship=RelationshipKind.CONTRADICTION,
                confidence=candidate.confidence,
                compression_blocked=True,
                notes="real-time supersession blocked by anti-compression safeguard",
            )

        # Mark old claim as superseded.
        await self.storage.update_claim(
            existing.id, status="superseded", valid_to=_now(), updated_at=_now(),
        )

        # Changelog: record the supersession on the old claim.
        try:
            history = await self.storage.get_changelog(existing.id)
            next_ver = len(history) + 1
            await self.storage.log_changelog(
                claim_id=existing.id, version=next_ver,
                content_snapshot=existing.object_value, action="superseded",
                reason=reason, changed_by="reconciliation",
                diff_summary=f"'{existing.object_value}' → '{candidate.object_value}'",
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("Failed to log changelog for superseded claim %s: %s", existing.id, exc)

        # Create the new (active) claim.
        new_claim = await self._add_claim(
            candidate=candidate, session_id=session_id, owner_id=owner_id,
            subject_entity_id=subject_entity_id, supersedes_id=existing.id,
        )

        # Set superseded_by FK on the old claim — closes the bidirectional chain.
        try:
            await self.storage.update_claim(existing.id, superseded_by=new_claim.id)
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "Failed to set superseded_by on claim %s → %s: %s",
                existing.id, new_claim.id, exc,
            )

        # Audit trail — in-memory + DB persistence.
        self.contradiction_tracker.record(
            claim_id=new_claim.id, contradicts_id=existing.id,
            resolution="superseded", reason=reason,
        )
        try:
            await _persist_contradiction(
                self.storage, claim_a_id=new_claim.id, claim_b_id=existing.id,
                owner_id=owner_id, reason=reason, resolution="kept_a",
            )
        except Exception as _exc:  # noqa: BLE001
            logger.warning("Failed to persist contradiction to DB: %s", _exc)

        logger.info(
            "Real-time supersession: claim %s superseded by %s (subject=%r predicate=%r)",
            existing.id[:8], new_claim.id[:8], candidate.subject, candidate.predicate,
        )

        return ReconciliationResult(
            candidate_subject=candidate.subject,
            candidate_predicate=candidate.predicate,
            candidate_value=candidate.object_value,
            action=ReconciliationAction.SUPERSEDE,
            existing_claim_id=existing.id,
            resulting_claim_id=new_claim.id,
            relationship=RelationshipKind.CONTRADICTION,
            confidence=candidate.confidence,
            used_llm=False,
            notes=f"real-time supersession: superseded claim {existing.id} ({reason})",
        )

    # ------------------------------------------------------------------
    # Deletion (#583)
    # ------------------------------------------------------------------

    async def _delete_claim(
        self,
        candidate: "CandidateClaim",
        existing: "SemanticClaim",
        reason: str = "action=DELETE: LLM-signalled deletion during ingestion",
    ) -> ReconciliationResult:
        """Deactivate an existing claim when the extractor signals action=DELETE."""
        await self.storage.update_claim(
            existing.id,
            status="deleted",
            is_deleted=1,
            deleted_at=_now(),
            updated_at=_now(),
        )

        try:
            history = await self.storage.get_changelog(existing.id)
            next_ver = len(history) + 1
            await self.storage.log_changelog(
                claim_id=existing.id, version=next_ver,
                content_snapshot=existing.object_value, action="deleted",
                reason=reason, changed_by="reconciliation",
                diff_summary=f"Deleted: '{existing.object_value}'",
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("Failed to log changelog for deleted claim %s: %s", existing.id, exc)

        logger.info(
            "Real-time deletion: claim %s deleted (subject=%r predicate=%r reason=%s)",
            existing.id[:8], candidate.subject, candidate.predicate, reason,
        )

        return ReconciliationResult(
            candidate_subject=candidate.subject,
            candidate_predicate=candidate.predicate,
            candidate_value=candidate.object_value,
            action=ReconciliationAction.DELETE,
            existing_claim_id=existing.id,
            relationship=RelationshipKind.CONTRADICTION,
            confidence=candidate.confidence,
            used_llm=False,
            notes=f"real-time deletion: deleted claim {existing.id} ({reason})",
        )

    # ------------------------------------------------------------------
    # Action application
    # ------------------------------------------------------------------

    async def _apply_action(
        self,
        candidate: "CandidateClaim",
        existing: "SemanticClaim",
        relationship: RelationshipKind,
        session_id: str,
        owner_id: str,
        subject_entity_id: Optional[str],
        used_llm: bool,
        relationship_metadata: Optional[dict] = None,
    ) -> ReconciliationResult:
        """Map relationship → action and write to storage."""
        base = ReconciliationResult(
            candidate_subject=candidate.subject,
            candidate_predicate=candidate.predicate,
            candidate_value=candidate.object_value,
            relationship=relationship,
            existing_claim_id=existing.id,
            confidence=candidate.confidence,
            used_llm=used_llm,
            relationship_metadata=relationship_metadata,
        )

        # EQUIVALENT → NOOP
        if relationship == RelationshipKind.EQUIVALENT:
            base.action = ReconciliationAction.NOOP
            base.notes = "equivalent claim already exists"
            try:
                if hasattr(self.storage, "increment_claim_mentions"):
                    await self.storage.increment_claim_mentions(existing.id, owner_id=owner_id)
                else:
                    new_mentions = getattr(existing, "mentions", 1) + 1
                    await self.storage.update_claim(
                        existing.id, owner_id=owner_id,
                        mentions=new_mentions,
                        last_mentioned_at=_now(),
                    )
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to bump mentions for equivalent claim %s: %s", existing.id, exc)
            return base

        # COMPLEMENTARY / ELABORATES → UPDATE (merge values)
        if relationship in (RelationshipKind.COMPLEMENTARY, RelationshipKind.ELABORATES):
            ok = await self._check_anti_compression(candidate, existing)
            if not ok:
                base.action = ReconciliationAction.NOOP
                base.compression_blocked = True
                base.notes = "update blocked by anti-compression safeguard"
                return base

            merged_value = f"{existing.object_value}; {candidate.object_value}"
            extra_notes = ""
            if relationship == RelationshipKind.ELABORATES and relationship_metadata:
                detail = relationship_metadata.get("detail_added", "")
                if detail:
                    extra_notes = f" [elaborates: {detail}]"

            updated = await self.storage.update_claim(
                existing.id,
                object_value=merged_value,
                confidence=max(existing.confidence, candidate.confidence),
                updated_at=_now(),
            )
            await self._record_provenance(updated.id, session_id, candidate)

            # Changelog: version N+1 for the updated claim
            try:
                history = await self.storage.get_changelog(existing.id)
                next_ver = len(history) + 1
                diff_summary = (
                    f"merged '{existing.object_value[:100]}' + '{candidate.object_value[:100]}'"
                )
                await self.storage.log_changelog(
                    claim_id=updated.id,
                    version=next_ver,
                    content_snapshot=merged_value,
                    action="updated",
                    reason="reconciliation merge",
                    changed_by="reconciliation",
                    diff_summary=diff_summary,
                )
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to log changelog for updated claim %s: %s", existing.id, exc)

            base.action = ReconciliationAction.UPDATE
            base.resulting_claim_id = updated.id
            if extra_notes:
                base.notes = extra_notes.strip()
            else:
                rel_label = relationship.value.lower()
                base.notes = f"merged existing claim {existing.id} ({rel_label})"
            return base

        # CONTRADICTION / CONTRADICTS → SUPERSEDE
        if relationship in (RelationshipKind.CONTRADICTION, RelationshipKind.CONTRADICTS):
            ok = await self._check_anti_compression(candidate, existing)
            if not ok:
                base.action = ReconciliationAction.NOOP
                base.compression_blocked = True
                base.notes = "supersede blocked by anti-compression safeguard"
                return base

            await self.storage.update_claim(
                existing.id,
                status="superseded",
                valid_to=_now(),
                updated_at=_now(),
            )

            try:
                history = await self.storage.get_changelog(existing.id)
                next_ver = len(history) + 1
                await self.storage.log_changelog(
                    claim_id=existing.id,
                    version=next_ver,
                    content_snapshot=existing.object_value,
                    action="superseded",
                    reason="superseded by contradicting claim",
                    changed_by="reconciliation",
                    diff_summary=None,
                )
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to log changelog for superseded claim %s: %s", existing.id, exc)

            conflict_note = ""
            if relationship == RelationshipKind.CONTRADICTS and relationship_metadata:
                reason = relationship_metadata.get("conflict_reason", "")
                if reason:
                    conflict_note = f" [conflict: {reason}]"

            new_claim = await self._add_claim(
                candidate=candidate,
                session_id=session_id,
                owner_id=owner_id,
                subject_entity_id=subject_entity_id,
                supersedes_id=existing.id,
                relationship_metadata=relationship_metadata,
                valid_from=_now(),
            )

            # Set superseded_by FK — closes bidirectional chain (#514)
            try:
                await self.storage.update_claim(
                    existing.id,
                    superseded_by=new_claim.id,
                )
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "Failed to set superseded_by on claim %s → %s: %s",
                    existing.id, new_claim.id, exc,
                )

            base.action = ReconciliationAction.SUPERSEDE
            base.resulting_claim_id = new_claim.id
            base.notes = f"superseded claim {existing.id}{conflict_note}"
            _contradiction_reason = "contradiction"
            if getattr(candidate, "is_user_explicit", False):
                _contradiction_reason = "recency: explicit user statement supersedes older inference"
            elif conflict_note:
                _contradiction_reason = conflict_note.strip(" []")
            self.contradiction_tracker.record(
                claim_id=new_claim.id,
                contradicts_id=existing.id,
                resolution="superseded",
                reason=_contradiction_reason,
            )
            try:
                await _persist_contradiction(
                    self.storage,
                    claim_a_id=new_claim.id,
                    claim_b_id=existing.id,
                    owner_id=owner_id,
                    reason=_contradiction_reason,
                    resolution="kept_a",
                )
            except Exception as _exc:  # noqa: BLE001
                logger.warning("Failed to persist contradiction to DB: %s", _exc)
            return base

        # TEMPORAL_UPDATE → SUPERSEDE with temporal metadata
        if relationship == RelationshipKind.TEMPORAL_UPDATE:
            ok = await self._check_anti_compression(candidate, existing)
            if not ok:
                base.action = ReconciliationAction.NOOP
                base.compression_blocked = True
                base.notes = "supersede blocked by anti-compression safeguard"
                return base

            await self.storage.update_claim(
                existing.id,
                status="superseded",
                valid_to=_now(),
                updated_at=_now(),
            )

            try:
                history = await self.storage.get_changelog(existing.id)
                next_ver = len(history) + 1
                await self.storage.log_changelog(
                    claim_id=existing.id,
                    version=next_ver,
                    content_snapshot=existing.object_value,
                    action="superseded",
                    reason="temporal update — replaced by newer value",
                    changed_by="reconciliation",
                    diff_summary=f"'{existing.object_value}' → '{candidate.object_value}'",
                )
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to log changelog for temporal supersede %s: %s", existing.id, exc)

            temporal_meta = dict(relationship_metadata or {})
            if "previous_value" not in temporal_meta:
                temporal_meta["previous_value"] = existing.object_value
            if "new_value" not in temporal_meta:
                temporal_meta["new_value"] = candidate.object_value

            new_claim = await self._add_claim(
                candidate=candidate,
                session_id=session_id,
                owner_id=owner_id,
                subject_entity_id=subject_entity_id,
                supersedes_id=existing.id,
                relationship_metadata=temporal_meta,
            )

            try:
                await self.storage.update_claim(existing.id, superseded_by=new_claim.id)
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to set superseded_by %s → %s: %s", existing.id, new_claim.id, exc)

            prev = temporal_meta.get("previous_value", existing.object_value)
            base.action = ReconciliationAction.SUPERSEDE
            base.resulting_claim_id = new_claim.id
            base.relationship_metadata = temporal_meta
            base.notes = f"temporal update: '{prev}' → '{candidate.object_value}'; superseded claim {existing.id}"
            _temporal_reason = f"temporal update: '{prev}' → '{candidate.object_value}'"
            self.contradiction_tracker.record(
                claim_id=new_claim.id,
                contradicts_id=existing.id,
                resolution="superseded",
                reason=_temporal_reason,
            )
            try:
                await _persist_contradiction(
                    self.storage,
                    claim_a_id=new_claim.id,
                    claim_b_id=existing.id,
                    owner_id=owner_id,
                    reason=_temporal_reason,
                    resolution="kept_a",
                )
            except Exception as _exc:  # noqa: BLE001
                logger.warning("Failed to persist contradiction to DB (temporal): %s", _exc)
            return base

        # CONDITIONAL → ADD (separate claim; existing stays active)
        if relationship == RelationshipKind.CONDITIONAL:
            condition_meta = dict(relationship_metadata or {})
            condition_str = condition_meta.get("condition", "")

            conditional_value = candidate.object_value
            if condition_str and condition_str.lower() not in conditional_value.lower():
                conditional_value = f"{candidate.object_value} [when: {condition_str}]"

            class _CondCandidateProxy:
                subject = candidate.subject
                predicate = candidate.predicate
                object_value = conditional_value
                confidence = candidate.confidence
                sensitivity = getattr(candidate, "sensitivity", "normal")
                temporal_marker = getattr(candidate, "temporal_marker", None)
                source_span = getattr(candidate, "source_span", None)

            new_claim = await self._add_claim(
                candidate=_CondCandidateProxy(),  # type: ignore[arg-type]
                session_id=session_id,
                owner_id=owner_id,
                subject_entity_id=subject_entity_id,
                relationship_metadata=condition_meta,
                supersede_stale=False,  # CONDITIONAL: existing claim stays active alongside new one
            )
            base.action = ReconciliationAction.ADD
            base.resulting_claim_id = new_claim.id
            base.candidate_value = conditional_value  # reflect the encoded value
            if condition_str:
                base.notes = f"conditional claim stored separately [when: {condition_str}]"
            else:
                base.notes = "conditional claim stored separately"
            return base

        # NEW or unrecognised → ADD separately
        new_claim = await self._add_claim(
            candidate=candidate,
            session_id=session_id,
            owner_id=owner_id,
            subject_entity_id=subject_entity_id,
        )
        base.action = ReconciliationAction.ADD
        base.resulting_claim_id = new_claim.id
        base.notes = f"new claim added (relationship: {relationship.value.lower()})"
        return base

    # _supersede_stale_claims, _add_claim, _record_provenance live in
    # StorageHelpersMixin (storage_helpers.py), inherited by this class.
