# autonomous_engine.py
"""
Truly autonomous decision-making engine for Miku.
Makes decisions based on context signals without constant LLM polling.
"""

import math
import time
import random
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from collections import deque
import discord
from .autonomous_persistence import save_autonomous_context, load_autonomous_context, apply_context_to_signals
from utils.logger import get_logger

logger = get_logger('autonomous')

@dataclass
class ContextSignals:
    """Lightweight context tracking without storing message content"""
    # Activity metrics
    messages_last_5min: int = 0
    messages_last_hour: int = 0
    unique_users_active: int = 0
    conversation_momentum: float = 0.0  # 0-1 score based on message frequency
    
    # User presence
    users_joined_recently: int = 0
    users_status_changed: int = 0
    users_started_activity: List[tuple] = field(default_factory=list)  # (activity_name, timestamp) tuples
    
    # Miku's state
    time_since_last_action: float = 0.0  # seconds
    time_since_last_interaction: float = 0.0  # seconds since someone talked to her
    messages_since_last_appearance: int = 0
    
    # Time context
    hour_of_day: int = 0
    is_weekend: bool = False
    
    # Emotional influence
    current_mood: str = "neutral"
    mood_energy_level: float = 0.5  # 0-1, affects likelihood of action


@dataclass
class ActionThresholds:
    """Dynamic thresholds that change based on mood and context"""
    # How long to wait before considering action (seconds)
    min_silence_for_general: float = 1800  # 30 min
    min_silence_for_engagement: float = 3600  # 1 hour
    
    # Activity level needed to join conversation (0-1)
    conversation_join_threshold: float = 0.6
    
    # How many messages before feeling "left out"
    messages_before_fomo: int = 25
    
    # Mood-based multipliers
    mood_action_multiplier: float = 1.0  # Higher = more likely to act


