"""
cortex/sleep/pipeline.py — Sleep Pipeline Orchestrator

Issue #360.

Runs the three-stage sleep pipeline in sequence:
    S1 (session consolidator) → S2 (promotion reviewer) → S3 (handoff builder)

Any individual stage failure is non-fatal: the pipeline logs the error,
records it in the result, and continues to the next stage.

Public API::

    result = await run_sleep(
        session_id="sess-abc",
        storage=storage,
        config=config,
        llm_fn=llm_provider,
        owner_id="default",
    )

Or via the class interface::

    pipeline = SleepPipeline(storage=storage, llm=llm, config=config)
    result = await pipeline.run(session_id="sess-abc")
"""

from __future__ import annotations

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

from cortex.commitments.carry_forward import carry_forward_loops
from cortex.sleep.s1_consolidator import S1Consolidator, S1Result
from cortex.sleep.s2_promotion import S2Promoter, S2Result
from cortex.sleep.s3_handoff import S3HandoffBuilder, S3Result
from cortex.sleep.s5_contradiction_resolver import S5ContradictionResolver, S5Result

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

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------

@dataclass
class SleepPipelineReport:
    """Aggregate result of the full S1→S2→S3 sleep pipeline."""

    session_id: str
    owner_id: str

    # Per-stage results (None if stage was skipped due to prior error)
    s1: Optional[S1Result] = None
    s2: Optional[S2Result] = None
    s3: Optional[S3Result] = None
    s5: Optional[S5Result] = None

    # Aggregate stats
    claims_reviewed: int = 0
    claims_promoted: int = 0
    handoff_packet_id: Optional[str] = None
    loops_incremented: int = 0
    loops_auto_resolved: int = 0
    contradictions_resolved: int = 0

    # Timing
    started_at: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )
    completed_at: Optional[str] = None
    duration_ms: int = 0

    # Stage errors (stage_name → error string)
    errors: Dict[str, str] = field(default_factory=dict)

    @property
    def success(self) -> bool:
        """True if all stages completed without error."""
        return len(self.errors) == 0

    def to_dict(self) -> dict:
        return {
            "session_id": self.session_id,
            "owner_id": self.owner_id,
            "success": self.success,
            "claims_reviewed": self.claims_reviewed,
            "claims_promoted": self.claims_promoted,
            "handoff_packet_id": self.handoff_packet_id,
            "loops_incremented": self.loops_incremented,
            "loops_auto_resolved": self.loops_auto_resolved,
            "contradictions_resolved": self.contradictions_resolved,
            "started_at": self.started_at,
            "completed_at": self.completed_at,
            "duration_ms": self.duration_ms,
            "errors": self.errors,
            "s1": self.s1.to_dict() if self.s1 else None,
            "s2": self.s2.to_dict() if self.s2 else None,
            "s3": self.s3.to_dict() if self.s3 else None,
            "s5": self.s5.to_dict() if self.s5 else None,
        }


# ---------------------------------------------------------------------------
# Convenience function (functional style)
# ---------------------------------------------------------------------------

async def run_sleep(
    session_id: str,
    storage: "StorageAdapter",
    config: "CortexConfig",
    llm_fn: "LLMProvider",
    owner_id: str = "default",
) -> SleepPipelineReport:
    """
    Run the full sleep pipeline for a session.

    Args:
        session_id: The session to consolidate (source_session FK for claims).
        storage:    StorageAdapter instance.
        config:     CortexConfig (used for extraction model, thresholds).
        llm_fn:     LLMProvider for S1 summarisation.
        owner_id:   Owner/tenant identifier.

    Returns:
        SleepPipelineReport with per-stage results and aggregate stats.
    """
    pipeline = SleepPipeline(storage=storage, llm=llm_fn, config=config)
    return await pipeline.run(session_id=session_id, owner_id=owner_id)


# ---------------------------------------------------------------------------
# Class interface
# ---------------------------------------------------------------------------

