#!/usr/bin/env python3
"""
Genesis Circadian Scheduler
============================
Temporal optimization for Genesis cognitive cycles.

Problem (from Gemini analysis):
- No temporal awareness in task scheduling
- Heavy operations run during peak interaction times
- No learning from temporal patterns

Solution:
- Track activity patterns across time periods
- Schedule heavy tasks (consolidation, axiom generation) during low-activity
- Learn optimal timing for different operation types
- Integrate with Heartbeat for intelligent pulsing

Usage:
    from circadian_scheduler import CircadianScheduler
    
    scheduler = CircadianScheduler()
    if scheduler.should_consolidate():
        # Run heavy consolidation
    
    optimal_time = scheduler.next_optimal_window("consolidation")
"""

import json
import time
from pathlib import Path
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict
import statistics


class ActivityLevel(Enum):
    """Activity level classifications."""
    DORMANT = "dormant"      # No activity
    LOW = "low"              # Minimal activity
    MODERATE = "moderate"    # Normal activity
    HIGH = "high"            # Peak activity
    BURST = "burst"          # Intense spike


class TaskType(Enum):
    """Types of scheduled tasks."""
    CONSOLIDATION = "consolidation"      # Memory consolidation
    AXIOM_GENERATION = "axiom_generation"  # Learning compression
    CLEANUP = "cleanup"                   # Data cleanup
    SYNC = "sync"                         # External sync
    HEARTBEAT = "heartbeat"               # Regular pulse
    REFLECTION = "reflection"             # Deep analysis


@dataclass
class TimeSlot:
    """Represents a time period with activity metrics."""
    hour: int  # 0-23
    day_of_week: int  # 0-6 (Monday=0)
    avg_activity: float = 0.0
    interaction_count: int = 0
    task_completions: Dict[str, int] = field(default_factory=dict)
    last_updated: str = ""


@dataclass
class ScheduledTask:
    """A task scheduled for execution."""
    task_type: TaskType
    scheduled_time: datetime
    priority: int = 5  # 1-10
    estimated_duration_seconds: int = 60
    callback: Optional[str] = None  # Function path to call
    metadata: Dict = field(default_factory=dict)


