import discord
import asyncio
import threading
import uvicorn
import logging
import sys
import random
import string
import signal
import atexit
from api import app

# Import new configuration system
from config import CONFIG, SECRETS, validate_config, print_config_summary

from server_manager import server_manager
from config_manager import config_manager
from utils.scheduled import (
    send_monday_video
)
from utils.image_handling import (
    process_media_in_message,
)
from utils.core import (
    is_miku_addressed,
)
from utils.moods import (
    detect_mood_shift
)
from utils.media import(
    overlay_username_with_ffmpeg
)
from utils.llm import query_llama
from utils.autonomous import (
    setup_autonomous_speaking,
    load_last_sent_tweets,
    # V2 imports
    on_message_event,
    on_presence_update as autonomous_presence_update,
    on_member_join as autonomous_member_join,
    initialize_v2_system
)
from utils.dm_logger import dm_logger
from utils.dm_interaction_analyzer import init_dm_analyzer
from utils.logger import get_logger
from utils.task_tracker import create_tracked_task

import globals

# Initialize bot logger
logger = get_logger('bot')

# Validate configuration on startup
is_valid, validation_errors = validate_config()
if not is_valid:
    logger.error("❌ Configuration validation failed!")
    for error in validation_errors:
        logger.error(f"   - {error}")
    logger.error("Please check your .env file and restart.")
    sys.exit(1)

# Print configuration summary for debugging
if CONFIG.autonomous.debug_mode:
    print_config_summary()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s: %(message)s",
    handlers=[
        logging.FileHandler("bot.log", mode='a', encoding='utf-8'),
        logging.StreamHandler(sys.stdout)  # Optional: see logs in stdout too
    ],
    force=True  # Override previous configs
)

# Reduce noise from discord voice receiving library
# CryptoErrors are routine packet decode failures (joins/leaves/key negotiation)
# RTCP packets are control packets sent every ~1s
# Both are harmless and just clutter logs
logging.getLogger('discord.ext.voice_recv.reader').setLevel(logging.CRITICAL)  # Only show critical errors