class SleepPipeline:
    """
    Orchestrates the S1→S2→S3 sleep pipeline.

    Args:
        storage:            StorageAdapter instance.
        llm:                LLMProvider for S1 summarisation.
        config:             CortexConfig for thresholds and model selection.
        s1_max_claims:      Cap on claims sent to LLM in S1.
        s2_access_threshold: Min access_count for S2 promotion.
        s2_min_confidence:  Min confidence for S2 promotion eligibility.
        s2_promoted_confidence: Confidence floor applied after S2 promotion.
        s3_max_key_facts:   Max cornerstones in S3 handoff packet.
        s3_max_decisions:   Max decision claims in S3 handoff packet.
        s3_max_open_loops:  Max open loops in S3 handoff packet.
    """

    def __init__(
        self,
        storage: "StorageAdapter",
        llm: "LLMProvider",
        config: Optional["CortexConfig"] = None,
        *,
        s1_max_claims: int = 50,
        s2_access_threshold: int = 2,
        s2_min_confidence: float = 0.4,
        s2_promoted_confidence: float = 0.8,
        s3_max_key_facts: int = 5,
        s3_max_decisions: int = 5,
        s3_max_open_loops: int = 10,
        promotion_scorer: Optional["PromotionScorer"] = None,
    ) -> None:
        self._storage = storage
        self._llm = llm
        self._config = config

        self._s1 = S1Consolidator(
            storage=storage,
            llm=llm,
            max_claims=s1_max_claims,
        )
        self._s2 = S2Promoter(
            storage=storage,
            access_threshold=s2_access_threshold,
            min_confidence=s2_min_confidence,
            promoted_confidence=s2_promoted_confidence,
            promotion_scorer=promotion_scorer,
        )
        self._s3 = S3HandoffBuilder(
            storage=storage,
            max_key_facts=s3_max_key_facts,
            max_decisions=s3_max_decisions,
            max_open_loops=s3_max_open_loops,
        )

    async def run(
        self,
        session_id: str,
        owner_id: str = "default",
    ) -> SleepPipelineReport:
        """
        Execute S1 → S2 → S3 for the given session.

        Stage failures are non-fatal: the pipeline logs each error,
        records it in SleepPipelineReport.errors, and continues.

        Args:
            session_id: The session to consolidate.
            owner_id:   Owner/tenant identifier.

        Returns:
            SleepPipelineReport.
        """
        import time
        t0 = time.monotonic()

        report = SleepPipelineReport(
            session_id=session_id,
            owner_id=owner_id,
        )

        # -----------------------------------------------------------
        # S1: Session Consolidation
        # -----------------------------------------------------------
        session_summary = ""
        try:
            logger.info("[Sleep] S1 starting session=%s", session_id)
            s1_result = await self._s1.run(session_id=session_id, owner_id=owner_id)
            report.s1 = s1_result
            session_summary = s1_result.summary
            if not s1_result.success:
                report.errors["s1"] = s1_result.error or "S1 failed"
                logger.warning("[Sleep] S1 completed with error: %s", s1_result.error)
            else:
                logger.info("[Sleep] S1 done: summary_len=%d", len(session_summary))
        except Exception as exc:
            logger.error("[Sleep] S1 uncaught exception: %s", exc, exc_info=True)
            report.errors["s1"] = str(exc)

        # -----------------------------------------------------------
        # S2: Promotion Review
        # -----------------------------------------------------------
        promoted_ids: List[str] = []
        try:
            logger.info("[Sleep] S2 starting session=%s", session_id)
            s2_result = await self._s2.run(session_id=session_id, owner_id=owner_id)
            report.s2 = s2_result
            report.claims_reviewed = s2_result.claims_reviewed
            report.claims_promoted = s2_result.claims_promoted
            promoted_ids = s2_result.promoted_ids
            if not s2_result.success:
                report.errors["s2"] = s2_result.error or "S2 failed"
                logger.warning("[Sleep] S2 completed with error: %s", s2_result.error)
            else:
                logger.info(
                    "[Sleep] S2 done: reviewed=%d promoted=%d",
                    s2_result.claims_reviewed, s2_result.claims_promoted,
                )
        except Exception as exc:
            logger.error("[Sleep] S2 uncaught exception: %s", exc, exc_info=True)
            report.errors["s2"] = str(exc)

        # -----------------------------------------------------------
        # S3: Handoff Packet Builder
        # -----------------------------------------------------------
        try:
            logger.info("[Sleep] S3 starting session=%s", session_id)
            s3_result = await self._s3.run(
                session_id=session_id,
                owner_id=owner_id,
                session_summary=session_summary,
                promoted_ids=promoted_ids,
            )
            report.s3 = s3_result
            report.handoff_packet_id = s3_result.packet_id
            if not s3_result.success:
                report.errors["s3"] = s3_result.error or "S3 failed"
                logger.warning("[Sleep] S3 completed with error: %s", s3_result.error)
            else:
                logger.info("[Sleep] S3 done: packet=%s", s3_result.packet_id)
        except Exception as exc:
            logger.error("[Sleep] S3 uncaught exception: %s", exc, exc_info=True)
            report.errors["s3"] = str(exc)

        # -----------------------------------------------------------
        # S4: Carry-forward open loops
        # -----------------------------------------------------------
        try:
            logger.info("[Sleep] S4 carry-forward starting session=%s", session_id)
            _stale = (
                getattr(self._config, "open_loop_stale_threshold", None)
                if self._config else None
            )
            cf_result = await carry_forward_loops(
                storage=self._storage,
                session_id=session_id,
                owner_id=owner_id,
                stale_threshold=_stale,
            )
            report.loops_incremented = cf_result["incremented"]
            report.loops_auto_resolved = cf_result["auto_resolved"]
            logger.info(
                "[Sleep] S4 done: incremented=%d auto_resolved=%d",
                cf_result["incremented"], cf_result["auto_resolved"],
            )
        except Exception as exc:
            logger.error("[Sleep] S4 carry-forward exception: %s", exc, exc_info=True)
            report.errors["s4_carry_forward"] = str(exc)

        # -----------------------------------------------------------
        # S5: Contradiction resolver (dream-cycle auto-resolution)
        # -----------------------------------------------------------
        try:
            logger.info("[Sleep] S5 contradiction resolver starting owner=%s", owner_id)
            s5_resolver = S5ContradictionResolver(storage=self._storage)
            s5_result = await s5_resolver.run(owner_id=owner_id)
            report.s5 = s5_result
            report.contradictions_resolved = s5_result.resolved_count
            if not s5_result.success:
                report.errors["s5"] = s5_result.error or "S5 failed"
                logger.warning("[Sleep] S5 completed with error: %s", s5_result.error)
            else:
                logger.info(
                    "[Sleep] S5 done: resolved=%d skipped=%d errors=%d",
                    s5_result.resolved_count, s5_result.skipped_count, s5_result.error_count,
                )
        except Exception as exc:
            logger.error("[Sleep] S5 uncaught exception: %s", exc, exc_info=True)
            report.errors["s5"] = str(exc)

        # -----------------------------------------------------------
        # Finalise
        # -----------------------------------------------------------
        elapsed_ms = int((time.monotonic() - t0) * 1000)
        report.duration_ms = elapsed_ms
        report.completed_at = datetime.now(timezone.utc).isoformat()

        if report.success:
            logger.info(
                "[Sleep] pipeline complete: session=%s duration_ms=%d promoted=%d packet=%s",
                session_id, elapsed_ms, report.claims_promoted, report.handoff_packet_id,
            )
        else:
            logger.warning(
                "[Sleep] pipeline finished with errors: session=%s errors=%s",
                session_id, list(report.errors.keys()),
            )

        return report


# Alias for backward-compat with types module stubs
SleepResult = SleepPipelineReport