class CircadianScheduler:
    """
    Manages temporal optimization for Genesis operations.
    
    Learns activity patterns and schedules heavy operations
    during low-activity periods.
    """
    
    # Default task preferences (when to run)
    TASK_PREFERENCES = {
        TaskType.CONSOLIDATION: {
            "preferred_activity": ActivityLevel.LOW,
            "min_gap_hours": 4,
            "max_gap_hours": 12,
            "duration_seconds": 300
        },
        TaskType.AXIOM_GENERATION: {
            "preferred_activity": ActivityLevel.LOW,
            "min_gap_hours": 2,
            "max_gap_hours": 8,
            "duration_seconds": 120
        },
        TaskType.CLEANUP: {
            "preferred_activity": ActivityLevel.DORMANT,
            "min_gap_hours": 24,
            "max_gap_hours": 72,
            "duration_seconds": 180
        },
        TaskType.SYNC: {
            "preferred_activity": ActivityLevel.MODERATE,
            "min_gap_hours": 1,
            "max_gap_hours": 4,
            "duration_seconds": 60
        },
        TaskType.HEARTBEAT: {
            "preferred_activity": ActivityLevel.MODERATE,
            "min_gap_hours": 0.1,  # 6 minutes
            "max_gap_hours": 0.5,  # 30 minutes
            "duration_seconds": 30
        },
        TaskType.REFLECTION: {
            "preferred_activity": ActivityLevel.LOW,
            "min_gap_hours": 6,
            "max_gap_hours": 24,
            "duration_seconds": 600
        }
    }
    
    def __init__(self, data_dir: str = "E:/genesis-system/data"):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        
        self.state_file = self.data_dir / "circadian_state.json"
        self.activity_file = self.data_dir / "activity_patterns.json"
        
        # Time slots: [hour][day_of_week] -> TimeSlot
        self.time_slots: Dict[int, Dict[int, TimeSlot]] = defaultdict(dict)
        
        # Task execution history
        self.task_history: Dict[str, List[datetime]] = defaultdict(list)
        
        # Pending scheduled tasks
        self.scheduled_tasks: List[ScheduledTask] = []
        
        # Current activity tracking
        self.current_window_start: datetime = datetime.now()
        self.current_window_interactions: int = 0
        
        self._load_state()
    
    def _load_state(self):
        """Load persisted state."""
        if self.state_file.exists():
            try:
                with open(self.state_file) as f:
                    data = json.load(f)
                    
                # Load task history
                for task_type, times in data.get("task_history", {}).items():
                    self.task_history[task_type] = [
                        datetime.fromisoformat(t) for t in times[-100:]  # Keep last 100
                    ]
                
                # Load scheduled tasks
                for task_data in data.get("scheduled_tasks", []):
                    task = ScheduledTask(
                        task_type=TaskType(task_data["task_type"]),
                        scheduled_time=datetime.fromisoformat(task_data["scheduled_time"]),
                        priority=task_data.get("priority", 5),
                        estimated_duration_seconds=task_data.get("duration", 60),
                        metadata=task_data.get("metadata", {})
                    )
                    if task.scheduled_time > datetime.now():
                        self.scheduled_tasks.append(task)
            except Exception as e:
                print(f"Error loading circadian state: {e}")
        
        if self.activity_file.exists():
            try:
                with open(self.activity_file) as f:
                    data = json.load(f)
                    
                for hour_str, days in data.items():
                    hour = int(hour_str)
                    for day_str, slot_data in days.items():
                        day = int(day_str)
                        self.time_slots[hour][day] = TimeSlot(
                            hour=hour,
                            day_of_week=day,
                            avg_activity=slot_data.get("avg_activity", 0),
                            interaction_count=slot_data.get("interaction_count", 0),
                            task_completions=slot_data.get("task_completions", {}),
                            last_updated=slot_data.get("last_updated", "")
                        )
            except Exception as e:
                print(f"Error loading activity patterns: {e}")
    
    def _save_state(self):
        """Persist state."""
        # Save task history and scheduled tasks
        state_data = {
            "task_history": {
                k: [t.isoformat() for t in v[-100:]]
                for k, v in self.task_history.items()
            },
            "scheduled_tasks": [
                {
                    "task_type": t.task_type.value,
                    "scheduled_time": t.scheduled_time.isoformat(),
                    "priority": t.priority,
                    "duration": t.estimated_duration_seconds,
                    "metadata": t.metadata
                }
                for t in self.scheduled_tasks
                if t.scheduled_time > datetime.now()
            ],
            "last_saved": datetime.now().isoformat()
        }
        
        with open(self.state_file, 'w') as f:
            json.dump(state_data, f, indent=2)
        
        # Save activity patterns
        activity_data = {}
        for hour, days in self.time_slots.items():
            activity_data[str(hour)] = {}
            for day, slot in days.items():
                activity_data[str(hour)][str(day)] = {
                    "avg_activity": slot.avg_activity,
                    "interaction_count": slot.interaction_count,
                    "task_completions": slot.task_completions,
                    "last_updated": slot.last_updated
                }
        
        with open(self.activity_file, 'w') as f:
            json.dump(activity_data, f, indent=2)
    
    def record_interaction(self):
        """Record a user interaction for activity tracking."""
        now = datetime.now()
        self.current_window_interactions += 1
        
        # Update time slot every 15 minutes
        window_duration = (now - self.current_window_start).total_seconds()
        if window_duration >= 900:  # 15 minutes
            self._update_time_slot(
                self.current_window_start.hour,
                self.current_window_start.weekday(),
                self.current_window_interactions
            )
            self.current_window_start = now
            self.current_window_interactions = 0
    
    def _update_time_slot(self, hour: int, day: int, interactions: int):
        """Update activity metrics for a time slot."""
        if day not in self.time_slots[hour]:
            self.time_slots[hour][day] = TimeSlot(hour=hour, day_of_week=day)
        
        slot = self.time_slots[hour][day]
        
        # Exponential moving average for activity
        alpha = 0.3  # Learning rate
        slot.avg_activity = alpha * interactions + (1 - alpha) * slot.avg_activity
        slot.interaction_count += interactions
        slot.last_updated = datetime.now().isoformat()
        
        self._save_state()
    
    def record_task_completion(self, task_type: TaskType):
        """Record that a task was completed."""
        now = datetime.now()
        self.task_history[task_type.value].append(now)
        
        # Update time slot task completions
        hour = now.hour
        day = now.weekday()
        if day not in self.time_slots[hour]:
            self.time_slots[hour][day] = TimeSlot(hour=hour, day_of_week=day)
        
        slot = self.time_slots[hour][day]
        slot.task_completions[task_type.value] = \
            slot.task_completions.get(task_type.value, 0) + 1
        
        self._save_state()
    
    def get_current_activity_level(self) -> ActivityLevel:
        """Get the current activity level."""
        now = datetime.now()
        hour = now.hour
        day = now.weekday()
        
        if day in self.time_slots[hour]:
            avg = self.time_slots[hour][day].avg_activity
        else:
            avg = 0
        
        # Add current window activity
        window_duration = (now - self.current_window_start).total_seconds()
        if window_duration > 0:
            current_rate = self.current_window_interactions / (window_duration / 900)
            avg = (avg + current_rate) / 2
        
        # Classify
        if avg < 0.5:
            return ActivityLevel.DORMANT
        elif avg < 2:
            return ActivityLevel.LOW
        elif avg < 5:
            return ActivityLevel.MODERATE
        elif avg < 10:
            return ActivityLevel.HIGH
        else:
            return ActivityLevel.BURST
    
    def get_activity_forecast(self, hours_ahead: int = 24) -> List[Tuple[datetime, ActivityLevel]]:
        """Forecast activity levels for the next N hours."""
        forecast = []
        now = datetime.now()
        
        for h in range(hours_ahead):
            future = now + timedelta(hours=h)
            hour = future.hour
            day = future.weekday()
            
            if day in self.time_slots[hour]:
                avg = self.time_slots[hour][day].avg_activity
            else:
                # No data - assume moderate
                avg = 3
            
            if avg < 0.5:
                level = ActivityLevel.DORMANT
            elif avg < 2:
                level = ActivityLevel.LOW
            elif avg < 5:
                level = ActivityLevel.MODERATE
            else:
                level = ActivityLevel.HIGH
            
            forecast.append((future, level))
        
        return forecast
    
    def should_run_task(self, task_type: TaskType) -> Tuple[bool, str]:
        """
        Determine if a task should run now.
        
        Returns (should_run, reason).
        """
        prefs = self.TASK_PREFERENCES.get(task_type, {})
        preferred_activity = prefs.get("preferred_activity", ActivityLevel.MODERATE)
        min_gap = prefs.get("min_gap_hours", 1)
        max_gap = prefs.get("max_gap_hours", 24)
        
        # Check last execution
        history = self.task_history.get(task_type.value, [])
        if history:
            last_run = history[-1]
            hours_since = (datetime.now() - last_run).total_seconds() / 3600
            
            if hours_since < min_gap:
                return False, f"Too soon (ran {hours_since:.1f}h ago, min gap: {min_gap}h)"
            
            if hours_since > max_gap:
                return True, f"Overdue (last ran {hours_since:.1f}h ago, max gap: {max_gap}h)"
        else:
            # Never run - should run
            return True, "Never executed before"
        
        # Check current activity
        current = self.get_current_activity_level()
        
        # Activity level ordering for comparison
        level_order = [
            ActivityLevel.DORMANT,
            ActivityLevel.LOW,
            ActivityLevel.MODERATE,
            ActivityLevel.HIGH,
            ActivityLevel.BURST
        ]
        
        current_idx = level_order.index(current)
        preferred_idx = level_order.index(preferred_activity)
        
        if current_idx <= preferred_idx:
            # PATENT COMPLIANCE: Heavy tasks must run in 2-6 AM window
            if task_type in [TaskType.CONSOLIDATION, TaskType.AXIOM_GENERATION, TaskType.REFLECTION]:
                current_hour = datetime.now().hour
                if 2 <= current_hour < 6:
                    return True, f"Activity level appropriate and within 2-6 AM window ({current.value})"
                else:
                    return False, f"Outside 2-6 AM window (current hour: {current_hour})"
            
            return True, f"Activity level appropriate ({current.value} <= {preferred_activity.value})"
        
        return False, f"Activity too high ({current.value} > {preferred_activity.value})"
    
    def next_optimal_window(self, task_type: TaskType, hours_ahead: int = 24) -> Optional[datetime]:
        """Find the next optimal time window for a task."""
        prefs = self.TASK_PREFERENCES.get(task_type, {})
        preferred_activity = prefs.get("preferred_activity", ActivityLevel.MODERATE)
        
        forecast = self.get_activity_forecast(hours_ahead)
        
        # Activity level ordering
        level_order = [
            ActivityLevel.DORMANT,
            ActivityLevel.LOW,
            ActivityLevel.MODERATE,
            ActivityLevel.HIGH,
            ActivityLevel.BURST
        ]
        preferred_idx = level_order.index(preferred_activity)
        
        # Find first window that matches or is better than preferred
        for future_time, level in forecast:
            level_idx = level_order.index(level)
            if level_idx <= preferred_idx:
                return future_time
        
        # No optimal window found - return time with lowest activity
        best_time = min(forecast, key=lambda x: level_order.index(x[1]))
        return best_time[0]
    
    def schedule_task(self, task_type: TaskType, 
                     when: Optional[datetime] = None,
                     priority: int = 5,
                     callback: Optional[str] = None) -> ScheduledTask:
        """Schedule a task for execution."""
        if when is None:
            when = self.next_optimal_window(task_type)
        
        prefs = self.TASK_PREFERENCES.get(task_type, {})
        duration = prefs.get("duration_seconds", 60)
        
        task = ScheduledTask(
            task_type=task_type,
            scheduled_time=when,
            priority=priority,
            estimated_duration_seconds=duration,
            callback=callback
        )
        
        self.scheduled_tasks.append(task)
        self.scheduled_tasks.sort(key=lambda t: (t.scheduled_time, -t.priority))
        
        self._save_state()
        return task

    def schedule_heavy_task(self, task_type: TaskType, priority: int = 8) -> ScheduledTask:
        """
        Schedule a heavy task for the next 2-6 AM window.
        
        Compliant with Patent Axiom: Circadian Optimization.
        """
        now = datetime.now()
        # Find next 2 AM
        target = now.replace(hour=2, minute=0, second=0, microsecond=0)
        if target <= now:
            target += timedelta(days=1)
            
        return self.schedule_task(task_type, when=target, priority=priority)
    
    def get_due_tasks(self) -> List[ScheduledTask]:
        """Get tasks that are due for execution."""
        now = datetime.now()
        due = []
        remaining = []
        
        for task in self.scheduled_tasks:
            if task.scheduled_time <= now:
                due.append(task)
            else:
                remaining.append(task)
        
        self.scheduled_tasks = remaining
        return due
    
    def get_schedule_summary(self) -> Dict:
        """Get a summary of the current schedule."""
        now = datetime.now()
        
        summary = {
            "current_time": now.isoformat(),
            "current_activity": self.get_current_activity_level().value,
            "pending_tasks": len(self.scheduled_tasks),
            "task_history": {},
            "upcoming": [],
            "activity_forecast_4h": []
        }
        
        # Task history summary
        for task_type in TaskType:
            history = self.task_history.get(task_type.value, [])
            if history:
                last = history[-1]
                hours_ago = (now - last).total_seconds() / 3600
                summary["task_history"][task_type.value] = {
                    "last_run": last.isoformat(),
                    "hours_ago": round(hours_ago, 1),
                    "total_runs": len(history)
                }
        
        # Upcoming tasks
        for task in self.scheduled_tasks[:5]:
            summary["upcoming"].append({
                "type": task.task_type.value,
                "scheduled": task.scheduled_time.isoformat(),
                "priority": task.priority
            })
        
        # 4-hour forecast
        forecast = self.get_activity_forecast(4)
        for time, level in forecast:
            summary["activity_forecast_4h"].append({
                "time": time.strftime("%H:%M"),
                "level": level.value
            })
        
        return summary
    
    # Convenience methods for common tasks
    def should_consolidate(self) -> bool:
        """Should memory consolidation run now?"""
        should, _ = self.should_run_task(TaskType.CONSOLIDATION)
        return should
    
    def should_generate_axioms(self) -> bool:
        """Should axiom generation run now?"""
        should, _ = self.should_run_task(TaskType.AXIOM_GENERATION)
        return should
    
    def should_heartbeat(self) -> bool:
        """Should heartbeat pulse now?"""
        should, _ = self.should_run_task(TaskType.HEARTBEAT)
        return should


