"""
Circadian engine — background scheduler loops (mixin).
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone

from cortex.circadian.engine.models import CircadianState

logger = logging.getLogger(__name__)


class SchedulerMixin:
    """Mixin providing background scheduler management for CircadianEngine."""

    def _ensure_idle_watcher(self) -> None:
        """Start the idle-watcher background task if not already running."""
        if self._idle_watcher_task is None or self._idle_watcher_task.done():
            self._idle_watcher_task = asyncio.create_task(
                self._idle_watcher_loop(),
                name="circadian-idle-watcher",
            )
            logger.debug("[circadian] idle watcher started (timeout=%ds)", self._idle_timeout_sec)

    def _ensure_dream_scheduler(self) -> None:
        """Start the dream-cycle scheduler background task if not already running."""
        if self._dream_scheduler_task is None or self._dream_scheduler_task.done():
            self._dream_scheduler_task = asyncio.create_task(
                self._dream_scheduler_loop(),
                name="circadian-dream-scheduler",
            )
            logger.debug("[circadian] dream scheduler started (hour_utc=%d)", self._dream_hour_utc)

    async def _idle_watcher_loop(self) -> None:
        """
        Background task: poll every 60 seconds and sleep any session that has
        been idle for longer than idle_timeout_sec.
        """
        poll_interval = min(60, self._idle_timeout_sec // 2)
        while True:
            try:
                await asyncio.sleep(poll_interval)
                now = datetime.now(timezone.utc).timestamp()
                # Snapshot to avoid modifying dict during iteration
                active = {
                    sid: ts for sid, ts in self._last_activity.items()
                    if self._states.get(sid) in (CircadianState.AWAKE, CircadianState.DROWSY)
                }
                for session_id, last_ts in active.items():
                    idle_sec = now - last_ts
                    if idle_sec >= self._idle_timeout_sec:
                        logger.info(
                            "[circadian] session %s idle for %.0fs → triggering sleep()",
                            session_id, idle_sec,
                        )
                        try:
                            await self.sleep(session_id, triggered_by="idle_timeout")
                        except Exception as exc:
                            logger.error("[circadian] auto-sleep failed for %s: %s", session_id, exc)
            except asyncio.CancelledError:
                logger.debug("[circadian] idle watcher cancelled")
                break
            except Exception as exc:
                logger.error("[circadian] idle watcher error: %s", exc)
                await asyncio.sleep(5)

    async def _dream_scheduler_loop(self) -> None:
        """
        Background task: fire dream() once per day at the configured UTC hour.
        Waits until next occurrence of dream_cycle_hour_utc, then fires and loops.
        """
        while True:
            try:
                seconds_until_dream = self._seconds_until_dream_hour()
                logger.debug("[circadian] dream scheduler: next dream in %.0fs", seconds_until_dream)
                await asyncio.sleep(seconds_until_dream)
                try:
                    _timeout = getattr(self.config, "dream_cycle_timeout_sec", 600) if self.config else 600
                    await asyncio.wait_for(
                        self.dream(owner_id=self._owner_id),
                        timeout=_timeout,
                    )
                except asyncio.TimeoutError:
                    logger.error(
                        "[circadian] dream cycle timed out after %ds — marking incomplete",
                        _timeout,
                    )
                    self._dream_cycle_last_ok = False
                    self._dream_cycle_last_run = datetime.now(timezone.utc).isoformat()
                except Exception as exc:
                    logger.error("[circadian] dream cycle failed: %s", exc)
                    self._dream_cycle_last_ok = False
                    self._dream_cycle_last_run = datetime.now(timezone.utc).isoformat()
            except asyncio.CancelledError:
                logger.debug("[circadian] dream scheduler cancelled")
                break
            except Exception as exc:
                logger.error("[circadian] dream scheduler error: %s", exc)
                await asyncio.sleep(60)

    def _seconds_until_dream_hour(self) -> float:
        """
        Compute how many seconds until the next occurrence of dream_cycle_hour_utc.
        Always returns a positive number (minimum 1 second).
        """
        now = datetime.now(timezone.utc)
        target_today = now.replace(
            hour=self._dream_hour_utc,
            minute=0,
            second=0,
            microsecond=0,
        )
        if now >= target_today:
            # Already past today's dream hour — schedule for tomorrow
            from datetime import timedelta
            target = target_today + timedelta(days=1)
        else:
            target = target_today
        delta = (target - now).total_seconds()
        return max(1.0, delta)
