"""LifecycleMixin — wake(), boot_session(), sleep(), and dream() operations."""
from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional

from .models import WakeReport, SleepReport, _now, _new_session_id

logger = logging.getLogger(__name__)


class LifecycleMixin:
    """Mixin providing session lifecycle operations."""

    async def wake(self, session_id: Optional[str] = None, owner_id: Optional[str] = None) -> WakeReport:
        """
        Start a new session (wake hook).

        Loads cornerstones and sets the active session_id.
        """
        sid = session_id or _new_session_id()
        effective_owner = owner_id or self.config.owner_id
        self._session_id = sid
        self._llm_call_count = 0   # reset per-session LLM budget

        errors: List[str] = []
        cornerstones_loaded = 0

        # Record session start in storage
        if self.storage is not None:
            try:
                await self.storage.create_session(
                    session_id=sid,
                    owner_id=effective_owner,
                )
            except Exception as exc:
                err = f"Failed to record session start: {exc}"
                logger.warning(err)
                errors.append(err)

        # Pre-load cornerstones
        if self.cornerstone_guardian is not None:
            try:
                cs_list = await self.cornerstone_guardian.load_all(
                    owner_id=effective_owner,
                )
                cornerstones_loaded = len(cs_list)
                logger.info("Woke: session=%s cornerstones=%d", sid, cornerstones_loaded)
            except Exception as exc:
                err = f"Failed to load cornerstones on wake: {exc}"
                logger.warning(err)
                errors.append(err)

        # Load latest handoff packet from previous sleep cycle
        handoff_packet: Optional[Dict[str, Any]] = None
        if self.storage is not None:
            try:
                packets = await self.storage.get_handoff_packets(
                    owner_id=effective_owner,
                    status="active",
                    limit=1,
                )
                if packets:
                    handoff_packet = packets[0]
                    logger.info("Wake: loaded handoff packet=%s", handoff_packet.get("id", "?"))
            except Exception as exc:
                err = f"Failed to load handoff packet on wake: {exc}"
                logger.warning(err)
                errors.append(err)

        # Load due commitments and open loops for boot injection
        due_commitments_count = 0
        open_loops_count = 0
        if self.storage is not None:
            try:
                from cortex.commitments.carry_forward import get_boot_items
                boot_items = await get_boot_items(
                    storage=self.storage,
                    owner_id=effective_owner,
                )
                due_commitments_count = len(boot_items.get("due_commitments", []))
                open_loops_count = len(boot_items.get("open_loops", []))
                logger.info(
                    "Wake: boot items loaded — due_commitments=%d open_loops=%d",
                    due_commitments_count, open_loops_count,
                )
            except Exception as exc:
                err = f"Failed to load boot items on wake: {exc}"
                logger.warning(err)
                errors.append(err)

        # Emit SESSION_STARTED event
        if getattr(self, "event_emitter", None) is not None:
            try:
                from cortex.events.emitter import CortexEvent, EventType
                await self.event_emitter.emit(CortexEvent(
                    event_type=EventType.SESSION_STARTED,
                    owner_id=effective_owner,
                    payload={
                        "session_id": sid,
                        "cornerstones_loaded": cornerstones_loaded,
                        "handoff_packet_id": handoff_packet.get("id") if handoff_packet else None,
                        "due_commitments": due_commitments_count,
                        "open_loops": open_loops_count,
                    },
                    session_id=sid,
                ))
            except Exception as emit_exc:
                logger.debug("Event emit failed (SESSION_STARTED): %s", emit_exc)

        # Boot loader — deterministic continuity context (#361)
        boot_sequence = None
        if getattr(self.config, "boot_enabled", True):
            try:
                boot_sequence = await self.boot_session(
                    session_id=sid,
                    owner_id=effective_owner,
                )
                if boot_sequence is not None:
                    logger.info(
                        "Wake: boot sequence loaded — tokens=%d time=%.1fms",
                        boot_sequence.boot_tokens,
                        boot_sequence.boot_time_ms,
                    )
            except Exception as exc:
                err = f"Failed to run boot sequence: {exc}"
                logger.warning(err)
                errors.append(err)

        return WakeReport(
            session_id=sid,
            cornerstones_loaded=cornerstones_loaded,
            handoff_packet=handoff_packet,
            due_commitments=due_commitments_count,
            open_loops=open_loops_count,
            errors=errors,
        )

    async def boot_session(
        self,
        session_id: str,
        owner_id: Optional[str] = None,
        context_budget: Optional[int] = None,
    ) -> Optional[Any]:
        """Run the session boot loader for continuity context (#361).

        Builds a deterministic boot sequence: handoff, identity, commitments,
        preferences, and recent summaries — then pre-warms working memory.

        Called automatically by wake() when boot_enabled is True.

        Args:
            session_id: The session being booted.
            owner_id: Memory owner namespace (defaults to config.owner_id).
            context_budget: Total context window tokens. Defaults to
                            config retrieval_token_budget.

        Returns:
            BootSequence or None if boot loader is unavailable.
        """
        if self.storage is None:
            logger.warning("boot_session: storage not available")
            return None

        effective_owner = owner_id or self.config.owner_id
        budget = context_budget or self.config.retrieval_token_budget

        try:
            from cortex.core.boot_loader import SessionBootLoader

            loader = SessionBootLoader(
                storage=self.storage,
                compiler=self.context_compiler,
                working_memory=self.working_memory,
                config=self.config,
            )
            return await loader.boot(
                session_id=session_id,
                owner_id=effective_owner,
                context_budget=budget,
            )
        except Exception as exc:
            logger.error("boot_session failed: %s", exc, exc_info=True)
            return None

    async def sleep(
        self,
        session_id: Optional[str] = None,
        consolidate: bool = False,
    ) -> SleepReport:
        """
        End the current session (sleep hook).

        Closes the session record in storage.  When *consolidate=True* the full
        SleepConsolidator pipeline (memory dump -> deprecation -> drift check ->
        consolidation -> confirmation) runs before the normal session teardown.

        Args:
            session_id:  Session to close.  Uses the current session when absent.
            consolidate: If True, run SleepConsolidator.sleep() before closing.
                         Defaults to False to preserve existing behaviour.
        """
        sid = session_id or self._session_id or "unknown"
        errors: List[str] = []
        memories_summarised = 0

        # ------------------------------------------------------------------
        # Optional consolidation path
        # ------------------------------------------------------------------
        if consolidate and self.storage is not None:
            logger.info("sleep(): running SleepConsolidator for session %s", sid)
            try:
                from cortex.circadian.sleep import SleepConsolidator
                from cortex.identity.drift_calculator import DriftCalculator
                from cortex.deprecation.pipeline import DeprecationPipeline

                drift_calculator = DriftCalculator(
                    storage=self.storage,
                    llm=self.llm,
                    config=self.config,
                )
                deprecation_pipeline = DeprecationPipeline(
                    storage=self.storage,
                    config=self.config,
                )
                consolidator = SleepConsolidator(
                    storage=self.storage,
                    config=self.config,
                    llm=self.llm,
                    cornerstone_guardian=self.cornerstone_guardian,
                    drift_calculator=drift_calculator,
                    deprecation_pipeline=deprecation_pipeline,
                    event_emitter=getattr(self, "event_emitter", None),
                )
                consolidation_report = await consolidator.sleep(sid)
                memories_summarised = getattr(consolidation_report, "claims_promoted", 0)
                logger.info(
                    "sleep(): consolidation complete for session %s "
                    "(promoted=%d pruned=%d drift_checks=%d)",
                    sid,
                    getattr(consolidation_report, "claims_promoted", 0),
                    getattr(consolidation_report, "claims_pruned", 0),
                    len(getattr(consolidation_report, "drift_checks", [])),
                )
                # SleepConsolidator.sleep() already called update_session with
                # status="sleeping", so we skip the close_session call below.
                _consolidation_handled_close = True
            except Exception as exc:
                err = f"SleepConsolidator failed (session={sid}): {exc}"
                logger.warning(err, exc_info=True)
                errors.append(err)
                _consolidation_handled_close = False
        else:
            if consolidate:
                logger.warning(
                    "sleep(): consolidate=True but storage is unavailable; skipping consolidation"
                )
            _consolidation_handled_close = False
            logger.debug("sleep(): consolidate=False -- minimal close for session %s", sid)

        if self.storage is not None and not _consolidation_handled_close:
            try:
                await self.storage.close_session(
                    session_id=sid,
                    owner_id=self.config.owner_id,
                    ended_at=_now(),
                    status="sleeping",
                )
                logger.info("Session %s closed (sleep)", sid)
            except Exception as exc:
                err = f"Failed to close session: {exc}"
                logger.warning(err)
                errors.append(err)

        # Clear working memory for the ended session
        if getattr(self, "working_memory", None) is not None:
            try:
                cleared = await self.working_memory.clear_session(sid)
                if cleared > 0:
                    logger.info("Cleared %d working memory entries for session %s", cleared, sid)
            except Exception as exc:
                err = f"Failed to clear working memory for session {sid}: {exc}"
                logger.warning(err)
                errors.append(err)

        # Emit SESSION_ENDED event
        if getattr(self, "event_emitter", None) is not None:
            try:
                from cortex.events.emitter import CortexEvent, EventType
                await self.event_emitter.emit(CortexEvent(
                    event_type=EventType.SESSION_ENDED,
                    owner_id=self.config.owner_id,
                    payload={"session_id": sid},
                    session_id=sid,
                ))
            except Exception as emit_exc:
                logger.debug("Event emit failed (SESSION_ENDED): %s", emit_exc)

        self._session_id = None
        return SleepReport(
            session_id=sid,
            memories_summarised=memories_summarised,
            errors=errors,
        )

    # =========================================================================
    # DREAM CYCLE (M6a/M6b full consolidation)
    # =========================================================================

    async def dream(self, owner_id: str = "default") -> Any:
        """Trigger the full nightly dream cycle (Electric Sheep consolidation).

        Wires CircadianEngine + SleepConsolidator and runs the complete dream
        pipeline: orphan recovery, Ebbinghaus decay, claim consolidation,
        contradiction resolution, cornerstone proposals, and journal generation.

        This is the canonical entry point for SDK and HTTP callers; MCP uses
        the same logic internally.

        Args:
            owner_id: Memory owner namespace.

        Returns:
            DreamReport dataclass instance.
        """
        from cortex.circadian.engine import CircadianEngine
        from cortex.circadian.sleep import SleepConsolidator

        from cortex.identity.drift_calculator import DriftCalculator
        from cortex.deprecation.pipeline import DeprecationPipeline

        drift_calculator = DriftCalculator(
            storage=self.storage,
            llm=self.llm,
            config=self.config,
        )
        deprecation_pipeline = DeprecationPipeline(
            storage=self.storage,
            config=self.config,
        )
        # Use the persistent circadian engine if available (#772),
        # otherwise create a throwaway one for manual triggers.
        _persistent_engine = getattr(self, "_circadian_engine", None)
        if _persistent_engine is not None:
            return await _persistent_engine.dream(owner_id=owner_id)

        engine = CircadianEngine(
            storage=self.storage,
            config=self.config,
            llm=self.llm,
            cornerstone_guardian=self.cornerstone_guardian,
            drift_calculator=drift_calculator,
            deprecation_pipeline=deprecation_pipeline,
            promotion_scorer=getattr(self, "promotion_scorer", None),
        )
        return await engine.dream(owner_id=owner_id)