@globals.client.event
async def on_ready():
    logger.info(f'🎤 MikuBot connected as {globals.client.user}')
    logger.info(f'💬 DM support enabled - users can message Miku directly!')

    globals.BOT_USER = globals.client.user
    
    # Intercept external library loggers (APScheduler, etc.)
    from utils.logger import intercept_external_loggers
    intercept_external_loggers()

    # Restore evil mode state from previous session (if any)
    from utils.evil_mode import restore_evil_mode_on_startup, restore_evil_cat_state
    restore_evil_mode_on_startup()
    
    # Restore Cat personality/model state (async — needs event loop running)
    await restore_evil_cat_state()
    
    # Restore bipolar mode state from previous session (if any)
    from utils.bipolar_mode import restore_bipolar_mode_on_startup
    restore_bipolar_mode_on_startup()

    # Restore runtime settings (language, debug flags, etc.) from config_runtime.yaml
    config_manager.restore_runtime_settings()

    # Initialize DM interaction analyzer
    if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
        init_dm_analyzer(globals.OWNER_USER_ID)
        logger.info(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
        
        # Schedule daily DM analysis (runs at 2 AM every day)
        from utils.scheduled import run_daily_dm_analysis
        globals.scheduler.add_job(
            run_daily_dm_analysis,
            'cron',
            hour=2,
            minute=0,
            id='daily_dm_analysis'
        )
        logger.info("⏰ Scheduled daily DM analysis at 2:00 AM")
    else:
        logger.warning("OWNER_USER_ID not set, DM analysis feature disabled")

    # Schedule nightly memory consolidation (runs at 4 AM UTC every day)
    from utils.consolidation_scheduler import run_nightly_consolidation
    globals.scheduler.add_job(
        run_nightly_consolidation,
        'cron',
        hour=4,
        minute=0,
        id='nightly_consolidation'
    )
    logger.info("🌙 Scheduled nightly memory consolidation at 4:00 AM UTC")

    # Setup autonomous speaking (now handled by server manager)
    setup_autonomous_speaking()
    load_last_sent_tweets()

    # Initialize the V2 autonomous system
    initialize_v2_system(globals.client)
    
    # Initialize profile picture manager
    from utils.profile_picture_manager import profile_picture_manager
    await profile_picture_manager.initialize()
    
    # Save current avatar as fallback
    await profile_picture_manager.save_current_avatar_as_fallback()

    # Set initial Discord presence based on current mood
    try:
        from utils.activities import update_bot_presence, is_manual_override_active
        # On reconnect, don't overwrite an active manual override
        if is_manual_override_active():
            logger.info("Manual override active on ready, preserving it")
        elif globals.EVIL_MODE:
            await update_bot_presence(globals.EVIL_DM_MOOD, is_evil=True, force=True)
        else:
            await update_bot_presence(globals.DM_MOOD, is_evil=False, force=True)
    except Exception as e:
        logger.error(f"Failed to set initial presence: {e}")

    # Start server-specific schedulers (includes DM mood rotation)
    server_manager.start_all_schedulers(globals.client)
    
    # Start the global scheduler for other tasks
    globals.scheduler.start()

@globals.client.event
async def on_message(message):
    if message.author == globals.client.user:
        return
    
    # Check for voice commands first (!miku join, !miku leave, !miku voice-status, !miku test, !miku say, !miku listen, !miku stop-listening)
    if not isinstance(message.channel, discord.DMChannel) and message.content.strip().lower().startswith('!miku '):
        from commands.voice import handle_voice_command
        
        parts = message.content.strip().split()
        if len(parts) >= 2:
            cmd = parts[1].lower()
            args = parts[2:] if len(parts) > 2 else []
            
            if cmd in ['join', 'leave', 'voice-status', 'test', 'say', 'listen', 'stop-listening']:
                await handle_voice_command(message, cmd, args)
                return
    
    # Check for UNO commands (!uno create, !uno join, !uno list, !uno quit, !uno help)
    if message.content.strip().lower().startswith('!uno'):
        from commands.uno import handle_uno_command
        await handle_uno_command(message)
        return
    
    # Block all text responses when voice session is active
    if globals.VOICE_SESSION_ACTIVE:
        # Queue the message for later processing (optional)
        if not hasattr(message, 'author') or message.author != globals.client.user:
            globals.TEXT_MESSAGE_QUEUE.append({
                'message': message,
                'timestamp': message.created_at,
                'channel_id': message.channel.id,
                'content': message.content
            })
            logger.debug(f"Message queued during voice session: {message.content[:50]}...")
        return  # Don't process any messages during voice session
    
    # Skip processing if a bipolar argument is in progress in this channel
    if not isinstance(message.channel, discord.DMChannel):
        from utils.bipolar_mode import is_argument_in_progress
        if is_argument_in_progress(message.channel.id):
            return
        
        # Skip processing if a persona dialogue is in progress in this channel
        from utils.persona_dialogue import is_persona_dialogue_active
        if is_persona_dialogue_active(message.channel.id):
            return

    # Bipolar mode: check if the opposite persona should interject on user messages
    # AND roll for random argument trigger (both non-blocking background tasks)
    if not isinstance(message.channel, discord.DMChannel) and globals.BIPOLAR_MODE:
        try:
            from utils.persona_dialogue import check_for_interjection, is_persona_dialogue_active as dialogue_active
            from utils.bipolar_mode import maybe_trigger_argument, is_argument_in_progress as arg_in_progress
            from utils.task_tracker import create_tracked_task
            
            # Check interjection on user messages (opposite of current active persona)
            if not message.author.bot or message.webhook_id:
                current_persona = "evil" if globals.EVIL_MODE else "miku"
                create_tracked_task(
                    check_for_interjection(message, current_persona),
                    task_name="interjection_check_user",
                )
            
            # Roll random argument trigger chance (15%) on eligible messages
            if not arg_in_progress(message.channel.id) and not dialogue_active(message.channel.id):
                create_tracked_task(
                    maybe_trigger_argument(message.channel, globals.client, "Triggered from conversation flow"),
                    task_name="random_argument_trigger",
                )
        except Exception as e:
            logger.error(f"Error in bipolar trigger checks: {e}")

    if message.content.strip().lower() == "miku, rape this nigga balls" and message.reference:
        async with message.channel.typing():
            # Get replied-to user
            try:
                replied_msg = await message.channel.fetch_message(message.reference.message_id)
                target_username = replied_msg.author.display_name

                # Prepare video
                base_video = "MikuMikuBeam.mp4"
                output_video = f"/tmp/video_{''.join(random.choices(string.ascii_letters, k=5))}.mp4"

                await overlay_username_with_ffmpeg(base_video, output_video, target_username)

                caption = f"Here you go, @{target_username}! 🌟"
                #await message.channel.send(content=caption, file=discord.File(output_video))
                await replied_msg.reply(file=discord.File(output_video))

            except Exception as e:
                logger.error(f"Error processing video: {e}")
                await message.channel.send("Sorry, something went wrong while generating the video.")
            return 

    text = message.content.strip()

    # Check if this is a DM
    is_dm = message.guild is None
    
    # Check if message is addressed to Miku (needed to decide whether to track for autonomous)
    miku_addressed = await is_miku_addressed(message)
    
    if is_dm:
        logger.info(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
        
        # Check if user is blocked
        if dm_logger.is_user_blocked(message.author.id):
            logger.info(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
            return
        
        # Log the user's DM message
        dm_logger.log_user_message(message.author, message, is_bot_message=False)

    if miku_addressed:

        prompt = text  # No cleanup — keep it raw 
        user_id = str(message.author.id)
        
        # If user is replying to a specific message, add context marker
        if message.reference:
            try:
                replied_msg = await message.channel.fetch_message(message.reference.message_id)
                # Only add context if replying to Miku's message
                if replied_msg.author == globals.client.user:
                    # Truncate the replied message to keep prompt manageable
                    replied_content = replied_msg.content[:200] + "..." if len(replied_msg.content) > 200 else replied_msg.content
                    # Add reply context marker to the prompt
                    prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
            except Exception as e:
                logger.error(f"Failed to fetch replied message for context: {e}")

        async with message.channel.typing():
            # Check if vision model is blocked (voice session active)
            if message.attachments and globals.VISION_MODEL_BLOCKED:
                await message.channel.send(
                    "🎤 I can't look at images or videos right now, I'm talking in voice chat! "
                    "Send it again after I leave the voice channel."
                )
                return
            
            # Dispatch media processing (images, videos, GIFs, embeds)
            # to utils/image_handling.process_media_in_message()
            guild_id = message.guild.id if message.guild else None
            if await process_media_in_message(message, prompt, is_dm, guild_id):
                return

            # Check if this is an image generation request
            from utils.image_generation import detect_image_request, handle_image_generation_request
            is_image_request, image_prompt = await detect_image_request(prompt)
            
            if is_image_request and image_prompt:
                logger.info(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
                
                # Block image generation during voice sessions
                if globals.IMAGE_GENERATION_BLOCKED:
                    await message.channel.send(globals.IMAGE_GENERATION_BLOCK_MESSAGE)
                    await message.add_reaction('🎤')
                    logger.info("🚫 Image generation blocked - voice session active")
                    return
                
                # Handle the image generation workflow
                success = await handle_image_generation_request(message, image_prompt)
                if success:
                    return  # Image generation completed successfully
                
                # If image generation failed, fall back to normal response
                logger.warning(f"Image generation failed, falling back to normal response")
            
            # If message is just a prompt, no image
            # For DMs, pass None as guild_id to use DM mood
            guild_id = message.guild.id if message.guild else None
            response_type = "dm_response" if is_dm else "server_response"
            author_name = message.author.display_name

            # Phase 3: Try Cheshire Cat pipeline first (memory-augmented response)
            # Falls back to query_llama if Cat is unavailable or disabled
            response = None
            if globals.USE_CHESHIRE_CAT:
                try:
                    from utils.cat_client import cat_adapter
                    current_mood = globals.DM_MOOD
                    if guild_id:
                        try:
                            from server_manager import server_manager
                            sc = server_manager.get_server_config(guild_id)
                            if sc:
                                current_mood = sc.current_mood_name
                        except Exception:
                            pass

                    cat_result = await cat_adapter.query(
                        text=prompt,
                        user_id=str(message.author.id),
                        guild_id=str(guild_id) if guild_id else None,
                        author_name=author_name,
                        mood=current_mood,
                        response_type=response_type,
                    )
                    if cat_result:
                        response, cat_full_prompt = cat_result
                        effective_mood = current_mood
                        if globals.EVIL_MODE:
                            effective_mood = f"EVIL:{getattr(globals, 'EVIL_DM_MOOD', 'evil_neutral')}"
                        logger.info(f"🐱 Cat response for {author_name} (mood: {effective_mood})")
                        # Track Cat interaction in unified prompt history
                        import datetime
                        globals._prompt_id_counter += 1
                        guild_name = message.guild.name if message.guild else "DM"
                        channel_name = message.channel.name if message.guild else "DM"
                        globals.PROMPT_HISTORY.append({
                            "id": globals._prompt_id_counter,
                            "source": "cat",
                            "full_prompt": cat_full_prompt,
                            "response": response if response else "",
                            "user": author_name,
                            "mood": effective_mood,
                            "guild": guild_name,
                            "channel": channel_name,
                            "timestamp": datetime.datetime.now().isoformat(),
                            "model": "Cat LLM",
                            "response_type": response_type,
                        })
                except Exception as e:
                    logger.warning(f"🐱 Cat pipeline error, falling back to query_llama: {e}")
                    response = None

            # Fallback to direct LLM query if Cat didn't respond
            if not response:
                response = await query_llama(
                    prompt, 
                    user_id=str(message.author.id), 
                    guild_id=guild_id, 
                    response_type=response_type,
                    author_name=author_name
                )
            
            from utils.image_handling import _send_log_bipolar
            response_message = await _send_log_bipolar(message, response, is_dm)
            
        # For server messages, do server-specific mood detection
        if not is_dm and message.guild:
            try:
                from server_manager import server_manager
                server_config = server_manager.get_server_config(message.guild.id)
                if server_config:
                    # Create server context for mood detection
                    server_context = {
                        'current_mood_name': server_config.current_mood_name,
                        'current_mood_description': server_config.current_mood_description,
                        'is_sleeping': server_config.is_sleeping
                    }
                    
                    detected = detect_mood_shift(response, server_context)
                    if detected and detected != server_config.current_mood_name:
                        logger.info(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
                        
                        # Block direct transitions to asleep unless from sleepy
                        if detected == "asleep" and server_config.current_mood_name != "sleepy":
                            logger.warning("Ignoring asleep mood; server wasn't sleepy before.")
                        else:
                            # Update server mood
                            server_manager.set_server_mood(message.guild.id, detected)
                            
                            # Update nickname for this server
                            from utils.moods import update_server_nickname
                            globals.client.loop.create_task(update_server_nickname(message.guild.id))
                            
                            # Update Discord presence to match new mood
                            try:
                                from utils.activities import update_bot_presence
                                await update_bot_presence(detected, is_evil=False)
                            except Exception as e:
                                logger.error(f"Failed to update presence after mood detection: {e}")
                            
                            logger.info(f"🔄 Server mood auto-updated to: {detected}")
                            
                            if detected == "asleep":
                                server_manager.set_server_sleep_state(message.guild.id, True)
                                server_manager.schedule_wakeup_task(message.guild.id, delay_seconds=3600)
                else:
                    logger.error(f"No server config found for guild {message.guild.id}, skipping mood detection")
            except Exception as e:
                logger.error(f"Error in server mood detection: {e}")
        elif is_dm:
            logger.debug("DM message - no mood detection (DM mood only changes via auto-rotation)")

    # V2: Track message for autonomous engine (non-blocking, no LLM calls)
    # IMPORTANT: Only call this if the message was NOT addressed to Miku
    # This prevents autonomous actions from firing when the user is directly talking to Miku
    if not miku_addressed:
        on_message_event(message)
    
    # Note: Autonomous reactions are now handled by V2 system via on_message_event()
    
    # Manual Monday test command (only for server messages)
    if not is_dm and message.content.lower().strip() == "!monday":
        await send_monday_video()
        #await message.channel.send("✅ Monday message sent (or attempted). Check logs.")
        return

@globals.client.event
async def on_raw_reaction_add(payload):
    """Handle reactions added to messages (including bot's own reactions and uncached messages)"""
    # Check if this is a DM
    if payload.guild_id is not None:
        return  # Only handle DM reactions
    
    # Get the channel
    channel = await globals.client.fetch_channel(payload.channel_id)
    if not isinstance(channel, discord.DMChannel):
        return
    
    # Get the user who reacted
    user = await globals.client.fetch_user(payload.user_id)
    
    # Get the DM partner (the person DMing the bot, not the bot itself)
    # For DMs, we want to log under the user's ID, not the bot's
    if user.id == globals.client.user.id:
        # Bot reacted - find the other user in the DM
        message = await channel.fetch_message(payload.message_id)
        dm_user_id = message.author.id if message.author.id != globals.client.user.id else channel.recipient.id
        is_bot_reactor = True
    else:
        # User reacted
        dm_user_id = user.id
        is_bot_reactor = False
    
    # Get emoji string
    emoji_str = str(payload.emoji)
    
    # Log the reaction
    await dm_logger.log_reaction_add(
        user_id=dm_user_id,
        message_id=payload.message_id,
        emoji=emoji_str,
        reactor_id=user.id,
        reactor_name=user.display_name or user.name,
        is_bot_reactor=is_bot_reactor
    )
    
    reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {user.display_name}"
    logger.debug(f"DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")

@globals.client.event
async def on_raw_reaction_remove(payload):
    """Handle reactions removed from messages (including bot's own reactions and uncached messages)"""
    # Check if this is a DM
    if payload.guild_id is not None:
        return  # Only handle DM reactions
    
    # Get the channel
    channel = await globals.client.fetch_channel(payload.channel_id)
    if not isinstance(channel, discord.DMChannel):
        return
    
    # Get the user who removed the reaction
    user = await globals.client.fetch_user(payload.user_id)
    
    # Get the DM partner (the person DMing the bot, not the bot itself)
    if user.id == globals.client.user.id:
        # Bot removed reaction - find the other user in the DM
        message = await channel.fetch_message(payload.message_id)
        dm_user_id = message.author.id if message.author.id != globals.client.user.id else channel.recipient.id
    else:
        # User removed reaction
        dm_user_id = user.id
    
    # Get emoji string
    emoji_str = str(payload.emoji)
    
    # Log the reaction removal
    await dm_logger.log_reaction_remove(
        user_id=dm_user_id,
        message_id=payload.message_id,
        emoji=emoji_str,
        reactor_id=user.id
    )
    
    reactor_type = "🤖 Miku" if user.id == globals.client.user.id else f"👤 {user.display_name}"
    logger.debug(f"DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")

@globals.client.event
async def on_presence_update(before, after):
    """Track user presence changes for autonomous V2 system"""
    # Discord.py passes before/after Member objects with different states
    # We pass the 'after' member and both states for comparison
    autonomous_presence_update(after, before, after)

@globals.client.event
async def on_member_join(member):
    """Track member joins for autonomous V2 system"""
    autonomous_member_join(member)

@globals.client.event
async def on_voice_state_update(member: discord.Member, before: discord.VoiceState, after: discord.VoiceState):
    """Track voice channel join/leave for voice call management."""
    from utils.voice_manager import VoiceSessionManager
    
    session_manager = VoiceSessionManager()
    if not session_manager.active_session:
        return
    
    # Check if this is our voice channel
    if before.channel != session_manager.active_session.voice_channel and \
       after.channel != session_manager.active_session.voice_channel:
        return
    
    # User joined our voice channel
    if before.channel != after.channel and after.channel == session_manager.active_session.voice_channel:
        logger.info(f"👤 {member.name} joined voice channel")
        await session_manager.active_session.on_user_join(member.id)
        
        # Auto-start listening if this is a voice call
        if session_manager.active_session.call_user_id == member.id:
            await session_manager.active_session.start_listening(member)
    
    # User left our voice channel
    elif before.channel == session_manager.active_session.voice_channel and \
         after.channel != before.channel:
        logger.info(f"👤 {member.name} left voice channel")
        await session_manager.active_session.on_user_leave(member.id)
        
        # Stop listening to this user
        await session_manager.active_session.stop_listening(member.id)

def start_api():
    # Set log_level to "critical" to silence uvicorn's access logs
    # Our custom api.requests middleware handles HTTP logging with better formatting and filtering
    uvicorn.run(app, host="0.0.0.0", port=3939, log_level="critical")

def save_autonomous_state():
    """Save autonomous context on shutdown"""
    try:
        from utils.autonomous import autonomous_engine
        autonomous_engine.save_context()
        logger.info("💾 Saved autonomous context on shutdown")
    except Exception as e:
        logger.error(f"Failed to save autonomous context on shutdown: {e}")

async def graceful_shutdown():
    """
    Perform a full async cleanup before the bot exits.

    Shutdown sequence:
      1. End active voice sessions (disconnect, release GPU locks)
      2. Save autonomous engine state
      3. Stop the APScheduler
      4. Cancel all tracked background tasks
      5. Close the Discord gateway connection
    """
    logger.warning("🛑 Graceful shutdown initiated...")

    # 1. End active voice session (cleans up audio, STT, GPU locks, etc.)
    try:
        from utils.voice_manager import VoiceSessionManager
        session_mgr = VoiceSessionManager()
        if session_mgr.active_session:
            logger.info("🎙️ Ending active voice session...")
            await session_mgr.end_session()
            logger.info("✓ Voice session ended")
    except Exception as e:
        logger.error(f"Error ending voice session during shutdown: {e}")

    # 2. Persist autonomous engine state
    save_autonomous_state()

    # 3. Shut down the APScheduler
    try:
        if globals.scheduler.running:
            globals.scheduler.shutdown(wait=False)
            logger.info("✓ Scheduler stopped")
    except Exception as e:
        logger.error(f"Error stopping scheduler: {e}")

    # 4. Cancel all tracked background tasks
    try:
        from utils.task_tracker import _active_tasks
        pending = [t for t in _active_tasks if not t.done()]
        if pending:
            logger.info(f"Cancelling {len(pending)} background tasks...")
            for t in pending:
                t.cancel()
            await asyncio.gather(*pending, return_exceptions=True)
            logger.info("✓ Background tasks cancelled")
    except Exception as e:
        logger.error(f"Error cancelling background tasks: {e}")

    # 5. Close the Discord gateway connection
    try:
        if not globals.client.is_closed():
            await globals.client.close()
            logger.info("✓ Discord client closed")
    except Exception as e:
        logger.error(f"Error closing Discord client: {e}")

    logger.warning("🛑 Graceful shutdown complete")

def _handle_shutdown_signal(sig, _frame):
    """Schedule the async shutdown from a sync signal handler."""
    sig_name = signal.Signals(sig).name
    logger.warning(f"Received {sig_name}, scheduling graceful shutdown...")
    # Schedule the coroutine on the running event loop
    loop = asyncio.get_event_loop()
    if loop.is_running():
        loop.create_task(graceful_shutdown())
    else:
        # Fallback: just save state synchronously
        save_autonomous_state()

# Register signal handlers (async-aware)
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
signal.signal(signal.SIGINT, _handle_shutdown_signal)

# Keep atexit as a last-resort sync fallback
atexit.register(save_autonomous_state)

threading.Thread(target=start_api, daemon=True).start()
globals.client.run(globals.DISCORD_BOT_TOKEN)
