"""
cortex/sleep/s5_contradiction_resolver.py — S5: Contradiction Resolver

Dream-cycle stage that auto-resolves persisted contradictions whose
outcome is already determined by claim status.

Logic
-----
For each unresolved contradiction row (resolution IS NULL):
  - If claim_a (the "new/winning" claim) status == 'active' AND
       claim_b (the "old" claim) status == 'superseded':
       → resolution = 'kept_a'  (the original engine decision stands)
  - If claim_a status == 'superseded' AND claim_b status == 'active':
       → resolution = 'kept_b'  (unusual — an edge case where the
          "winner" was itself later superseded)
  - If both claims are superseded:
       → resolution = 'merged'  (both replaced; treat as merged/obsolete)
  - Otherwise: leave unresolved (claims may still be active, or missing)

This stage is non-fatal: individual row failures are logged and skipped.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter

logger = logging.getLogger(__name__)


def _now() -> str:
    """Return current UTC time as ISO-8601 string."""
    return datetime.now(timezone.utc).isoformat()


# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------


@dataclass
class S5Result:
    """Result of the S5 contradiction-resolution dream-cycle stage."""

    owner_id: str
    total_unresolved: int = 0
    """How many unresolved contradiction rows were found at the start."""
    resolved_count: int = 0
    """How many were successfully resolved during this run."""
    skipped_count: int = 0
    """Rows skipped because claim status could not be determined."""
    error_count: int = 0
    """Rows that raised exceptions (logged; not fatal)."""
    completed_at: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )
    success: bool = True
    error: Optional[str] = None

    def to_dict(self) -> dict:
        return {
            "owner_id": self.owner_id,
            "total_unresolved": self.total_unresolved,
            "resolved_count": self.resolved_count,
            "skipped_count": self.skipped_count,
            "error_count": self.error_count,
            "completed_at": self.completed_at,
            "success": self.success,
            "error": self.error,
        }


# ---------------------------------------------------------------------------
# Stage implementation
# ---------------------------------------------------------------------------


class S5ContradictionResolver:
    """Dream-cycle stage: resolve dormant contradictions by inspecting claim status.

    Parameters
    ----------
    storage:
        Open StorageAdapter instance with access to ``contradictions`` and
        ``semantic_claim`` tables.
    """

    def __init__(self, storage: "StorageAdapter") -> None:
        self._storage = storage

    async def run(self, owner_id: str = "default") -> S5Result:
        """Run the contradiction-resolution stage for *owner_id*.

        Fetches all unresolved contradiction rows and attempts to
        auto-resolve each one based on the current status of the two
        referenced claims.

        Args:
            owner_id: Multi-tenant scope key.

        Returns:
            S5Result with resolution statistics.
        """
        from cortex.core.contradiction_store import get_unresolved, mark_resolved

        result = S5Result(owner_id=owner_id)

        try:
            rows = await get_unresolved(self._storage, owner_id)
        except Exception as exc:
            msg = f"S5: failed to fetch unresolved contradictions: {exc}"
            logger.error(msg)
            result.success = False
            result.error = msg
            return result

        result.total_unresolved = len(rows)
        if not rows:
            logger.debug("S5: no unresolved contradictions for owner=%s", owner_id)
            return result

        logger.info(
            "S5: processing %d unresolved contradiction(s) for owner=%s",
            len(rows), owner_id,
        )

        now = _now()
        for row in rows:
            contradiction_id: int = row["id"]
            claim_a_id: str = row["claim_a_id"]
            claim_b_id: str = row["claim_b_id"]

            try:
                resolution = await self._determine_resolution(claim_a_id, claim_b_id)
                if resolution is None:
                    result.skipped_count += 1
                    logger.debug(
                        "S5: skip contradiction id=%d — cannot determine resolution "
                        "(claims may still be active or missing)",
                        contradiction_id,
                    )
                    continue

                ok = await mark_resolved(
                    self._storage,
                    contradiction_id=contradiction_id,
                    resolution=resolution,
                    resolved_at=now,
                )
                if ok:
                    result.resolved_count += 1
                    logger.debug(
                        "S5: resolved contradiction id=%d → %s",
                        contradiction_id, resolution,
                    )
                else:
                    result.error_count += 1
                    logger.warning(
                        "S5: mark_resolved returned False for contradiction id=%d",
                        contradiction_id,
                    )

            except Exception as exc:  # noqa: BLE001
                result.error_count += 1
                logger.warning(
                    "S5: error processing contradiction id=%d: %s",
                    contradiction_id, exc,
                )

        logger.info(
            "S5 done: resolved=%d skipped=%d errors=%d owner=%s",
            result.resolved_count, result.skipped_count, result.error_count, owner_id,
        )
        return result

    async def _determine_resolution(
        self,
        claim_a_id: str,
        claim_b_id: str,
    ) -> Optional[str]:
        """Return resolution string based on the current status of the two claims.

        Returns:
            'kept_a', 'kept_b', 'merged', or None (cannot resolve yet).
        """
        status_a = await self._get_claim_status(claim_a_id)
        status_b = await self._get_claim_status(claim_b_id)

        # Both missing (deleted) → treat as merged/gone
        if status_a is None and status_b is None:
            return "merged"

        # claim_a active, claim_b superseded → original engine decision stands
        if status_a == "active" and status_b == "superseded":
            return "kept_a"

        # claim_b somehow active, claim_a superseded → rare reversal
        if status_a == "superseded" and status_b == "active":
            return "kept_b"

        # Both superseded → both replaced, treat as merged
        if status_a == "superseded" and status_b == "superseded":
            return "merged"

        # One missing, one still active → cannot decide yet
        return None

    async def _get_claim_status(self, claim_id: str) -> Optional[str]:
        """Return the ``status`` column of a claim, or None if not found."""
        try:
            conn = await self._storage._get_conn()
            async with conn.execute(
                "SELECT status FROM semantic_claim WHERE id = ? LIMIT 1",
                (claim_id,),
            ) as cur:
                row = await cur.fetchone()
            return row["status"] if row else None
        except Exception as exc:  # noqa: BLE001
            logger.warning("S5: failed to fetch status for claim %s: %s", claim_id, exc)
            return None
