"""
cortex/sleep/s2_promotion.py — S2: Conservative Promotion Reviewer

Issue #360, Stage 2 of the Sleep Pipeline.

Responsibilities:
  1. Review session claims for eligibility to be promoted to long-term memory
  2. Apply conservative thresholds:
       - access_count >= 2  (retrieved multiple times → reinforced)
       - OR explicit user statement (explicitness == 'explicit')
  3. Require evidence: source_session must be set (traceable provenance)
  4. Promote eligible claims: set memory_type = 'long-term', confidence bumped
     to at least 0.8
  5. Return list of promoted claim IDs

Design principles:
  - No LLM calls — purely rule-based (fast, cheap, deterministic)
  - All methods are async
  - Non-fatal: failures per-claim are logged and skipped
  - Does NOT delete or archive anything — promotion only

Thresholds (all configurable via kwargs to S2Promoter):
  access_threshold    int   default 2   — access_count required for auto-promotion
  min_confidence      float default 0.4 — claims below this are never promoted
  promoted_confidence float default 0.8 — confidence floor after promotion
"""

from __future__ import annotations

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

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter
    from cortex.config import CortexConfig
    from cortex.core.feedback import FeedbackSystem
    from cortex.core.promotion import PromotionScorer
    from cortex.types import SemanticClaim

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result types
# ---------------------------------------------------------------------------

@dataclass
class PromotionDecision:
    """Record of a single claim promotion decision."""

    claim_id: str
    promoted: bool
    reason: str  # why promoted or skipped
    access_count: int = 0
    explicitness: str = "inferred"
    had_evidence: bool = False


@dataclass
class S2Result:
    """Result of the S2 promotion review stage."""

    session_id: str
    owner_id: str
    claims_reviewed: int = 0
    claims_promoted: int = 0
    promoted_ids: List[str] = field(default_factory=list)
    decisions: List[PromotionDecision] = field(default_factory=list)
    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 {
            "session_id": self.session_id,
            "owner_id": self.owner_id,
            "claims_reviewed": self.claims_reviewed,
            "claims_promoted": self.claims_promoted,
            "promoted_ids": self.promoted_ids,
            "completed_at": self.completed_at,
            "success": self.success,
            "error": self.error,
        }


# ---------------------------------------------------------------------------
# S2Promoter
# ---------------------------------------------------------------------------


