"""
Circadian engine — sleep and dream consolidation logic (mixin).
"""

from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from typing import Optional

from cortex.circadian.engine.models import (
    CircadianState,
    DreamReport,
    SleepReport,
)

logger = logging.getLogger(__name__)


class ConsolidationMixin:
    """Mixin providing sleep() and dream() consolidation methods for CircadianEngine."""

    async def sleep(
        self,
        session_id: str,
        *,
        triggered_by: str = "idle_timeout",
    ) -> SleepReport:
        """
        End-of-session consolidation trigger.

        M6a responsibility: transition state SLEEPING, log the event, fire sleep hooks.
        Full consolidation (memory dump, deprecation review, etc.) is implemented in M6b.

        Args:
            session_id:    The session to sleep.
            triggered_by:  Reason string logged to session_record ('idle_timeout',
                           'manual', 'dream_cycle').

        Returns:
            SleepReport (stub — consolidation fields populated by M6b).
        """
        logger.info("[circadian] sleep() — session=%s trigger=%s", session_id, triggered_by)
        start_ts = datetime.now(timezone.utc)

        # Concurrency guard — reject duplicate concurrent sleep() calls (#477).
        if self._sleep_lock.locked():
            logger.warning(
                "[circadian] sleep() concurrent call rejected for session=%s — already sleeping",
                session_id,
            )
            return SleepReport(session_id=session_id)

        async with self._sleep_lock:
            session = await self._get_session(session_id)
            if session is None:
                logger.warning("[circadian] sleep: session %s not found", session_id)
                return SleepReport(session_id=session_id)

            # Transition: → SLEEPING
            async with self._states_lock:
                self._states[session_id] = CircadianState.SLEEPING
            await self.storage.update_session(
                session_id,
                status=CircadianState.SLEEPING.value,
            )

            # Emit SESSION_ENDED event
            await self._emit_event("SESSION_ENDED", {
                "session_id": session_id,
                "triggered_by": triggered_by,
            }, session_id=session_id)

            # Fire sleep hooks
            await self._run_hooks(self._sleep_hooks, session_id)

            # Delegate actual sleep work to the canonical SleepPipeline (S1→S2→S3→S4).
            pipeline_report = None
            try:
                pipeline_report = await self._sleep_pipeline.run(
                    session_id=session_id,
                    owner_id=self._owner_id,
                )
                if pipeline_report.errors:
                    logger.warning(
                        "[circadian] sleep pipeline finished with stage errors session=%s errors=%s",
                        session_id,
                        list(pipeline_report.errors.keys()),
                    )
                else:
                    logger.info(
                        "[circadian] sleep pipeline complete session=%s promoted=%d",
                        session_id,
                        pipeline_report.claims_promoted,
                    )
            except Exception as exc:
                logger.error("[circadian] sleep pipeline failed for %s: %s", session_id, exc)

            # Mark ended_at and compute duration
            end_ts = datetime.now(timezone.utc)
            session_start = datetime.fromisoformat(session.started_at)
            if session_start.tzinfo is None:
                session_start = session_start.replace(tzinfo=timezone.utc)
            duration_ms = int((end_ts - session_start).total_seconds() * 1000)

            await self.storage.update_session(
                session_id,
                ended_at=end_ts.isoformat(),
                status=CircadianState.SLEEPING.value,
            )

            # Emit SLEEP_COMPLETED event
            await self._emit_event("SLEEP_COMPLETED", {
                "session_id": session_id,
                "duration_ms": duration_ms,
                "triggered_by": triggered_by,
            }, session_id=session_id)

            logger.info("[circadian] session %s is now SLEEPING (duration=%dms)", session_id, duration_ms)

            # Build report, merging SleepPipeline results where available
            report = SleepReport(
                session_id=session_id,
                duration_ms=duration_ms,
            )
            if pipeline_report is not None:
                report.claims_promoted = pipeline_report.claims_promoted
                report.summary = pipeline_report.s1.summary if pipeline_report.s1 else ""

            return report

    async def dream(self, owner_id: str = "default") -> DreamReport:
        """
        Nightly deep consolidation trigger (Electric Sheep).

        M6a responsibility: transition to DREAMING state, recover orphaned sessions,
        log the dream cycle start. Full dream logic (consolidation, decay, etc.) is M6b.

        Returns:
            DreamReport (stub — detailed fields populated by M6b).
        """
        logger.info("[circadian] dream() — owner=%s", owner_id)
        start_ts = datetime.now(timezone.utc)

        # Concurrency guard — reject duplicate concurrent dream() calls (#477).
        if self._dream_lock.locked():
            logger.warning(
                "[circadian] dream() concurrent call rejected for owner=%s — already dreaming",
                owner_id,
            )
            return DreamReport(
                owner_id=owner_id,
                journal="concurrent dream rejected — dream cycle already in progress",
            )

        async with self._dream_lock:
            # Recover orphaned sessions first
            orphans_recovered = await self._recover_orphans(owner_id=owner_id)

            # Also recover sessions interrupted mid-consolidation
            orphans_recovered += await self._recover_interrupted_sessions(owner_id=owner_id)

            # Run SleepConsolidator dream cycle if available (M6b full consolidation)
            consolidator_report = None
            if self._consolidator is not None:
                try:
                    consolidator_report = await self._consolidator.dream(owner_id)
                    logger.info(
                        "[circadian] consolidator dream complete owner=%s "
                        "consolidated=%d contradictions=%d",
                        owner_id,
                        consolidator_report.claims_consolidated,
                        consolidator_report.contradictions_resolved,
                    )
                except Exception as exc:
                    logger.error("[circadian] consolidator dream failed for owner=%s: %s", owner_id, exc)

            # WAL checkpoint after consolidation — prevent unbounded WAL growth (#490)
            try:
                result = await self.storage._db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
                logger.info("[circadian] WAL checkpoint (TRUNCATE) complete owner=%s result=%s", owner_id, result)
            except Exception as exc:
                logger.warning("[circadian] WAL checkpoint failed (non-fatal) owner=%s: %s", owner_id, exc)

            end_ts = datetime.now(timezone.utc)
            duration_ms = int((end_ts - start_ts).total_seconds() * 1000)

            # Build report: merge consolidator data if available
            if consolidator_report is not None:
                report = DreamReport(
                    owner_id=owner_id,
                    duration_ms=consolidator_report.duration_ms or duration_ms,
                    orphans_recovered=orphans_recovered + consolidator_report.orphans_recovered,
                    claims_consolidated=consolidator_report.claims_consolidated,
                    contradictions_resolved=consolidator_report.contradictions_resolved,
                    claims_decayed=consolidator_report.claims_decayed,
                    claims_archived=consolidator_report.claims_archived,
                    cornerstone_proposals=consolidator_report.cornerstone_proposals,
                    entity_merge_suggestions=consolidator_report.entity_merge_suggestions,
                    brain_graphs_refreshed=consolidator_report.brain_graphs_refreshed,
                    feedback_applied=consolidator_report.feedback_applied,
                    journal=consolidator_report.journal,
                )
            else:
                journal = (
                    f"[M6a] Dream cycle triggered at {start_ts.isoformat()} UTC. "
                    f"Orphans recovered: {orphans_recovered}. "
                    "Full consolidation pending M6b."
                )
                report = DreamReport(
                    owner_id=owner_id,
                    duration_ms=duration_ms,
                    orphans_recovered=orphans_recovered,
                    journal=journal,
                )

            # Emit DREAM_COMPLETED event
            await self._emit_event("DREAM_COMPLETED", {
                "duration_ms": report.duration_ms,
                "orphans_recovered": orphans_recovered,
                "claims_consolidated": report.claims_consolidated,
            }, owner_id=owner_id)

            # Persist dream cycle completion timestamp (#772)
            self._dream_cycle_last_run = report.completed_at
            self._dream_cycle_last_duration_ms = report.duration_ms
            self._dream_cycle_last_ok = True
            try:
                await self.storage.set_config(
                    "last_dream_cycle",
                    json.dumps({
                        "completed_at": report.completed_at,
                        "duration_ms": report.duration_ms,
                        "owner_id": owner_id,
                        "orphans_recovered": report.orphans_recovered,
                        "claims_consolidated": report.claims_consolidated,
                    }),
                )
            except Exception as exc:
                logger.warning("[circadian] failed to persist dream timestamp: %s", exc)

            logger.info("[circadian] dream cycle complete owner=%s orphans=%d",
                        owner_id, orphans_recovered)

            return report

    # ------------------------------------------------------------------
    # Manual trigger API
    # ------------------------------------------------------------------

    async def force_sleep(self, session_id: str) -> SleepReport:
        """Force sleep on a specific session (manual trigger)."""
        return await self.sleep(session_id, triggered_by="manual")

    async def force_dream(self, owner_id: str = "default") -> DreamReport:
        """Force dream cycle immediately (manual trigger)."""
        logger.info("[circadian] force_dream() triggered manually for owner=%s", owner_id)
        return await self.dream(owner_id=owner_id)

    # ------------------------------------------------------------------
    # Event emission helper
    # ------------------------------------------------------------------

    async def _emit_event(
        self,
        event_type_name: str,
        payload: dict,
        session_id: Optional[str] = None,
        owner_id: Optional[str] = None,
    ) -> None:
        """Emit an event via event_emitter if available (best-effort)."""
        if self.event_emitter is None:
            return
        try:
            from cortex.events.emitter import CortexEvent, EventType
            event_type = getattr(EventType, event_type_name, None)
            if event_type is None:
                return
            kwargs: dict = {
                "event_type": event_type,
                "owner_id": owner_id or self._owner_id,
                "payload": payload,
            }
            if session_id is not None:
                kwargs["session_id"] = session_id
            await self.event_emitter.emit(CortexEvent(**kwargs))
        except Exception as exc:
            logger.debug("[circadian] event emit failed (%s): %s", event_type_name, exc)