class AutonomousEngine:
    """
    Decision engine that determines WHEN Miku should act,
    then delegates to existing autonomous functions for WHAT to do.
    """
    
    def __init__(self):
        self.server_contexts: Dict[int, ContextSignals] = {}
        self.server_message_times: Dict[int, deque] = {}  # Track message timestamps
        self.server_last_action: Dict[int, float] = {}
        self.bot_startup_time: float = time.time()  # Track when bot started
        
        # Mood personality profiles
        self.mood_profiles = {
            "bubbly": {"energy": 0.9, "sociability": 0.95, "impulsiveness": 0.8},
            "sleepy": {"energy": 0.2, "sociability": 0.3, "impulsiveness": 0.1},
            "curious": {"energy": 0.7, "sociability": 0.6, "impulsiveness": 0.7},
            "shy": {"energy": 0.4, "sociability": 0.2, "impulsiveness": 0.2},
            "serious": {"energy": 0.6, "sociability": 0.5, "impulsiveness": 0.3},
            "excited": {"energy": 0.95, "sociability": 0.9, "impulsiveness": 0.9},
            "silly": {"energy": 0.8, "sociability": 0.85, "impulsiveness": 0.95},
            "melancholy": {"energy": 0.3, "sociability": 0.4, "impulsiveness": 0.2},
            "flirty": {"energy": 0.75, "sociability": 0.85, "impulsiveness": 0.7},
            "romantic": {"energy": 0.6, "sociability": 0.7, "impulsiveness": 0.5},
            "irritated": {"energy": 0.5, "sociability": 0.3, "impulsiveness": 0.6},
            "angry": {"energy": 0.7, "sociability": 0.2, "impulsiveness": 0.8},
            "neutral": {"energy": 0.5, "sociability": 0.5, "impulsiveness": 0.5},
            "asleep": {"energy": 0.0, "sociability": 0.0, "impulsiveness": 0.0},
        }
        
        # Load persisted context on initialization
        self._load_persisted_context()
    
    def _load_persisted_context(self):
        """Load saved context data on bot startup"""
        context_data, last_action = load_autonomous_context()
        
        # Restore last action timestamps
        self.server_last_action = last_action
        
        # Restore context signals
        for guild_id, data in context_data.items():
            self.server_contexts[guild_id] = ContextSignals()
            self.server_message_times[guild_id] = deque(maxlen=100)
            apply_context_to_signals(data, self.server_contexts[guild_id])
    
    def save_context(self):
        """Save current context to disk"""
        save_autonomous_context(self.server_contexts, self.server_last_action)
    
    def track_message(self, guild_id: int, author_is_bot: bool = False):
        """Track a message without storing content"""
        if guild_id not in self.server_contexts:
            self.server_contexts[guild_id] = ContextSignals()
            self.server_message_times[guild_id] = deque(maxlen=100)
        
        if author_is_bot:
            return  # Don't count bot messages
        
        now = time.time()
        self.server_message_times[guild_id].append(now)
        
        ctx = self.server_contexts[guild_id]
        ctx.messages_since_last_appearance += 1
        
        # Cap at 100 to prevent massive buildup during sleep/inactivity
        # This prevents inappropriate FOMO triggers after long periods
        if ctx.messages_since_last_appearance > 100:
            ctx.messages_since_last_appearance = 100
        
        # Update time-based metrics
        self._update_activity_metrics(guild_id)
    
    def track_user_event(self, guild_id: int, event_type: str, data: dict = None):
        """Track user presence events (joins, status changes, etc.)"""
        if guild_id not in self.server_contexts:
            self.server_contexts[guild_id] = ContextSignals()
            self.server_message_times[guild_id] = deque(maxlen=100)
        
        ctx = self.server_contexts[guild_id]
        
        if event_type == "user_joined":
            ctx.users_joined_recently += 1
        elif event_type == "status_changed":
            ctx.users_status_changed += 1
        elif event_type == "activity_started" and data:
            activity_name = data.get("activity_name")
            if activity_name:
                now = time.time()
                
                # Remove duplicate activities (same name)
                ctx.users_started_activity = [
                    (name, ts) for name, ts in ctx.users_started_activity 
                    if name != activity_name
                ]
                
                # Add new activity with timestamp
                ctx.users_started_activity.append((activity_name, now))
                
                # Keep only last 5 activities
                if len(ctx.users_started_activity) > 5:
                    ctx.users_started_activity.pop(0)
    
    def _clean_old_activities(self, guild_id: int, max_age_seconds: float = 3600):
        """Remove activities older than max_age (default 1 hour)"""
        if guild_id not in self.server_contexts:
            return
        
        ctx = self.server_contexts[guild_id]
        now = time.time()
        
        # Filter out old activities
        ctx.users_started_activity = [
            (name, ts) for name, ts in ctx.users_started_activity
            if now - ts < max_age_seconds
        ]
    
    def update_mood(self, guild_id: int, mood: str):
        """Update mood and recalculate energy level"""
        if guild_id not in self.server_contexts:
            self.server_contexts[guild_id] = ContextSignals()
            self.server_message_times[guild_id] = deque(maxlen=100)
        
        ctx = self.server_contexts[guild_id]
        ctx.current_mood = mood
        
        # Get mood personality profile
        profile = self.mood_profiles.get(mood, self.mood_profiles["neutral"])
        ctx.mood_energy_level = profile["energy"]
    
    def _update_activity_metrics(self, guild_id: int):
        """Update activity metrics based on message timestamps"""
        ctx = self.server_contexts[guild_id]
        times = self.server_message_times[guild_id]
        
        now = time.time()
        
        # Count messages in time windows
        ctx.messages_last_5min = sum(1 for t in times if now - t < 300)
        ctx.messages_last_hour = sum(1 for t in times if now - t < 3600)
        
        # Calculate conversation momentum (0-1 scale)
        # Smooth curve: grows quickly at first, then tapers off toward 1.0
        # 1 msg → 0.10, 5 msgs → 0.41, 10 msgs → 0.63, 20 msgs → 0.82, 40 msgs → 0.95
        if ctx.messages_last_5min == 0:
            ctx.conversation_momentum = 0.0
        else:
            ctx.conversation_momentum = min(1.0, math.log1p(ctx.messages_last_5min) / math.log1p(30))
        
        # Time since last action
        if guild_id in self.server_last_action:
            ctx.time_since_last_action = now - self.server_last_action[guild_id]
        else:
            ctx.time_since_last_action = float('inf')
        
        # Time context
        ctx.hour_of_day = datetime.now().hour
        ctx.is_weekend = datetime.now().weekday() >= 5
    
    def should_take_action(self, guild_id: int, debug: bool = False, triggered_by_message: bool = False) -> Optional[str]:
        """
        Determine if Miku should take action and what type.
        Returns action type or None.
        
        This is the CORE decision logic - no LLM needed!
        
        Args:
            guild_id: Server ID
            debug: If True, print detailed decision reasoning
            triggered_by_message: If True, this check was triggered immediately after someone sent a message
        """
        if guild_id not in self.server_contexts:
            return None
        
        ctx = self.server_contexts[guild_id]
        
        # STARTUP COOLDOWN: Don't act for first 2 minutes after bot startup
        # This prevents rapid-fire messages when bot restarts
        time_since_startup = time.time() - self.bot_startup_time
        if time_since_startup < 120:  # 2 minutes
            if debug:
                logger.debug(f"[V2 Debug] Startup cooldown active ({time_since_startup:.0f}s / 120s)")
            return None
        
        # Never act when asleep
        if ctx.current_mood == "asleep":
            if debug:
                logger.debug(f"[V2 Debug] Mood is 'asleep' - no action taken")
            return None
        
        # Get mood personality
        profile = self.mood_profiles.get(ctx.current_mood, self.mood_profiles["neutral"])
        
        # Update metrics
        self._update_activity_metrics(guild_id)
        
        if debug:
            logger.debug(f"\n[V2 Debug] Decision Check for Guild {guild_id}")
            logger.debug(f"   Triggered by message: {triggered_by_message}")
            logger.debug(f"   Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
            logger.debug(f"   Momentum: {ctx.conversation_momentum:.2f}")
            logger.debug(f"   Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
            logger.debug(f"   Messages since appearance: {ctx.messages_since_last_appearance}")
            logger.debug(f"   Time since last action: {ctx.time_since_last_action:.0f}s")
            logger.debug(f"   Active activities: {len(ctx.users_started_activity)}")
        
        # --- Decision Logic ---
        
        # CRITICAL: If triggered by a message, we should ONLY do join_conversation
        # This ensures Miku responds to what's being said, not random autonomous actions
        # Exception: Reactions are handled separately and are allowed
        
        # 1. CONVERSATION JOIN (high priority when momentum is high)
        if self._should_join_conversation(ctx, profile, debug):
            if debug:
                logger.debug(f"[V2 Debug] DECISION: join_conversation")
            return "join_conversation"
        
        # 2. USER ENGAGEMENT (someone interesting appeared)
        if self._should_engage_user(ctx, profile, debug):
            if triggered_by_message:
                # Convert to join_conversation when message-triggered
                if debug:
                    logger.debug(f"[V2 Debug] DECISION: join_conversation (engage_user converted due to message trigger)")
                return "join_conversation"
            if debug:
                logger.debug(f"[V2 Debug] DECISION: engage_user")
            return "engage_user"
        
        # 3. FOMO RESPONSE (lots of activity without her)
        # When FOMO triggers, join the conversation instead of saying something random
        if self._should_respond_to_fomo(ctx, profile, debug):
            if debug:
                logger.debug(f"[V2 Debug] DECISION: join_conversation (FOMO)")
            return "join_conversation"  # Jump in and respond to what's being said
        
        # 4. BORED/LONELY (quiet for too long, depending on mood)
        # CRITICAL FIX: If this check was triggered by a message, convert "general" to "join_conversation"
        # This ensures Miku responds to the message instead of saying something random
        if self._should_break_silence(ctx, profile, debug):
            if triggered_by_message:
                if debug:
                    logger.debug(f"[V2 Debug] DECISION: join_conversation (break silence, but message just sent)")
                return "join_conversation"  # Respond to the message instead of random general statement
            else:
                if debug:
                    logger.debug(f"[V2 Debug] DECISION: general (break silence)")
                return "general"
        
        # 5. SHARE TWEET (low activity, wants to share something)
        # Skip this entirely when triggered by message - would be inappropriate to ignore user's message
        if not triggered_by_message and self._should_share_content(ctx, profile, debug):
            if debug:
                logger.debug(f"[V2 Debug] DECISION: share_tweet")
            return "share_tweet"
        
        # 6. CHANGE PROFILE PICTURE (very rare, once per day)
        # Skip this entirely when triggered by message
        if not triggered_by_message and self._should_change_profile_picture(ctx, profile, debug):
            if debug:
                logger.debug(f"[V2 Debug] DECISION: change_profile_picture")
            return "change_profile_picture"
        
        if debug:
            logger.debug(f"[V2 Debug] DECISION: None (no conditions met)")
        
        return None
    
    def _should_join_conversation(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """Decide if Miku should join an active conversation"""
        # High conversation momentum + sociable mood + hasn't spoken recently
        base_threshold = 0.6
        mood_adjusted = base_threshold * (2.0 - profile["sociability"])  # Lower threshold if sociable
        
        conditions = {
            "momentum_check": ctx.conversation_momentum > mood_adjusted,
            "messages_check": ctx.messages_since_last_appearance >= 5,
            "cooldown_check": ctx.time_since_last_action > 300,
            "impulsiveness_roll": random.random() < profile["impulsiveness"]
        }
        
        result = all(conditions.values())
        
        if debug:
            logger.debug(f"   [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}")
            logger.debug(f"   [Join Conv] messages={ctx.messages_since_last_appearance} >= 5? {conditions['messages_check']}")
            logger.debug(f"   [Join Conv] cooldown={ctx.time_since_last_action:.0f}s > 300s? {conditions['cooldown_check']}")
            logger.debug(f"   [Join Conv] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
        
        return result
    
    def _should_engage_user(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """Decide if Miku should engage with a user (status change/activity)"""
        # Someone started a new activity or status changed + enough time passed
        has_activities = len(ctx.users_started_activity) > 0
        cooldown_ok = ctx.time_since_last_action > 1800
        roll = random.random()
        threshold = profile["sociability"] * profile["impulsiveness"]
        roll_ok = roll < threshold
        
        result = has_activities and cooldown_ok and roll_ok
        
        if debug and has_activities:
            activities = [name for name, ts in ctx.users_started_activity]
            logger.debug(f"   [Engage] activities={activities}, cooldown={ctx.time_since_last_action:.0f}s > 1800s? {cooldown_ok}")
            logger.debug(f"   [Engage] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
        
        return result
    
    def _should_respond_to_fomo(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """Decide if Miku feels left out (FOMO)"""
        # Lots of messages but she hasn't participated
        fomo_threshold = 25 * (2.0 - profile["sociability"])  # Social moods have lower threshold
        
        msgs_check = ctx.messages_since_last_appearance > fomo_threshold
        momentum_check = ctx.conversation_momentum > 0.3
        cooldown_check = ctx.time_since_last_action > 900
        
        result = msgs_check and momentum_check and cooldown_check
        
        if debug:
            logger.debug(f"   [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}")
            logger.debug(f"   [FOMO] momentum={ctx.conversation_momentum:.2f} > 0.3? {momentum_check}")
            logger.debug(f"   [FOMO] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
        
        return result
    
    def _should_break_silence(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """Decide if Miku should break a long silence"""
        # Low activity + long time + mood-dependent
        min_silence = 1800 * (2.0 - profile["energy"])  # High energy = shorter wait
        
        quiet_check = ctx.messages_last_hour < 5
        silence_check = ctx.time_since_last_action > min_silence
        energy_roll = random.random()
        energy_ok = energy_roll < profile["energy"]
        
        result = quiet_check and silence_check and energy_ok
        
        if debug:
            logger.debug(f"   [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
            logger.debug(f"   [Silence] time={ctx.time_since_last_action:.0f}s > {min_silence:.0f}s? {silence_check}")
            logger.debug(f"   [Silence] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
        
        return result
    
    def _should_share_content(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """Decide if Miku should share a tweet/content"""
        # RELAXED CONDITIONS: Made tweet sharing more frequent
        # Old: quiet_check required < 10 messages, now < 20
        # Old: cooldown was 3600s (1 hour), now 2400s (40 minutes)
        # Old: energy threshold was 50%, now 70%
        quiet_check = ctx.messages_last_hour < 20  # Increased from 10
        cooldown_check = ctx.time_since_last_action > 2400  # Reduced from 3600
        energy_roll = random.random()
        energy_threshold = profile["energy"] * 0.7  # Increased from 0.5
        energy_ok = energy_roll < energy_threshold
        # Added more moods that can share content
        mood_ok = ctx.current_mood in ["curious", "excited", "bubbly", "neutral", "silly", "flirty"]
        
        result = quiet_check and cooldown_check and energy_ok and mood_ok
        
        if debug:
            logger.debug(f"   [Share] msgs_last_hour={ctx.messages_last_hour} < 20? {quiet_check}")
            logger.debug(f"   [Share] cooldown={ctx.time_since_last_action:.0f}s > 2400s? {cooldown_check}")
            logger.debug(f"   [Share] energy roll={energy_roll:.2f} < {energy_threshold:.2f}? {energy_ok}")
            logger.debug(f"   [Share] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
        
        return result
    
    def _should_change_profile_picture(self, ctx: ContextSignals, profile: dict, debug: bool = False) -> bool:
        """
        Decide if Miku should change her profile picture.
        This is a rare, once-per-day action.
        """
        # Check if we've changed recently (track globally, not per-server)
        from datetime import datetime, timedelta
        import os
        import json
        
        metadata_path = "memory/profile_pictures/metadata.json"
        
        # Load last change time
        try:
            if os.path.exists(metadata_path):
                with open(metadata_path, 'r') as f:
                    metadata = json.load(f)
                    last_change = metadata.get("changed_at")
                    if last_change:
                        last_change_dt = datetime.fromisoformat(last_change)
                        hours_since_change = (datetime.now() - last_change_dt).total_seconds() / 3600
                        
                        if hours_since_change < 20:  # At least 20 hours between changes
                            if debug:
                                logger.debug(f"   [PFP] Last change {hours_since_change:.1f}h ago, waiting...")
                            return False
        except Exception as e:
            if debug:
                logger.debug(f"   [PFP] Error checking last change: {e}")
        
        # Only consider changing during certain hours (10 AM - 10 PM)
        hour = ctx.hour_of_day
        time_check = 10 <= hour <= 22
        
        # Require low activity + long cooldown
        quiet_check = ctx.messages_last_hour < 5
        cooldown_check = ctx.time_since_last_action > 5400  # 1.5 hours
        
        # Mood influences decision (more likely when bubbly, curious, excited)
        mood_boost = ctx.current_mood in ["bubbly", "curious", "excited", "silly"]
        
        # Very low base chance (roughly once per day)
        base_chance = 0.02 if mood_boost else 0.01
        roll = random.random()
        roll_ok = roll < base_chance
        
        result = time_check and quiet_check and cooldown_check and roll_ok
        
        if debug:
            logger.debug(f"   [PFP] hour={hour}, time_ok={time_check}")
            logger.debug(f"   [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
            logger.debug(f"   [PFP] cooldown={ctx.time_since_last_action:.0f}s > 5400s? {cooldown_check}")
            logger.debug(f"   [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
            logger.debug(f"   [PFP] Result: {result}")
        
        return result
    
    def should_react_to_message(self, guild_id: int, message_age_seconds: float = 0) -> bool:
        """
        Decide if Miku should react to a message with an emoji.
        Called when new messages arrive OR by periodic scheduler.
        
        Args:
            guild_id: Server ID
            message_age_seconds: How old the message is (0 = brand new)
        
        Returns:
            True if should react, False otherwise
        """
        if guild_id not in self.server_contexts:
            return False
        
        ctx = self.server_contexts[guild_id]
        
        # Never react when asleep
        if ctx.current_mood == "asleep":
            return False
        
        profile = self.mood_profiles.get(ctx.current_mood, self.mood_profiles["neutral"])
        
        # Brand new message (real-time reaction)
        if message_age_seconds < 10:
            # Base 30% chance, modified by mood
            base_chance = 0.30
            mood_multiplier = (profile["impulsiveness"] + profile["sociability"]) / 2
            reaction_chance = base_chance * mood_multiplier
            
            # More likely to react to messages in active conversations
            if ctx.conversation_momentum > 0.5:
                reaction_chance *= 1.5  # Boost in active chats
            
            # Less likely if just reacted recently
            if ctx.time_since_last_action < 300:  # 5 minutes
                reaction_chance *= 0.3  # Reduce significantly
            
            return random.random() < reaction_chance
        
        # Older message (scheduled reaction check)
        else:
            # Base 20% chance for scheduled reactions
            base_chance = 0.20
            mood_multiplier = (profile["impulsiveness"] + profile["energy"]) / 2
            reaction_chance = base_chance * mood_multiplier
            
            # Don't react to very old messages if chat is active
            if message_age_seconds > 1800 and ctx.messages_last_5min > 5:  # 30 min old + active chat
                return False
            
            return random.random() < reaction_chance
    
    def record_action(self, guild_id: int):
        """Record that Miku took an action"""
        self.server_last_action[guild_id] = time.time()
        if guild_id in self.server_contexts:
            self.server_contexts[guild_id].messages_since_last_appearance = 0
            # Clear some event counters
            self.server_contexts[guild_id].users_joined_recently = 0
            self.server_contexts[guild_id].users_status_changed = 0
    
    def decay_events(self, guild_id: int):
        """
        Decay event counters over time (call periodically every 15 minutes).
        Uses proper exponential decay with 1-hour half-life.
        Also cleans up old activities.
        """
        if guild_id not in self.server_contexts:
            return
        
        ctx = self.server_contexts[guild_id]
        
        # Decay user events (half-life of 1 hour)
        # For 15-minute intervals: decay_factor = 0.5^(1/4) ≈ 0.841
        decay_factor = 0.5 ** (1/4)  # ≈ 0.8408964...
        ctx.users_joined_recently = round(ctx.users_joined_recently * decay_factor)
        ctx.users_status_changed = round(ctx.users_status_changed * decay_factor)
        
        # Clean up old activities (older than 1 hour)
        self._clean_old_activities(guild_id, max_age_seconds=3600)


# Global instance
autonomous_engine = AutonomousEngine()