class S2Promoter:
    """
    S2: Conservative Promotion Reviewer.

    Reviews all session-tier claims for a session and promotes qualifying
    claims to long-term memory.

    Promotion criteria (either condition must be met):
      A. access_count >= access_threshold (default 2)
      B. explicitness == 'explicit'  (user directly stated this fact)

    Hard gate (both required to even consider promotion):
      - source_session is not None  (claim has a traceable session origin)
      - confidence >= min_confidence (default 0.4)
      - status == 'active'
      - memory_type == 'session'  (don't re-promote already-promoted claims)

    Args:
        storage:            StorageAdapter for reading/updating claims.
        access_threshold:   Minimum access_count for auto-promotion.
        min_confidence:     Minimum confidence for promotion eligibility.
        promoted_confidence: Confidence floor set after promotion.
    """

    def __init__(
        self,
        storage: "StorageAdapter",
        access_threshold: int = 2,
        min_confidence: float = 0.4,
        promoted_confidence: float = 0.8,
        *,
        config: Optional["CortexConfig"] = None,
        feedback_system: Optional["FeedbackSystem"] = None,
        promotion_scorer: Optional["PromotionScorer"] = None,
    ) -> None:
        self._storage = storage
        self._access_threshold = access_threshold
        self._min_confidence = min_confidence
        self._promoted_confidence = promoted_confidence
        self._config = config
        self._feedback_system = feedback_system
        self._promotion_scorer = promotion_scorer

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def run(self, session_id: str, owner_id: str = "default") -> S2Result:
        """
        Run S2 for the given session.

        Args:
            session_id: The session whose claims are reviewed (source_session FK).
            owner_id:   Owner/tenant identifier.

        Returns:
            S2Result with list of promoted claim IDs and per-claim decisions.
        """
        logger.info("[S2] reviewing session=%s owner=%s", session_id, owner_id)
        result = S2Result(session_id=session_id, owner_id=owner_id)

        # Gather session claims
        claims = await self._gather_claims(session_id)
        result.claims_reviewed = len(claims)

        if not claims:
            logger.info("[S2] no session claims found for session=%s", session_id)
            result.completed_at = datetime.now(timezone.utc).isoformat()
            return result

        # Use PromotionScorer if available (Issue #376)
        if self._promotion_scorer is not None:
            return await self._run_with_scorer(
                session_id=session_id,
                owner_id=owner_id,
                claims=claims,
                result=result,
            )

        # Legacy path: rule-based evaluation
        for claim in claims:
            decision = self._evaluate(claim)
            result.decisions.append(decision)

            if decision.promoted:
                try:
                    await self._promote(claim)
                    result.promoted_ids.append(claim.id)
                    result.claims_promoted += 1
                    logger.debug(
                        "[S2] promoted claim=%s reason=%s", claim.id, decision.reason
                    )
                except Exception as exc:
                    logger.error(
                        "[S2] promote failed for claim=%s: %s", claim.id, exc
                    )
                    decision.promoted = False
                    decision.reason = f"promote_error: {exc}"

        result.completed_at = datetime.now(timezone.utc).isoformat()
        logger.info(
            "[S2] done: session=%s reviewed=%d promoted=%d",
            session_id, result.claims_reviewed, result.claims_promoted,
        )
        return result

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    async def _run_with_scorer(
        self,
        session_id: str,
        owner_id: str,
        claims: List["SemanticClaim"],
        result: S2Result,
    ) -> S2Result:
        """Run promotion using the PromotionScorer (Issue #376).

        Scores all claims in batch, promotes those above threshold,
        and logs per-claim scoring breakdowns.
        """
        assert self._promotion_scorer is not None

        # Filter to eligible claims (session-type, active, has evidence)
        eligible = [
            c for c in claims
            if getattr(c, "memory_type", "session") == "session"
            and getattr(c, "status", "active") == "active"
            and getattr(c, "source_session", None) is not None
            and getattr(c, "confidence", 0.5) >= self._min_confidence
        ]

        if not eligible:
            logger.info("[S2] no eligible claims for scoring in session=%s", session_id)
            result.completed_at = datetime.now(timezone.utc).isoformat()
            return result

        # Score all eligible claims in batch
        scores = await self._promotion_scorer.score_batch(
            eligible, session_id, self._storage,
        )

        for score in scores:
            decision = PromotionDecision(
                claim_id=score.claim_id,
                promoted=score.promoted,
                reason=score.reason,
                access_count=0,
                explicitness="",
                had_evidence=True,
            )
            result.decisions.append(decision)

            if score.promoted:
                # Find the claim object
                claim = next((c for c in eligible if c.id == score.claim_id), None)
                if claim is None:
                    continue
                try:
                    await self._promote(claim)
                    result.promoted_ids.append(score.claim_id)
                    result.claims_promoted += 1
                    logger.info(
                        "[S2] promoted claim=%s score=%.3f components=%s",
                        score.claim_id,
                        score.overall,
                        {
                            "evidence": score.evidence_strength,
                            "sessions": score.cross_session_frequency,
                            "explicit": score.user_explicitness,
                            "utility": score.retrieval_utility,
                            "stability": score.temporal_stability,
                            "no_contradiction": score.contradiction_absence,
                        },
                    )
                except Exception as exc:
                    logger.error(
                        "[S2] promote failed for claim=%s: %s", score.claim_id, exc,
                    )
                    decision.promoted = False
                    decision.reason = f"promote_error: {exc}"
            else:
                logger.debug(
                    "[S2] skip claim=%s score=%.3f reason=%s",
                    score.claim_id, score.overall, score.reason,
                )

        result.completed_at = datetime.now(timezone.utc).isoformat()
        logger.info(
            "[S2] done (scorer): session=%s reviewed=%d promoted=%d",
            session_id, result.claims_reviewed, result.claims_promoted,
        )
        return result

    async def _gather_claims(self, session_id: str) -> List["SemanticClaim"]:
        """Fetch session-tier claims for this session."""
        try:
            return await self._storage.get_session_claims(session_id)
        except Exception as exc:
            logger.error("[S2] get_session_claims failed: %s", exc)
            return []

    def _evaluate(self, claim: "SemanticClaim") -> PromotionDecision:
        """
        Evaluate a single claim for promotion eligibility.

        Returns a PromotionDecision (promoted=True if eligible).
        """
        # Hard gates — claim must pass ALL of these
        if getattr(claim, "status", "active") != "active":
            return PromotionDecision(
                claim_id=claim.id,
                promoted=False,
                reason="skip:not_active",
                access_count=getattr(claim, "access_count", 0),
                explicitness=getattr(claim, "explicitness", "inferred"),
                had_evidence=bool(getattr(claim, "source_session", None)),
            )

        if getattr(claim, "memory_type", "session") != "session":
            return PromotionDecision(
                claim_id=claim.id,
                promoted=False,
                reason="skip:already_promoted",
                access_count=getattr(claim, "access_count", 0),
                explicitness=getattr(claim, "explicitness", "inferred"),
                had_evidence=bool(getattr(claim, "source_session", None)),
            )

        had_evidence = bool(getattr(claim, "source_session", None))
        if not had_evidence:
            return PromotionDecision(
                claim_id=claim.id,
                promoted=False,
                reason="skip:no_evidence",
                access_count=getattr(claim, "access_count", 0),
                explicitness=getattr(claim, "explicitness", "inferred"),
                had_evidence=False,
            )

        confidence = getattr(claim, "confidence", 0.5)
        if confidence < self._min_confidence:
            return PromotionDecision(
                claim_id=claim.id,
                promoted=False,
                reason=f"skip:low_confidence({confidence:.2f})",
                access_count=getattr(claim, "access_count", 0),
                explicitness=getattr(claim, "explicitness", "inferred"),
                had_evidence=had_evidence,
            )

        access_count = getattr(claim, "access_count", 0)
        explicitness = getattr(claim, "explicitness", "inferred")

        # Promotion condition A: high access count
        if access_count >= self._access_threshold:
            return PromotionDecision(
                claim_id=claim.id,
                promoted=True,
                reason=f"promote:access_count({access_count}>={self._access_threshold})",
                access_count=access_count,
                explicitness=explicitness,
                had_evidence=had_evidence,
            )

        # Promotion condition B: explicit user statement
        if explicitness == "explicit":
            return PromotionDecision(
                claim_id=claim.id,
                promoted=True,
                reason="promote:explicit_user_statement",
                access_count=access_count,
                explicitness=explicitness,
                had_evidence=had_evidence,
            )

        return PromotionDecision(
            claim_id=claim.id,
            promoted=False,
            reason=f"skip:below_threshold(access={access_count},explicit={explicitness})",
            access_count=access_count,
            explicitness=explicitness,
            had_evidence=had_evidence,
        )

    async def _promote(self, claim: "SemanticClaim") -> None:
        """Persist promotion: set memory_type='long-term', bump confidence."""
        new_confidence = max(
            getattr(claim, "confidence", 0.5),
            self._promoted_confidence,
        )
        await self._storage.update_claim(
            claim.id,
            memory_type="long-term",
            confidence=new_confidence,
        )
