"""
MODULE 6a: CIRCADIAN ENGINE — Scheduler + Trigger Logic
=========================================================
Manages the session lifecycle state machine: AWAKE → DROWSY → SLEEPING → DREAMING → AWAKE.

Responsibilities (M6a):
  - wake()           — create session record, load cornerstones, handle orphans
  - update_fatigue() — track session fatigue from context ratio / events / time
  - should_nap()     — threshold check against nap_threshold
  - nap()            — mid-session mini-consolidation, reset fatigue to 0.4
  - get_fatigue()    — current fatigue accessor
  - sleep()          — stub (consolidation logic in M6b); triggers state transition
  - dream()          — stub (deep consolidation in M6b); triggers state transition
  - shutdown()       — clean shutdown: cancel tasks, emergency-sleep active sessions,
                       persist state to DB for crash recovery
  - Asyncio background scheduler for auto-sleep after idle_timeout
  - Asyncio background scheduler for dream cycle at configured UTC hour
  - Manual trigger support for forced dream/sleep cycles
  - wake() / sleep() lifecycle hooks
  - Context manager support (async with CircadianEngine(...) as engine: ...)
  - SIGTERM / SIGINT signal handling for graceful OS-level shutdown

Design principles:
  - All public methods are async
  - State transitions logged to session_record
  - Orphaned session detection and recovery at wake time
  - Fatigue score: 0.0 (fresh) → 1.0 (exhausted)
  - Shutdown state persisted to cortex_config for crash recovery
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Optional

from cortex.circadian.engine.models import (
    CircadianState,
    WakeReport,
    FATIGUE_TIME_INTERVAL_SEC,
)
from cortex.circadian.engine.fatigue import FatigueMixin
from cortex.circadian.engine.scheduler import SchedulerMixin
from cortex.circadian.engine.persistence import PersistenceMixin
from cortex.circadian.engine.consolidation import ConsolidationMixin

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter
    from cortex.config import CortexConfig
    from cortex.llm.provider import LLMProvider
    from cortex.identity.cornerstone import CornerstoneGuardian
    from cortex.identity.drift_calculator import DriftCalculator
    from cortex.deprecation.pipeline import DeprecationPipeline
    from cortex.core.entity_resolution import EntityResolver
    from cortex.core.feedback import FeedbackSystem
    from cortex.core.promotion import PromotionScorer
    from cortex.circadian.sleep import SleepConsolidator

logger = logging.getLogger(__name__)


class CircadianEngine(
    ConsolidationMixin,
    FatigueMixin,
    SchedulerMixin,
    PersistenceMixin,
):
    """
    Session lifecycle manager implementing the AWAKE → DROWSY → SLEEPING → DREAMING → AWAKE
    state machine.

    Args:
        storage:              StorageAdapter for persistence.
        config:               CortexConfig with circadian parameters.
        llm:                  LLMProvider for generating summaries.
        cornerstone_guardian: CornerstoneGuardian for loading/checking cornerstones.
    """

    # Class-level guard: signal handlers are process-global, so registering
    # per-instance would double-register when multiple engines are created (#595).
    _signal_registered: bool = False

    def __init__(
        self,
        storage: "StorageAdapter",
        config: "CortexConfig",
        llm: "LLMProvider",
        cornerstone_guardian: "CornerstoneGuardian",
        deprecation_pipeline: Optional["DeprecationPipeline"] = None,
        drift_calculator: Optional["DriftCalculator"] = None,
        entity_resolver: Optional["EntityResolver"] = None,
        feedback_system: Optional["FeedbackSystem"] = None,
        event_emitter: Optional[Any] = None,
        peer_manager: Optional[Any] = None,
        promotion_scorer: Optional["PromotionScorer"] = None,
    ) -> None:
        self.storage = storage
        self.config = config
        self.llm = llm
        self.cornerstone_guardian = cornerstone_guardian
        self.deprecation_pipeline = deprecation_pipeline
        self.drift_calculator = drift_calculator
        self.entity_resolver = entity_resolver
        self.feedback_system = feedback_system
        self.event_emitter = event_emitter
        self.peer_manager = peer_manager
        self.promotion_scorer = promotion_scorer

        # Canonical SleepPipeline — always created (S1→S2→S3→S4 consolidation).
        from cortex.sleep.pipeline import SleepPipeline
        self._sleep_pipeline = SleepPipeline(
            storage=storage,
            llm=llm,
            config=config,
            promotion_scorer=promotion_scorer,
        )

        # Create SleepConsolidator if required deps are available.
        self._consolidator: Optional["SleepConsolidator"] = None
        if deprecation_pipeline is not None and drift_calculator is not None:
            from cortex.circadian.sleep import SleepConsolidator
            self._consolidator = SleepConsolidator(
                storage=storage,
                config=config,
                llm=llm,
                cornerstone_guardian=cornerstone_guardian,
                drift_calculator=drift_calculator,
                deprecation_pipeline=deprecation_pipeline,
                entity_resolver=entity_resolver,
                feedback_system=feedback_system,
                event_emitter=event_emitter,
                peer_manager=peer_manager,
            )

        # Config helpers
        self._nap_threshold: float = getattr(config, "session_fatigue_nap_threshold", 0.7)
        self._sleep_threshold: float = getattr(config, "session_fatigue_sleep_threshold", 0.9)
        self._idle_timeout_sec: int = getattr(config, "dream_cycle_idle_timeout_sec", 3600)
        self._dream_hour_utc: int = getattr(config, "dream_cycle_hour_utc", 8)
        self._dream_enabled: bool = getattr(config, "dream_cycle_enabled", True)
        self._owner_id: str = getattr(config, "owner_id", "default")

        # In-memory state cache: session_id → state
        self._states: dict[str, CircadianState] = {}
        self._states_lock: asyncio.Lock = asyncio.Lock()

        # Last activity timestamps: session_id → epoch seconds (float)
        self._last_activity: dict[str, float] = {}

        # Fatigue interval from config
        _fi = getattr(config, "fatigue_interval_sec", None)
        self._fatigue_interval_sec: int = int(_fi) if isinstance(_fi, (int, float)) else FATIGUE_TIME_INTERVAL_SEC

        # Background task handles
        self._idle_watcher_task: Optional[asyncio.Task] = None
        self._dream_scheduler_task: Optional[asyncio.Task] = None

        # Lifecycle callbacks (hooks)
        self._wake_hooks: list = []
        self._sleep_hooks: list = []

        # Shutdown guard
        self._is_shutdown: bool = False
        self._shutdown_lock: asyncio.Lock = asyncio.Lock()

        # Concurrency guards (#477)
        self._dream_lock: asyncio.Lock = asyncio.Lock()
        self._sleep_lock: asyncio.Lock = asyncio.Lock()

        # Dream cycle tracking (#772)
        self._dream_cycle_last_run: Optional[str] = None
        self._dream_cycle_last_duration_ms: int = 0
        self._dream_cycle_last_ok: bool = True

    # ------------------------------------------------------------------
    # Lifecycle hooks registration
    # ------------------------------------------------------------------

    def on_wake(self, fn) -> None:
        """Register a callable invoked after wake(). Signature: fn(session_id: str)."""
        self._wake_hooks.append(fn)

    def on_sleep(self, fn) -> None:
        """Register a callable invoked after sleep(). Signature: fn(session_id: str)."""
        self._sleep_hooks.append(fn)

    async def _run_hooks(self, hooks: list, session_id: str) -> None:
        for hook in hooks:
            try:
                result = hook(session_id)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as exc:
                logger.warning("Hook %s raised: %s", hook, exc)

    # ------------------------------------------------------------------
    # Session lifecycle — public API
    # ------------------------------------------------------------------

    async def wake(
        self,
        session_id: str,
        owner_id: str = "default",
    ) -> WakeReport:
        """
        Start a new session ("day").

        1. Check for orphaned sessions → run sleep() on them first.
        2. Create session_record with status='awake'.
        3. Load cornerstones.
        4. Retrieve any pending context from last session.
        5. Set fatigue_score = 0.0.
        6. Start background idle watcher.
        7. Fire wake hooks.

        Returns:
            WakeReport with cornerstones + context summary.
        """
        logger.info("[circadian] wake() — session=%s owner=%s", session_id, owner_id)

        # 0. Register signal handlers (idempotent)
        self.register_signal_handlers()

        # 0b. Resume any saved state from previous shutdown (crash recovery)
        await self.resume_saved_state()

        # 1. Orphan recovery
        orphans_recovered = await self._recover_orphans()

        # 1b. Interrupted consolidation recovery
        orphans_recovered += await self._recover_interrupted_sessions(owner_id=owner_id)

        # 2. Create session record
        session = await self.storage.create_session(session_id, owner_id=owner_id)
        logger.debug("[circadian] session_record created: %s status=%s", session_id, session.status)

        # 3. Update in-memory state
        async with self._states_lock:
            self._states[session_id] = CircadianState.AWAKE
        self._last_activity[session_id] = datetime.now(timezone.utc).timestamp()

        # 4. Load cornerstones
        cornerstones = await self.cornerstone_guardian.load_all(owner_id=owner_id)
        cornerstone_tokens = sum(getattr(cs, "token_count", 0) for cs in cornerstones)
        logger.debug("[circadian] cornerstones loaded: %d (%d tokens)", len(cornerstones), cornerstone_tokens)

        # 5. Retrieve pending context (best-effort)
        context_retrieved: Optional[str] = None
        try:
            context_retrieved = await self._retrieve_pending_context(owner_id)
        except Exception as exc:
            logger.warning("[circadian] context retrieval failed: %s", exc)

        # 6. Start background schedulers (idempotent)
        self._ensure_idle_watcher()
        if self._dream_enabled:
            self._ensure_dream_scheduler()

        # 7. Emit SESSION_STARTED event
        await self._emit_event("SESSION_STARTED", {
            "session_id": session_id,
            "cornerstones_loaded": len(cornerstones),
        }, session_id=session_id, owner_id=owner_id)

        # 8. Fire wake hooks
        await self._run_hooks(self._wake_hooks, session_id)

        return WakeReport(
            session_id=session_id,
            cornerstones_loaded=len(cornerstones),
            cornerstone_tokens=cornerstone_tokens,
            context_retrieved=context_retrieved,
            orphans_recovered=orphans_recovered,
        )

    # ------------------------------------------------------------------
    # State accessors
    # ------------------------------------------------------------------

    def get_state(self, session_id: str) -> CircadianState:
        """Return the current in-memory state for a session."""
        return self._states.get(session_id, CircadianState.AWAKE)

    async def touch_activity(self, session_id: str) -> None:
        """Update the last-activity timestamp for a session (reset idle timer)."""
        self._last_activity[session_id] = datetime.now(timezone.utc).timestamp()