# CLI Interface
if __name__ == "__main__":
    import sys
    
    scheduler = CircadianScheduler()
    
    if len(sys.argv) < 2:
        print("""
Genesis Circadian Scheduler
============================

Commands:
  status              Show current schedule status
  activity            Show current activity level
  forecast [hours]    Show activity forecast (default: 24h)
  should <task>       Check if task should run now
  schedule <task>     Schedule a task for optimal time
  interact            Record a user interaction

Task types: consolidation, axiom_generation, cleanup, sync, heartbeat, reflection
        """)
        sys.exit(0)
    
    command = sys.argv[1]
    
    if command == "status":
        summary = scheduler.get_schedule_summary()
        print(f"\n{'='*60}")
        print(f"Circadian Scheduler Status")
        print(f"{'='*60}")
        print(f"Current time: {summary['current_time']}")
        print(f"Activity level: {summary['current_activity']}")
        print(f"Pending tasks: {summary['pending_tasks']}")
        
        print(f"\nTask History:")
        for task, info in summary['task_history'].items():
            print(f"  {task}: {info['hours_ago']}h ago ({info['total_runs']} total)")
        
        print(f"\nUpcoming Tasks:")
        for task in summary['upcoming']:
            print(f"  [{task['priority']}] {task['type']} @ {task['scheduled']}")
        
        print(f"\n4-Hour Forecast:")
        for slot in summary['activity_forecast_4h']:
            print(f"  {slot['time']}: {slot['level']}")
    
    elif command == "activity":
        level = scheduler.get_current_activity_level()
        print(f"Current activity: {level.value}")
    
    elif command == "forecast":
        hours = int(sys.argv[2]) if len(sys.argv) > 2 else 24
        forecast = scheduler.get_activity_forecast(hours)
        print(f"\nActivity Forecast ({hours}h):")
        for time, level in forecast:
            print(f"  {time.strftime('%Y-%m-%d %H:%M')}: {level.value}")
    
    elif command == "should" and len(sys.argv) > 2:
        task_name = sys.argv[2].upper()
        try:
            task_type = TaskType[task_name]
            should, reason = scheduler.should_run_task(task_type)
            print(f"{task_type.value}: {'YES' if should else 'NO'}")
            print(f"Reason: {reason}")
        except KeyError:
            print(f"Unknown task: {task_name}")
            print(f"Valid tasks: {[t.name for t in TaskType]}")
    
    elif command == "schedule" and len(sys.argv) > 2:
        task_name = sys.argv[2].upper()
        try:
            task_type = TaskType[task_name]
            task = scheduler.schedule_task(task_type)
            print(f"Scheduled {task_type.value} for {task.scheduled_time}")
        except KeyError:
            print(f"Unknown task: {task_name}")
    
    elif command == "interact":
        scheduler.record_interaction()
        print("Interaction recorded")
    
    else:
        print(f"Unknown command: {command}")
