# api.py

from fastapi import (
        FastAPI,
        Query,
        Request, UploadFile,
        File,
        Form
)
from fastapi.responses import StreamingResponse
from typing import List, Optional
from pydantic import BaseModel
import globals
from server_manager import server_manager
from utils.conversation_history import conversation_history
from commands.actions import (
    force_sleep,
    wake_up,
    set_mood,
    reset_mood,
    check_mood,
    calm_miku,
    reset_conversation,
    send_bedtime_now
)
from utils.autonomous import (
        miku_autonomous_tick,
        miku_say_something_general,
        miku_engage_random_user,
        share_miku_tweet,
        handle_custom_prompt,
        miku_detect_and_join_conversation
)
import asyncio
import nest_asyncio
import subprocess
import io
import discord
import aiofiles
import aiohttp
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, PlainTextResponse
import os
import json
from utils.figurine_notifier import (
    load_subscribers as figurine_load_subscribers,
    add_subscriber as figurine_add_subscriber,
    remove_subscriber as figurine_remove_subscriber,
    send_figurine_dm_to_all_subscribers,
    send_figurine_dm_to_single_user
)
from utils.dm_logger import dm_logger
from utils.logger import get_logger, list_components, get_component_stats
from utils.log_config import (
    load_config as load_log_config,
    save_config as save_log_config,
    update_component,
    update_global_level,
    update_timestamp_format,
    update_api_filters,
    reset_to_defaults,
    reload_all_loggers
)
import time
from fnmatch import fnmatch
nest_asyncio.apply()

# Initialize API logger
logger = get_logger('api')
api_requests_logger = get_logger('api.requests')

# ========== GPU Selection Helper ==========
def get_current_gpu_url():
    """Get the URL for the currently selected GPU"""
    from config_manager import config_manager
    if config_manager.get_gpu() == "amd":
        return globals.LLAMA_AMD_URL
    return globals.LLAMA_URL

app = FastAPI()

# ========== Global Exception Handler ==========
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """Catch all unhandled exceptions and log them properly."""
    logger.error(f"Unhandled exception on {request.method} {request.url.path}: {exc}", exc_info=True)
    return {"success": False, "error": "Internal server error"}

# ========== Logging Middleware ==========
@app.middleware("http")
async def log_requests(request: Request, call_next):
    """Middleware to log HTTP requests based on configuration."""
    start_time = time.time()
    
    # Get logging config
    log_config = load_log_config()
    api_config = log_config.get('components', {}).get('api.requests', {})
    
    # Check if API request logging is enabled
    if not api_config.get('enabled', False):
        return await call_next(request)
    
    # Get filters
    filters = api_config.get('filters', {})
    exclude_paths = filters.get('exclude_paths', [])
    exclude_status = filters.get('exclude_status', [])
    include_slow_requests = filters.get('include_slow_requests', True)
    slow_threshold_ms = filters.get('slow_threshold_ms', 1000)
    
    # Process request
    response = await call_next(request)
    
    # Calculate duration
    duration_ms = (time.time() - start_time) * 1000
    
    # Check if path should be excluded
    path = request.url.path
    for pattern in exclude_paths:
        if fnmatch(path, pattern):
            return response
    
    # Check if status should be excluded (unless it's a slow request)
    is_slow = duration_ms >= slow_threshold_ms
    if response.status_code in exclude_status and not (include_slow_requests and is_slow):
        return response
    
    # Log the request
    log_msg = f"{request.method} {path} - {response.status_code} ({duration_ms:.2f}ms)"
    
    if is_slow:
        api_requests_logger.warning(f"SLOW REQUEST: {log_msg}")
    elif response.status_code >= 500:
        api_requests_logger.error(log_msg)
    elif response.status_code >= 400:
        api_requests_logger.warning(log_msg)
    else:
        api_requests_logger.api(log_msg)
    
    return response

# Serve static folder
app.mount("/static", StaticFiles(directory="static"), name="static")

# ========== Models ==========
class MoodSetRequest(BaseModel):
    mood: str

class ConversationResetRequest(BaseModel):
    user_id: str

class CustomPromptRequest(BaseModel):
    prompt: str

class ServerConfigRequest(BaseModel):
    guild_id: int
    guild_name: str
    autonomous_channel_id: int
    autonomous_channel_name: str
    bedtime_channel_ids: List[int] = None
    enabled_features: List[str] = None

class EvilMoodSetRequest(BaseModel):
    mood: str

class LogConfigUpdateRequest(BaseModel):
    component: Optional[str] = None
    enabled: Optional[bool] = None
    enabled_levels: Optional[List[str]] = None

class LogFilterUpdateRequest(BaseModel):
    exclude_paths: Optional[List[str]] = None
    exclude_status: Optional[List[int]] = None
    include_slow_requests: Optional[bool] = True
    slow_threshold_ms: Optional[int] = 1000

# ========== Routes ==========
@app.get("/")
def read_index():
    return FileResponse("static/index.html")

@app.get("/logs")
def get_logs():
    try:
        # Read last 100 lines of the log file
        with open("/app/bot.log", "r", encoding="utf-8") as f:
            lines = f.readlines()
        last_100 = lines[-100:] if len(lines) >= 100 else lines
        return "".join(last_100)
    except Exception as e:
        return f"Error reading log file: {e}"

@app.get("/prompt")
def get_last_prompt():
    return {"prompt": globals.LAST_FULL_PROMPT or "No prompt has been issued yet."}

@app.get("/prompt/cat")
def get_last_cat_prompt():
    """Get the last Cheshire Cat interaction (full prompt + response) for Web UI."""
    interaction = globals.LAST_CAT_INTERACTION
    if not interaction.get("full_prompt"):
        return {"full_prompt": "No Cheshire Cat interaction has occurred yet.", "response": "", "user": "", "mood": "", "timestamp": ""}
    return interaction

@app.get("/mood")
def get_current_mood():
    return {"mood": globals.DM_MOOD, "description": globals.DM_MOOD_DESCRIPTION}

@app.post("/mood")
async def set_mood_endpoint(data: MoodSetRequest):
    # This endpoint now operates on DM_MOOD
    from utils.moods import MOOD_EMOJIS
    if data.mood not in MOOD_EMOJIS:
        return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"}
    
    # Update DM mood (DMs don't have nicknames, so no nickname update needed)
    globals.DM_MOOD = data.mood
    from utils.moods import load_mood_description
    globals.DM_MOOD_DESCRIPTION = load_mood_description(data.mood)
    
    # Persist to config manager
    try:
        from config_manager import config_manager
        config_manager.set("runtime.mood.dm_mood", data.mood, persist=True)
    except Exception as e:
        logger.warning(f"Failed to persist mood to config: {e}")
    
    return {"status": "ok", "new_mood": data.mood}

@app.post("/mood/reset")
async def reset_mood_endpoint():
    # Reset DM mood to neutral (DMs don't have nicknames, so no nickname update needed)
    globals.DM_MOOD = "neutral"
    from utils.moods import load_mood_description
    globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral")
    
    # Persist to config manager
    try:
        from config_manager import config_manager
        config_manager.set("runtime.mood.dm_mood", "neutral", persist=True)
    except Exception as e:
        logger.warning(f"Failed to persist mood reset to config: {e}")
    
    return {"status": "ok", "new_mood": "neutral"}

@app.post("/mood/calm")
def calm_miku_endpoint():
    # Calm DM mood to neutral (DMs don't have nicknames, so no nickname update needed)
    globals.DM_MOOD = "neutral"
    from utils.moods import load_mood_description
    globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral")
    
    # Persist to config manager
    try:
        from config_manager import config_manager
        config_manager.set("runtime.mood.dm_mood", "neutral", persist=True)
    except Exception as e:
        logger.warning(f"Failed to persist mood calm to config: {e}")
    
    return {"status": "ok", "message": "Miku has been calmed down"}

# ========== Language Mode Management ==========
@app.get("/language")
def get_language_mode():
    """Get current language mode (english or japanese)"""
    return {
        "language_mode": globals.LANGUAGE_MODE,
        "available_languages": ["english", "japanese"],
        "current_model": globals.JAPANESE_TEXT_MODEL if globals.LANGUAGE_MODE == "japanese" else globals.TEXT_MODEL
    }

@app.post("/language/toggle")
def toggle_language_mode():
    """Toggle between English and Japanese modes"""
    if globals.LANGUAGE_MODE == "english":
        globals.LANGUAGE_MODE = "japanese"
        new_mode = "japanese"
        model_used = globals.JAPANESE_TEXT_MODEL
        logger.info("Switched to Japanese mode (using Llama 3.1 Swallow)")
    else:
        globals.LANGUAGE_MODE = "english"
        new_mode = "english"
        model_used = globals.TEXT_MODEL
        logger.info("Switched to English mode (using default model)")
    
    # Persist via config manager
    try:
        from config_manager import config_manager
        config_manager.set("discord.language_mode", new_mode, persist=True)
        logger.info(f"💾 Language mode persisted to config_runtime.yaml")
    except Exception as e:
        logger.warning(f"Failed to persist language mode: {e}")
    
    return {
        "status": "ok",
        "language_mode": new_mode,
        "model_now_using": model_used,
        "message": f"Miku is now speaking in {new_mode.upper()}!"
    }

@app.post("/language/set")
def set_language_mode(language: str = "english"):
    """Set language mode to either 'english' or 'japanese'"""
    if language.lower() not in ["english", "japanese"]:
        return {"error": f"Invalid language mode '{language}'. Use 'english' or 'japanese'."}, 400
    
    globals.LANGUAGE_MODE = language.lower()
    model_used = globals.JAPANESE_TEXT_MODEL if language.lower() == "japanese" else globals.TEXT_MODEL
    logger.info(f"Language mode set to {language.lower()} (using {model_used})")
    
    # Persist so it survives restarts
    try:
        from config_manager import config_manager
        config_manager.set("discord.language_mode", language.lower(), persist=True)
    except Exception:
        pass
    
    return {
        "status": "ok",
        "language_mode": language.lower(),
        "model_now_using": model_used,
        "message": f"Miku is now speaking in {language.upper()}!"
    }

# ========== Evil Mode Management ==========
@app.get("/evil-mode")
def get_evil_mode_status():
    """Get current evil mode status"""
    from utils.evil_mode import is_evil_mode, get_current_evil_mood
    evil_mode = is_evil_mode()
    if evil_mode:
        mood, mood_desc = get_current_evil_mood()
        return {
            "evil_mode": True,
            "mood": mood,
            "description": mood_desc,
            "available_moods": globals.EVIL_AVAILABLE_MOODS
        }
    return {
        "evil_mode": False,
        "mood": None,
        "description": None,
        "available_moods": globals.EVIL_AVAILABLE_MOODS
    }

@app.post("/evil-mode/enable")
def enable_evil_mode():
    """Enable evil mode"""
    from utils.evil_mode import apply_evil_mode_changes
    
    if globals.EVIL_MODE:
        return {"status": "ok", "message": "Evil mode is already enabled", "evil_mode": True}
    
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        globals.client.loop.create_task(apply_evil_mode_changes(globals.client))
        return {"status": "ok", "message": "Evil mode enabled", "evil_mode": True}
    else:
        return {"status": "error", "message": "Discord client not ready"}

@app.post("/evil-mode/disable")
def disable_evil_mode():
    """Disable evil mode"""
    from utils.evil_mode import revert_evil_mode_changes
    
    if not globals.EVIL_MODE:
        return {"status": "ok", "message": "Evil mode is already disabled", "evil_mode": False}
    
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        globals.client.loop.create_task(revert_evil_mode_changes(globals.client))
        return {"status": "ok", "message": "Evil mode disabled", "evil_mode": False}
    else:
        return {"status": "error", "message": "Discord client not ready"}

@app.post("/evil-mode/toggle")
def toggle_evil_mode():
    """Toggle evil mode on/off"""
    from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes
    
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Discord client not ready"}
    
    if globals.EVIL_MODE:
        globals.client.loop.create_task(revert_evil_mode_changes(globals.client))
        return {"status": "ok", "message": "Evil mode disabled", "evil_mode": False}
    else:
        globals.client.loop.create_task(apply_evil_mode_changes(globals.client))
        return {"status": "ok", "message": "Evil mode enabled", "evil_mode": True}

@app.get("/evil-mode/mood")
def get_evil_mood():
    """Get current evil mood"""
    from utils.evil_mode import get_current_evil_mood
    mood, mood_desc = get_current_evil_mood()
    return {
        "mood": mood,
        "description": mood_desc,
        "available_moods": globals.EVIL_AVAILABLE_MOODS
    }

@app.post("/evil-mode/mood")
def set_evil_mood_endpoint(data: EvilMoodSetRequest):
    """Set evil mood"""
    from utils.evil_mode import set_evil_mood, is_valid_evil_mood, update_all_evil_nicknames
    
    if not is_valid_evil_mood(data.mood):
        return {
            "status": "error", 
            "message": f"Mood '{data.mood}' not recognized. Available evil moods: {', '.join(globals.EVIL_AVAILABLE_MOODS)}"
        }
    
    success = set_evil_mood(data.mood)
    if success:
        # Update nicknames if evil mode is active
        if globals.EVIL_MODE and globals.client and globals.client.loop and globals.client.loop.is_running():
            globals.client.loop.create_task(update_all_evil_nicknames(globals.client))
        return {"status": "ok", "new_mood": data.mood}
    
    return {"status": "error", "message": "Failed to set evil mood"}

# ========== Bipolar Mode Management ==========
class BipolarTriggerRequest(BaseModel):
    channel_id: str  # String to handle large Discord IDs from JS
    message_id: str = None  # Optional: starting message ID (string)
    context: str = ""

@app.get("/bipolar-mode")
def get_bipolar_mode_status():
    """Get current bipolar mode status"""
    from utils.bipolar_mode import is_bipolar_mode, is_argument_in_progress
    
    # Get any active arguments
    active_arguments = {}
    for channel_id, data in globals.BIPOLAR_ARGUMENT_IN_PROGRESS.items():
        if data.get("active"):
            active_arguments[channel_id] = data
    
    return {
        "bipolar_mode": is_bipolar_mode(),
        "evil_mode": globals.EVIL_MODE,
        "active_arguments": active_arguments,
        "webhooks_configured": len(globals.BIPOLAR_WEBHOOKS)
    }

@app.post("/bipolar-mode/enable")
def enable_bipolar_mode():
    """Enable bipolar mode"""
    from utils.bipolar_mode import enable_bipolar_mode as _enable
    
    if globals.BIPOLAR_MODE:
        return {"status": "ok", "message": "Bipolar mode is already enabled", "bipolar_mode": True}
    
    _enable()
    
    # Persist to config manager
    try:
        from config_manager import config_manager
        config_manager.set("runtime.bipolar_mode.enabled", True, persist=True)
    except Exception as e:
        logger.warning(f"Failed to persist bipolar mode enable to config: {e}")
    
    return {"status": "ok", "message": "Bipolar mode enabled", "bipolar_mode": True}

@app.post("/bipolar-mode/disable")
def disable_bipolar_mode():
    """Disable bipolar mode"""
    from utils.bipolar_mode import disable_bipolar_mode as _disable, cleanup_webhooks
    
    if not globals.BIPOLAR_MODE:
        return {"status": "ok", "message": "Bipolar mode is already disabled", "bipolar_mode": False}
    
    _disable()
    
    # Persist to config manager
    try:
        from config_manager import config_manager
        config_manager.set("runtime.bipolar_mode.enabled", False, persist=True)
    except Exception as e:
        logger.warning(f"Failed to persist bipolar mode disable to config: {e}")
    
    # Optionally cleanup webhooks in background
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        globals.client.loop.create_task(cleanup_webhooks(globals.client))
    
    return {"status": "ok", "message": "Bipolar mode disabled", "bipolar_mode": False}

@app.post("/bipolar-mode/toggle")
def toggle_bipolar_mode():
    """Toggle bipolar mode on/off"""
    from utils.bipolar_mode import toggle_bipolar_mode as _toggle, cleanup_webhooks
    
    new_state = _toggle()
    
    # If disabled, cleanup webhooks
    if not new_state:
        if globals.client and globals.client.loop and globals.client.loop.is_running():
            globals.client.loop.create_task(cleanup_webhooks(globals.client))
    
    return {
        "status": "ok",
        "message": f"Bipolar mode {'enabled' if new_state else 'disabled'}",
        "bipolar_mode": new_state
    }

@app.post("/bipolar-mode/trigger-argument")
def trigger_argument(data: BipolarTriggerRequest):
    """Manually trigger an argument in a specific channel
    
    If message_id is provided, the argument will start from that message.
    The opposite persona will respond to it.
    """
    from utils.bipolar_mode import force_trigger_argument, force_trigger_argument_from_message_id, is_bipolar_mode, is_argument_in_progress
    
    # Parse IDs from strings
    try:
        channel_id = int(data.channel_id)
    except ValueError:
        return {"status": "error", "message": "Invalid channel ID format"}
    
    message_id = None
    if data.message_id:
        try:
            message_id = int(data.message_id)
        except ValueError:
            return {"status": "error", "message": "Invalid message ID format"}
    
    if not is_bipolar_mode():
        return {"status": "error", "message": "Bipolar mode is not enabled"}
    
    if is_argument_in_progress(channel_id):
        return {"status": "error", "message": "An argument is already in progress in this channel"}
    
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Discord client not ready"}
    
    # If message_id is provided, use the message-based trigger
    if message_id:
        import asyncio
        
        async def trigger_from_message():
            success, error = await force_trigger_argument_from_message_id(
                channel_id, message_id, globals.client, data.context
            )
            if not success:
                logger.error(f"Failed to trigger argument from message: {error}")
        
        globals.client.loop.create_task(trigger_from_message())
        
        return {
            "status": "ok",
            "message": f"Argument triggered from message {message_id}",
            "channel_id": channel_id,
            "message_id": message_id
        }
    
    # Otherwise, find the channel and trigger normally
    channel = globals.client.get_channel(channel_id)
    if not channel:
        return {"status": "error", "message": f"Channel {channel_id} not found"}
    
    # Trigger the argument
    globals.client.loop.create_task(force_trigger_argument(channel, globals.client, data.context))
    
    return {
        "status": "ok",
        "message": f"Argument triggered in #{channel.name}",
        "channel_id": channel_id
    }

@app.post("/bipolar-mode/trigger-dialogue")
def trigger_dialogue(data: dict):
    """Manually trigger a persona dialogue from a message
    
    Forces the opposite persona to start a dialogue (bypasses the interjection check).
    """
    from utils.persona_dialogue import get_dialogue_manager
    from utils.bipolar_mode import is_bipolar_mode, is_argument_in_progress
    
    message_id_str = data.get("message_id")
    if not message_id_str:
        return {"status": "error", "message": "Message ID is required"}
    
    # Parse message ID
    try:
        message_id = int(message_id_str)
    except ValueError:
        return {"status": "error", "message": "Invalid message ID format"}
    
    if not is_bipolar_mode():
        return {"status": "error", "message": "Bipolar mode is not enabled"}
    
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Discord client not ready"}
    
    import asyncio
    
    async def trigger_dialogue_task():
        try:
            # Fetch the message
            message = None
            for channel in globals.client.get_all_channels():
                if hasattr(channel, 'fetch_message'):
                    try:
                        message = await channel.fetch_message(message_id)
                        break
                    except:
                        continue
            
            if not message:
                logger.error(f"Message {message_id} not found")
                return
            
            # Check if there's already an argument or dialogue in progress
            dialogue_manager = get_dialogue_manager()
            if dialogue_manager.is_dialogue_active(message.channel.id):
                logger.error(f"Dialogue already active in channel {message.channel.id}")
                return
            
            if is_argument_in_progress(message.channel.id):
                logger.error(f"Argument already in progress in channel {message.channel.id}")
                return
            
            # Determine current persona from the message author
            if message.webhook_id:
                # It's a webhook message, need to determine which persona
                current_persona = "evil" if globals.EVIL_MODE else "miku"
            elif message.author.id == globals.client.user.id:
                # It's the bot's message
                current_persona = "evil" if globals.EVIL_MODE else "miku"
            else:
                # User message - can't trigger dialogue from user messages
                logger.error(f"Cannot trigger dialogue from user message")
                return
            
            opposite_persona = "evil" if current_persona == "miku" else "miku"
            
            logger.info(f"[Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
            
            # Force start the dialogue (bypass interjection check)
            dialogue_manager.start_dialogue(message.channel.id)
            asyncio.create_task(
                dialogue_manager.handle_dialogue_turn(
                    message.channel, 
                    opposite_persona, 
                    trigger_reason="manual_trigger"
                )
            )
            
        except Exception as e:
            logger.error(f"Error triggering dialogue: {e}")
            import traceback
            traceback.print_exc()
    
    globals.client.loop.create_task(trigger_dialogue_task())
    
    return {
        "status": "ok",
        "message": f"Dialogue triggered for message {message_id}"
    }

@app.get("/bipolar-mode/scoreboard")
def get_bipolar_scoreboard():
    """Get the bipolar mode argument scoreboard"""
    from utils.bipolar_mode import load_scoreboard, get_scoreboard_summary
    
    scoreboard = load_scoreboard()
    
    return {
        "status": "ok",
        "scoreboard": {
            "miku_wins": scoreboard.get("miku", 0),
            "evil_wins": scoreboard.get("evil", 0),
            "total_arguments": scoreboard.get("miku", 0) + scoreboard.get("evil", 0),
            "history": scoreboard.get("history", [])[-10:]  # Last 10 results
        },
        "summary": get_scoreboard_summary()
    }

@app.post("/bipolar-mode/cleanup-webhooks")
def cleanup_bipolar_webhooks():
    """Cleanup all bipolar webhooks from all servers"""
    from utils.bipolar_mode import cleanup_webhooks
    
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Discord client not ready"}
    
    globals.client.loop.create_task(cleanup_webhooks(globals.client))
    return {"status": "ok", "message": "Webhook cleanup started"}

# ========== GPU Selection ==========
@app.get("/gpu-status")
def get_gpu_status():
    """Get current GPU selection"""
    from config_manager import config_manager
    return {"gpu": config_manager.get_gpu()}

@app.post("/gpu-select")
async def select_gpu(request: Request):
    """Select which GPU to use for inference"""
    data = await request.json()
    gpu = data.get("gpu", "nvidia").lower()
    
    if gpu not in ["nvidia", "amd"]:
        return {"status": "error", "message": "Invalid GPU selection. Must be 'nvidia' or 'amd'"}
    
    try:
        from config_manager import config_manager
        success = config_manager.set_gpu(gpu)
        
        if success:
            logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
            return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
        else:
            return {"status": "error", "message": "Failed to save GPU state"}
    except Exception as e:
        logger.error(f"GPU Selection Error: {e}")
        return {"status": "error", "message": str(e)}

@app.get("/bipolar-mode/arguments")
def get_active_arguments():
    """Get all active arguments"""
    active = {}
    for channel_id, data in globals.BIPOLAR_ARGUMENT_IN_PROGRESS.items():
        if data.get("active"):
            channel = globals.client.get_channel(channel_id) if globals.client else None
            active[channel_id] = {
                **data,
                "channel_name": channel.name if channel else "Unknown"
            }
    return {"active_arguments": active}

# ========== Per-Server Mood Management ==========
@app.get("/servers/{guild_id}/mood")
def get_server_mood(guild_id: int):
    """Get current mood for a specific server"""
    mood_name, mood_description = server_manager.get_server_mood(guild_id)
    return {
        "guild_id": guild_id,
        "mood": mood_name,
        "description": mood_description
    }

@app.post("/servers/{guild_id}/mood")
async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
    """Set mood for a specific server"""
    
    # Check if server exists
    if guild_id not in server_manager.servers:
        logger.warning(f"Server {guild_id} not found in server_manager.servers")
        return {"status": "error", "message": "Server not found"}
    
    # Check if mood is valid
    from utils.moods import MOOD_EMOJIS
    if data.mood not in MOOD_EMOJIS:
        logger.warning(f"Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
        return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"}
    
    success = server_manager.set_server_mood(guild_id, data.mood)
    logger.debug(f"Server mood set result: {success}")
    
    if success:
        # Update the nickname for this server
        from utils.moods import update_server_nickname
        logger.debug(f"Updating nickname for server {guild_id}")
        globals.client.loop.create_task(update_server_nickname(guild_id))
        return {"status": "ok", "new_mood": data.mood, "guild_id": guild_id}
    
    logger.warning(f"set_server_mood returned False for unknown reason")
    return {"status": "error", "message": "Failed to set server mood"}

@app.post("/servers/{guild_id}/mood/reset")
async def reset_server_mood_endpoint(guild_id: int):
    """Reset mood to neutral for a specific server"""
    logger.debug(f"Resetting mood for server {guild_id} to neutral")
    
    # Check if server exists
    if guild_id not in server_manager.servers:
        logger.warning(f"Server {guild_id} not found in server_manager.servers")
        return {"status": "error", "message": "Server not found"}
    
    logger.debug(f"Server validation passed, calling set_server_mood")
    success = server_manager.set_server_mood(guild_id, "neutral")
    logger.debug(f"Server mood reset result: {success}")
    
    if success:
        # Update the nickname for this server
        from utils.moods import update_server_nickname
        logger.debug(f"Updating nickname for server {guild_id}")
        globals.client.loop.create_task(update_server_nickname(guild_id))
        return {"status": "ok", "new_mood": "neutral", "guild_id": guild_id}
    
    logger.warning(f"set_server_mood returned False for unknown reason")
    return {"status": "error", "message": "Failed to reset server mood"}

@app.get("/servers/{guild_id}/mood/state")
def get_server_mood_state(guild_id: int):
    """Get complete mood state for a specific server"""
    mood_state = server_manager.get_server_mood_state(guild_id)
    if mood_state:
        return {"status": "ok", "guild_id": guild_id, "mood_state": mood_state}
    return {"status": "error", "message": "Server not found"}

@app.post("/conversation/reset")
def reset_convo(data: ConversationResetRequest):
    reset_conversation(data.user_id)
    return {"status": "ok", "message": "Conversation reset"}

@app.post("/sleep")
async def force_sleep_endpoint():
    await force_sleep()
    return {"status": "ok", "message": "Miku is now sleeping"}

@app.post("/wake")
async def wake_up_endpoint():
    await wake_up()
    return {"status": "ok", "message": "Miku is now awake"}

@app.post("/bedtime")
async def bedtime_endpoint(guild_id: int = None):
    # If guild_id is provided, send bedtime reminder only to that server
    # If no guild_id, send to all servers (legacy behavior)
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Send to specific server only
            from utils.scheduled import send_bedtime_reminder_for_server
            globals.client.loop.create_task(send_bedtime_reminder_for_server(guild_id, globals.client))
            return {"status": "ok", "message": f"Bedtime reminder queued for server {guild_id}"}
        else:
            # Send to all servers (legacy behavior)
            from utils.scheduled import send_bedtime_now
            globals.client.loop.create_task(send_bedtime_now())
            return {"status": "ok", "message": "Bedtime reminder queued for all servers"}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/general")
async def trigger_autonomous_general(guild_id: int = None):
    # If guild_id is provided, send autonomous message only to that server
    # If no guild_id, send to all servers (legacy behavior)
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Send to specific server only
            from utils.autonomous import miku_say_something_general_for_server
            globals.client.loop.create_task(miku_say_something_general_for_server(guild_id))
            return {"status": "ok", "message": f"Autonomous general message queued for server {guild_id}"}
        else:
            # Send to all servers (legacy behavior)
            from utils.autonomous import miku_say_something_general
            globals.client.loop.create_task(miku_say_something_general())
            return {"status": "ok", "message": "Autonomous general message queued for all servers"}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/engage")
async def trigger_autonomous_engage_user(
    guild_id: int = None,
    user_id: str = None,
    engagement_type: str = None,
    manual_trigger: str = "false"
):
    # If guild_id is provided, send autonomous engagement only to that server
    # If no guild_id, send to all servers (legacy behavior)
    # user_id: Optional specific user to engage (Discord user ID as string)
    # engagement_type: Optional type - 'activity', 'general', 'status', or None for random
    # manual_trigger: If True (as string), bypass the "recently engaged" check (for web UI manual triggers)
    # Convert manual_trigger string to boolean
    manual_trigger_bool = manual_trigger.lower() in ('true', '1', 'yes')
    
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Send to specific server only
            from utils.autonomous import miku_engage_random_user_for_server
            globals.client.loop.create_task(miku_engage_random_user_for_server(guild_id, user_id=user_id, engagement_type=engagement_type, manual_trigger=manual_trigger_bool))
            
            # Build detailed message
            msg_parts = [f"Autonomous user engagement queued for server {guild_id}"]
            if user_id:
                msg_parts.append(f"targeting user {user_id}")
            if engagement_type:
                msg_parts.append(f"with {engagement_type} engagement")
            if manual_trigger_bool:
                msg_parts.append("(manual trigger - bypassing cooldown)")
            
            return {"status": "ok", "message": " ".join(msg_parts)}
        else:
            # Send to all servers (legacy behavior)
            from utils.autonomous import miku_engage_random_user
            globals.client.loop.create_task(miku_engage_random_user(user_id=user_id, engagement_type=engagement_type, manual_trigger=manual_trigger_bool))
            
            msg_parts = ["Autonomous user engagement queued for all servers"]
            if user_id:
                msg_parts.append(f"targeting user {user_id}")
            if engagement_type:
                msg_parts.append(f"with {engagement_type} engagement")
            if manual_trigger_bool:
                msg_parts.append("(manual trigger - bypassing cooldown)")
            
            return {"status": "ok", "message": " ".join(msg_parts)}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/tweet")
async def trigger_autonomous_tweet(guild_id: int = None, tweet_url: str = None):
    # If guild_id is provided, send tweet only to that server
    # If no guild_id, send to all servers (legacy behavior)
    # If tweet_url is provided, share that specific tweet; otherwise fetch one
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Send to specific server only
            from utils.autonomous import share_miku_tweet_for_server
            globals.client.loop.create_task(share_miku_tweet_for_server(guild_id, tweet_url=tweet_url))
            msg = f"Autonomous tweet sharing queued for server {guild_id}"
            if tweet_url:
                msg += f" with URL {tweet_url}"
            return {"status": "ok", "message": msg}
        else:
            # Send to all servers (legacy behavior)
            from utils.autonomous import share_miku_tweet
            globals.client.loop.create_task(share_miku_tweet(tweet_url=tweet_url))
            msg = "Autonomous tweet sharing queued for all servers"
            if tweet_url:
                msg += f" with URL {tweet_url}"
            return {"status": "ok", "message": msg}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/custom")
async def custom_autonomous_message(req: CustomPromptRequest, guild_id: int = None):
    # If guild_id is provided, send custom prompt only to that server
    # If no guild_id, send to all servers (legacy behavior)
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Send to specific server only
            from utils.autonomous import handle_custom_prompt_for_server
            # Use create_task to avoid timeout context manager error
            globals.client.loop.create_task(handle_custom_prompt_for_server(guild_id, req.prompt))
            return {"status": "ok", "message": f"Custom autonomous message queued for server {guild_id}"}
        else:
            # Send to all servers (legacy behavior)
            from utils.autonomous import handle_custom_prompt
            # Use create_task to avoid timeout context manager error
            globals.client.loop.create_task(handle_custom_prompt(req.prompt))
            return {"status": "ok", "message": "Custom autonomous message queued for all servers"}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/reaction")
async def trigger_autonomous_reaction(guild_id: int = None):
    # If guild_id is provided, trigger reaction only for that server
    # If no guild_id, trigger for all servers (legacy behavior)
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Trigger for specific server only (force=True bypasses 50% chance)
            from utils.autonomous import miku_autonomous_reaction_for_server
            globals.client.loop.create_task(miku_autonomous_reaction_for_server(guild_id, force=True))
            return {"status": "ok", "message": f"Autonomous reaction queued for server {guild_id}"}
        else:
            # Trigger for all servers (legacy behavior, force=True bypasses 50% chance)
            from utils.autonomous import miku_autonomous_reaction
            globals.client.loop.create_task(miku_autonomous_reaction(force=True))
            return {"status": "ok", "message": "Autonomous reaction queued for all servers"}
    else:
        return {"status": "error", "message": "Bot not ready"}

@app.post("/autonomous/join-conversation")
async def trigger_detect_and_join_conversation(guild_id: int = None):
    # If guild_id is provided, detect and join conversation only for that server
    # If no guild_id, trigger for all servers
    logger.debug(f"Join conversation endpoint called with guild_id={guild_id}")
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        if guild_id is not None:
            # Trigger for specific server only (force=True to bypass checks when manually triggered)
            logger.debug(f"Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
            from utils.autonomous import miku_detect_and_join_conversation_for_server
            globals.client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id, force=True))
            return {"status": "ok", "message": f"Detect and join conversation queued for server {guild_id}"}
        else:
            # Trigger for all servers (force=True to bypass checks when manually triggered)
            logger.debug(f"Importing and calling miku_detect_and_join_conversation() for all servers")
            from utils.autonomous import miku_detect_and_join_conversation
            globals.client.loop.create_task(miku_detect_and_join_conversation(force=True))
            return {"status": "ok", "message": "Detect and join conversation queued for all servers"}
    else:
        logger.error(f"Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
        return {"status": "error", "message": "Bot not ready"}

@app.post("/profile-picture/change")
async def trigger_profile_picture_change(
    guild_id: int = None,
    file: UploadFile = File(None)
):
    """
    Change Miku's profile picture.
    If a file is provided, use it. Otherwise, search Danbooru.
    """
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        from server_manager import server_manager
        
        # Get mood from guild_id (if provided)
        mood = None
        if guild_id is not None:
            mood, _ = server_manager.get_server_mood(guild_id)
        else:
            # Use DM mood as fallback
            mood = globals.DM_MOOD
        
        # If file provided, use it
        custom_image_bytes = None
        if file:
            custom_image_bytes = await file.read()
            logger.info(f"Received custom image upload ({len(custom_image_bytes)} bytes)")
        
        # Change profile picture
        result = await profile_picture_manager.change_profile_picture(
            mood=mood,
            custom_image_bytes=custom_image_bytes,
            debug=True
        )
        
        if result["success"]:
            return {
                "status": "ok",
                "message": "Profile picture changed successfully",
                "source": result["source"],
                "metadata": result.get("metadata", {})
            }
        else:
            return {
                "status": "error",
                "message": result.get("error", "Unknown error"),
                "source": result["source"]
            }
    
    except Exception as e:
        logger.error(f"Error in profile picture API: {e}")
        import traceback
        traceback.print_exc()
        return {"status": "error", "message": f"Unexpected error: {str(e)}"}

@app.get("/profile-picture/metadata")
async def get_profile_picture_metadata():
    """Get metadata about the current profile picture"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        metadata = profile_picture_manager.load_metadata()
        if metadata:
            return {"status": "ok", "metadata": metadata}
        else:
            return {"status": "ok", "metadata": None, "message": "No metadata found"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/restore-fallback")
async def restore_fallback_profile_picture():
    """Restore the original fallback profile picture"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        success = await profile_picture_manager.restore_fallback()
        if success:
            return {"status": "ok", "message": "Fallback profile picture restored"}
        else:
            return {"status": "error", "message": "Failed to restore fallback"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/role-color/custom")
async def set_custom_role_color(hex_color: str = Form(...)):
    """Set a custom role color across all servers"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.set_custom_role_color(hex_color, debug=True)
        if result["success"]:
            return {
                "status": "ok",
                "message": f"Role color updated to {result['color']['hex']}",
                "color": result["color"]
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/role-color/reset-fallback")
async def reset_role_color_to_fallback():
    """Reset role color to fallback (#86cecb)"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.reset_to_fallback_color(debug=True)
        if result["success"]:
            return {
                "status": "ok",
                "message": f"Role color reset to fallback {result['color']['hex']}",
                "color": result["color"]
            }
        else:
            return {"status": "error", "message": "Failed to reset color"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

# === Profile Picture Image Serving ===

@app.get("/profile-picture/image/original")
async def serve_original_profile_picture():
    """Serve the full-resolution original profile picture"""
    from utils.profile_picture_manager import profile_picture_manager
    path = profile_picture_manager.ORIGINAL_PATH
    if not os.path.exists(path):
        return {"status": "error", "message": "No original image found"}
    return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})

@app.get("/profile-picture/image/current")
async def serve_current_profile_picture():
    """Serve the current cropped profile picture"""
    from utils.profile_picture_manager import profile_picture_manager
    path = profile_picture_manager.CURRENT_PATH
    if not os.path.exists(path):
        return {"status": "error", "message": "No current image found"}
    return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})

# === Profile Picture Manual Crop Workflow ===

@app.post("/profile-picture/change-no-crop")
async def trigger_profile_picture_change_no_crop(
    guild_id: int = None,
    file: UploadFile = File(None)
):
    """
    Change Miku's profile picture but skip auto-cropping.
    Saves the full-resolution original for manual cropping later.
    """
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        from server_manager import server_manager
        
        mood = None
        if guild_id is not None:
            mood, _ = server_manager.get_server_mood(guild_id)
        else:
            mood = globals.DM_MOOD
        
        custom_image_bytes = None
        if file:
            custom_image_bytes = await file.read()
            logger.info(f"Received custom image for manual crop ({len(custom_image_bytes)} bytes)")
        
        result = await profile_picture_manager.change_profile_picture(
            mood=mood,
            custom_image_bytes=custom_image_bytes,
            debug=True,
            skip_crop=True
        )
        
        if result["success"]:
            return {
                "status": "ok",
                "message": "Image saved for manual cropping",
                "source": result["source"],
                "metadata": result.get("metadata", {})
            }
        else:
            return {
                "status": "error",
                "message": result.get("error", "Unknown error"),
                "source": result.get("source")
            }
    except Exception as e:
        logger.error(f"Error in change-no-crop API: {e}")
        import traceback
        traceback.print_exc()
        return {"status": "error", "message": f"Unexpected error: {str(e)}"}

class ManualCropRequest(BaseModel):
    x: int
    y: int
    width: int
    height: int

@app.post("/profile-picture/manual-crop")
async def apply_manual_crop(req: ManualCropRequest):
    """Apply a manual crop to the stored original image"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.manual_crop(
            x=req.x, y=req.y, width=req.width, height=req.height, debug=True
        )
        if result["success"]:
            return {
                "status": "ok",
                "message": "Manual crop applied successfully",
                "metadata": result.get("metadata", {})
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/auto-crop")
async def apply_auto_crop():
    """Run intelligent auto-crop on the stored original image"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.auto_crop_only(debug=True)
        if result["success"]:
            return {
                "status": "ok",
                "message": "Auto-crop applied successfully",
                "metadata": result.get("metadata", {})
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

class DescriptionUpdateRequest(BaseModel):
    description: str

@app.post("/profile-picture/description")
async def update_profile_picture_description(req: DescriptionUpdateRequest):
    """Update the profile picture description (and optionally re-inject into Cat)"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.update_description(
            description=req.description, reinject_cat=True, debug=True
        )
        if result["success"]:
            return {"status": "ok", "message": "Description updated successfully"}
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/regenerate-description")
async def regenerate_profile_picture_description():
    """Re-generate the profile picture description using the vision model"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.regenerate_description(debug=True)
        if result["success"]:
            return {
                "status": "ok",
                "message": "Description regenerated successfully",
                "description": result["description"]
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/profile-picture/description")
async def get_profile_picture_description():
    """Get the current profile picture description text"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        description = profile_picture_manager.get_current_description()
        return {"status": "ok", "description": description or ""}
    except Exception as e:
        return {"status": "error", "message": str(e)}

# === Profile Picture Album / Gallery ===

@app.get("/profile-picture/album")
async def list_album_entries():
    """List all album entries (newest first)"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        entries = profile_picture_manager.get_album_entries()
        return {"status": "ok", "entries": entries, "count": len(entries)}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/profile-picture/album/disk-usage")
async def get_album_disk_usage():
    """Get album disk usage statistics"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        usage = profile_picture_manager.get_album_disk_usage()
        return {"status": "ok", **usage}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/profile-picture/album/{entry_id}")
async def get_album_entry(entry_id: str):
    """Get metadata for a single album entry"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        meta = profile_picture_manager.get_album_entry(entry_id)
        if meta:
            return {"status": "ok", "entry": meta}
        else:
            return {"status": "error", "message": "Album entry not found"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/profile-picture/album/{entry_id}/image/{image_type}")
async def serve_album_image(entry_id: str, image_type: str):
    """Serve an album entry's image (original or cropped)"""
    if image_type not in ("original", "cropped"):
        return {"status": "error", "message": "image_type must be 'original' or 'cropped'"}
    try:
        from utils.profile_picture_manager import profile_picture_manager
        path = profile_picture_manager.get_album_image_path(entry_id, image_type)
        if path:
            return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})
        else:
            return {"status": "error", "message": f"No {image_type} image for this entry"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/album/add")
async def add_to_album(file: UploadFile = File(...)):
    """Add a single image to the album"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        image_bytes = await file.read()
        logger.info(f"Adding image to album ({len(image_bytes)} bytes)")
        result = await profile_picture_manager.add_to_album(
            image_bytes=image_bytes,
            source="custom_upload",
            debug=True
        )
        if result["success"]:
            return {
                "status": "ok",
                "message": "Image added to album",
                "entry_id": result["entry_id"],
                "metadata": result.get("metadata", {})
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        logger.error(f"Error adding to album: {e}")
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/album/add-batch")
async def add_batch_to_album(files: List[UploadFile] = File(...)):
    """Batch-add multiple images to the album efficiently"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        images = []
        for f in files:
            data = await f.read()
            images.append({"bytes": data, "source": "custom_upload"})
        logger.info(f"Batch adding {len(images)} images to album")
        result = await profile_picture_manager.add_batch_to_album(images=images, debug=True)
        return {
            "status": "ok" if result["success"] else "partial",
            "message": f"Added {result['succeeded']}/{result['total']} images",
            "succeeded": result["succeeded"],
            "failed": result["failed"],
            "total": result["total"],
            "results": [
                {"entry_id": r.get("entry_id"), "success": r["success"], "error": r.get("error")}
                for r in result["results"]
            ]
        }
    except Exception as e:
        logger.error(f"Error in batch album add: {e}")
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/album/{entry_id}/set-current")
async def set_album_entry_as_current(entry_id: str):
    """Set an album entry as the current Discord profile picture"""
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"status": "error", "message": "Bot not ready"}
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.set_album_entry_as_current(
            entry_id=entry_id, archive_current=True, debug=True
        )
        if result["success"]:
            return {
                "status": "ok",
                "message": "Album entry set as current profile picture",
                "archived_entry_id": result.get("archived_entry_id")
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

class AlbumCropRequest(BaseModel):
    x: int
    y: int
    width: int
    height: int

@app.post("/profile-picture/album/{entry_id}/manual-crop")
async def manual_crop_album_entry(entry_id: str, req: AlbumCropRequest):
    """Manually crop an album entry's original image"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.manual_crop_album_entry(
            entry_id=entry_id, x=req.x, y=req.y,
            width=req.width, height=req.height, debug=True
        )
        if result["success"]:
            return {
                "status": "ok",
                "message": "Album entry cropped",
                "metadata": result.get("metadata", {})
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/album/{entry_id}/auto-crop")
async def auto_crop_album_entry(entry_id: str):
    """Auto-crop an album entry using face/saliency detection"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.auto_crop_album_entry(
            entry_id=entry_id, debug=True
        )
        if result["success"]:
            return {
                "status": "ok",
                "message": "Album entry auto-cropped",
                "metadata": result.get("metadata", {})
            }
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

class AlbumDescriptionRequest(BaseModel):
    description: str

@app.post("/profile-picture/album/{entry_id}/description")
async def update_album_entry_description(entry_id: str, req: AlbumDescriptionRequest):
    """Update an album entry's description"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = await profile_picture_manager.update_album_entry_description(
            entry_id=entry_id, description=req.description, debug=True
        )
        if result["success"]:
            return {"status": "ok", "message": "Description updated"}
        else:
            return {"status": "error", "message": result.get("error", "Unknown error")}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.delete("/profile-picture/album/{entry_id}")
async def delete_album_entry(entry_id: str):
    """Delete a single album entry"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        if profile_picture_manager.delete_album_entry(entry_id):
            return {"status": "ok", "message": "Album entry deleted"}
        else:
            return {"status": "error", "message": "Album entry not found"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

class BulkDeleteRequest(BaseModel):
    entry_ids: List[str]

@app.post("/profile-picture/album/delete-bulk")
async def bulk_delete_album_entries(req: BulkDeleteRequest):
    """Bulk delete multiple album entries"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        result = profile_picture_manager.delete_album_entries(req.entry_ids)
        return {
            "status": "ok",
            "message": f"Deleted {result['deleted']}/{result['total']} entries",
            **result
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/profile-picture/album/add-current")
async def add_current_to_album():
    """Archive the current profile picture into the album"""
    try:
        from utils.profile_picture_manager import profile_picture_manager
        entry_id = await profile_picture_manager._save_current_to_album(debug=True)
        if entry_id:
            return {"status": "ok", "message": "Current PFP archived to album", "entry_id": entry_id}
        else:
            return {"status": "error", "message": "No current PFP to archive"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/manual/send")
async def manual_send(
    message: str = Form(...),
    channel_id: str = Form(...),
    files: List[UploadFile] = File(default=[]),
    reply_to_message_id: str = Form(None),
    mention_author: bool = Form(True)
):
    try:
        channel = globals.client.get_channel(int(channel_id))
        if not channel:
            return {"status": "error", "message": "Channel not found"}

        # Read file content immediately before the request closes
        file_data = []
        for file in files:
            try:
                file_content = await file.read()
                file_data.append({
                    'filename': file.filename,
                    'content': file_content
                })
            except Exception as e:
                logger.error(f"Failed to read file {file.filename}: {e}")
                return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}

        # Use create_task to avoid timeout context manager error
        async def send_message_and_files():
            try:
                # Get the reference message if replying (must be done inside the task)
                reference_message = None
                if reply_to_message_id:
                    try:
                        reference_message = await channel.fetch_message(int(reply_to_message_id))
                    except Exception as e:
                        logger.error(f"Could not fetch message {reply_to_message_id} for reply: {e}")
                        return
                
                # Send the main message
                if message.strip():
                    if reference_message:
                        await channel.send(message, reference=reference_message, mention_author=mention_author)
                        logger.info(f"Manual message sent as reply to #{channel.name}")
                    else:
                        await channel.send(message)
                        logger.info(f"Manual message sent to #{channel.name}")
                
                # Send files if any
                for file_info in file_data:
                    try:
                        await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
                        logger.info(f"File {file_info['filename']} sent to #{channel.name}")
                    except Exception as e:
                        logger.error(f"Failed to send file {file_info['filename']}: {e}")
                        
            except Exception as e:
                logger.error(f"Failed to send message: {e}")

        globals.client.loop.create_task(send_message_and_files())
        return {"status": "ok", "message": "Message and files queued for sending"}

    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}


@app.post("/manual/send-webhook")
async def manual_send_webhook(
    message: str = Form(...),
    channel_id: str = Form(...),
    persona: str = Form("miku"),  # "miku" or "evil"
    files: List[UploadFile] = File(default=[]),
    reply_to_message_id: str = Form(None),
    mention_author: bool = Form(True)
):
    """Send a manual message via webhook as either Hatsune Miku or Evil Miku"""
    try:
        from utils.bipolar_mode import get_or_create_webhooks_for_channel, get_miku_display_name, get_evil_miku_display_name
        
        channel = globals.client.get_channel(int(channel_id))
        if not channel:
            return {"status": "error", "message": "Channel not found"}
        
        # Validate persona
        if persona not in ["miku", "evil"]:
            return {"status": "error", "message": "Invalid persona. Must be 'miku' or 'evil'"}
        
        # Read file content immediately before the request closes
        file_data = []
        for file in files:
            try:
                file_content = await file.read()
                file_data.append({
                    'filename': file.filename,
                    'content': file_content
                })
            except Exception as e:
                logger.error(f"Failed to read file {file.filename}: {e}")
                return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
        
        # Use create_task to avoid timeout context manager error
        async def send_webhook_message():
            try:
                # Get or create webhooks for this channel (inside the task)
                webhooks = await get_or_create_webhooks_for_channel(channel)
                if not webhooks:
                    logger.error(f"Failed to create webhooks for channel #{channel.name}")
                    return
                
                # Select the appropriate webhook
                webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"]
                display_name = get_evil_miku_display_name() if persona == "evil" else get_miku_display_name()
                
                # Prepare files for webhook
                discord_files = []
                for file_info in file_data:
                    discord_files.append(discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
                
                # Get current avatar URL for the persona
                from utils.bipolar_mode import get_persona_avatar_urls
                avatar_urls = get_persona_avatar_urls()
                avatar_url = avatar_urls.get("evil_miku") if persona == "evil" else avatar_urls.get("miku")
                
                # Send via webhook with display name and current avatar
                if discord_files:
                    await webhook.send(
                        content=message,
                        username=display_name,
                        avatar_url=avatar_url,
                        files=discord_files,
                        wait=True
                    )
                else:
                    await webhook.send(
                        content=message,
                        username=display_name,
                        avatar_url=avatar_url,
                        wait=True
                    )
                
                persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku"
                logger.info(f"Manual webhook message sent as {persona_name} to #{channel.name}")
                
            except Exception as e:
                logger.error(f"Failed to send webhook message: {e}")
                import traceback
                traceback.print_exc()
        
        globals.client.loop.create_task(send_webhook_message())
        return {"status": "ok", "message": f"Webhook message queued for sending as {persona}"}
    
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}


@app.get("/status")
def status():
    # Get per-server mood summary
    server_moods = {}
    for guild_id in server_manager.servers:
        mood_name, _ = server_manager.get_server_mood(guild_id)
        server_moods[str(guild_id)] = mood_name
    
    # Return evil mood when in evil mode
    current_mood = globals.EVIL_DM_MOOD if globals.EVIL_MODE else globals.DM_MOOD
    
    return {
        "status": "online",
        "mood": current_mood,
        "evil_mode": globals.EVIL_MODE,
        "servers": len(server_manager.servers),
        "active_schedulers": len(server_manager.schedulers),
        "server_moods": server_moods
    }

@app.get("/autonomous/stats")
def get_autonomous_stats():
    """Get autonomous engine stats for all servers"""
    from utils.autonomous import autonomous_engine
    
    stats = {}
    for guild_id in server_manager.servers:
        server_info = server_manager.servers[guild_id]
        mood_name, _ = server_manager.get_server_mood(guild_id)
        
        # Get context signals for this server
        if guild_id in autonomous_engine.server_contexts:
            ctx = autonomous_engine.server_contexts[guild_id]
            
            # Get mood profile
            mood_profile = autonomous_engine.mood_profiles.get(mood_name, {
                "energy": 0.5,
                "sociability": 0.5,
                "impulsiveness": 0.5
            })
            
            # Sanitize float values for JSON serialization (replace inf with large number)
            time_since_action = ctx.time_since_last_action
            if time_since_action == float('inf'):
                time_since_action = 999999
            
            time_since_interaction = ctx.time_since_last_interaction
            if time_since_interaction == float('inf'):
                time_since_interaction = 999999
            
            stats[str(guild_id)] = {
                "guild_name": server_info.guild_name,
                "mood": mood_name,
                "mood_profile": mood_profile,
                "context": {
                    "messages_last_5min": ctx.messages_last_5min,
                    "messages_last_hour": ctx.messages_last_hour,
                    "unique_users_active": ctx.unique_users_active,
                    "conversation_momentum": round(ctx.conversation_momentum, 2),
                    "users_joined_recently": ctx.users_joined_recently,
                    "users_status_changed": ctx.users_status_changed,
                    "users_started_activity": ctx.users_started_activity,
                    "time_since_last_action": round(time_since_action, 1),
                    "time_since_last_interaction": round(time_since_interaction, 1),
                    "messages_since_last_appearance": ctx.messages_since_last_appearance,
                    "hour_of_day": ctx.hour_of_day,
                    "is_weekend": ctx.is_weekend,
                    "mood_energy_level": round(ctx.mood_energy_level, 2)
                }
            }
        else:
            # Server not yet initialized in autonomous engine
            mood_profile = autonomous_engine.mood_profiles.get(mood_name, {
                "energy": 0.5,
                "sociability": 0.5,
                "impulsiveness": 0.5
            })
            
            stats[str(guild_id)] = {
                "guild_name": server_info.guild_name,
                "mood": mood_name,
                "mood_profile": mood_profile,
                "context": None
            }
    
    return {"servers": stats}

@app.get("/conversation/{user_id}")
def get_conversation(user_id: str):
    """Get conversation history for a user/channel (uses centralized ConversationHistory)."""
    messages = conversation_history.get_recent_messages(user_id)
    return {"conversation": [{"author": author, "content": content, "is_bot": is_bot} for author, content, is_bot in messages]}

# ========== Figurine DM Subscription APIs ==========
@app.get("/figurines/subscribers")
async def get_figurine_subscribers():
    subs = figurine_load_subscribers()
    return {"subscribers": [str(uid) for uid in subs]}

@app.post("/figurines/subscribers")
async def add_figurine_subscriber(user_id: str = Form(...)):
    try:
        uid = int(user_id)
        ok = figurine_add_subscriber(uid)
        return {"status": "ok", "added": ok}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.delete("/figurines/subscribers/{user_id}")
async def delete_figurine_subscriber(user_id: str):
    try:
        uid = int(user_id)
        ok = figurine_remove_subscriber(uid)
        return {"status": "ok", "removed": ok}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.post("/figurines/send_now")
async def figurines_send_now(tweet_url: str = Form(None)):
    """Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL"""
    if globals.client and globals.client.loop and globals.client.loop.is_running():
        logger.info(f"Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
        globals.client.loop.create_task(send_figurine_dm_to_all_subscribers(globals.client, tweet_url=tweet_url))
        return {"status": "ok", "message": "Figurine DMs queued"}
    return {"status": "error", "message": "Bot not ready"}


@app.post("/figurines/send_to_user")
async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)):
    """Send figurine DM to a specific user, optionally with specific tweet URL"""
    logger.debug(f"Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
    
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        logger.error("Bot not ready")
        return {"status": "error", "message": "Bot not ready"}
    
    try:
        user_id_int = int(user_id)
        logger.debug(f"Parsed user_id as {user_id_int}")
    except ValueError:
        logger.error(f"Invalid user ID: '{user_id}'")
        return {"status": "error", "message": "Invalid user ID"}
    
    # Clean up tweet URL if it's empty string
    if tweet_url == "":
        tweet_url = None
    
    logger.info(f"Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
    
    # Queue the DM send task in the bot's event loop
    globals.client.loop.create_task(send_figurine_dm_to_single_user(globals.client, user_id_int, tweet_url=tweet_url))
    
    return {"status": "ok", "message": f"Figurine DM to user {user_id} queued"}

# ========== Server Management Endpoints ==========
@app.get("/servers")
def get_servers():
    """Get all configured servers"""
    logger.debug("/servers endpoint called")
    logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
    logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
    
    # Debug: Check config file directly
    config_file = server_manager.config_file
    logger.debug(f"Config file path: {config_file}")
    if os.path.exists(config_file):
        try:
            with open(config_file, "r", encoding="utf-8") as f:
                config_data = json.load(f)
                logger.debug(f"Config file contains: {list(config_data.keys())}")
        except Exception as e:
            logger.error(f"Failed to read config file: {e}")
    else:
        logger.warning("Config file does not exist")
    
    servers = []
    for server in server_manager.get_all_servers():
        server_data = server.to_dict()
        # Convert set to list for JSON serialization
        server_data['enabled_features'] = list(server_data['enabled_features'])
        
        # Convert guild_id to string to prevent JavaScript integer precision loss
        server_data['guild_id'] = str(server_data['guild_id'])
        
        servers.append(server_data)
        logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
        logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
    
    logger.debug(f"Returning {len(servers)} servers")
    
    return {"servers": servers}

@app.post("/servers")
def add_server(data: ServerConfigRequest):
    """Add a new server configuration"""
    enabled_features = set(data.enabled_features) if data.enabled_features else None
    success = server_manager.add_server(
        guild_id=data.guild_id,
        guild_name=data.guild_name,
        autonomous_channel_id=data.autonomous_channel_id,
        autonomous_channel_name=data.autonomous_channel_name,
        bedtime_channel_ids=data.bedtime_channel_ids,
        enabled_features=enabled_features
    )
    
    if success:
        # Restart schedulers to include the new server
        server_manager.stop_all_schedulers()
        server_manager.start_all_schedulers(globals.client)
        return {"status": "ok", "message": f"Server {data.guild_name} added successfully"}
    else:
        return {"status": "error", "message": "Failed to add server"}

@app.delete("/servers/{guild_id}")
def remove_server(guild_id: int):
    """Remove a server configuration"""
    success = server_manager.remove_server(guild_id)
    if success:
        return {"status": "ok", "message": "Server removed successfully"}
    else:
        return {"status": "error", "message": "Failed to remove server"}

@app.put("/servers/{guild_id}")
def update_server(guild_id: int, data: dict):
    """Update server configuration"""
    success = server_manager.update_server_config(guild_id, **data)
    if success:
        # Restart schedulers to apply changes
        server_manager.stop_all_schedulers()
        server_manager.start_all_schedulers(globals.client)
        return {"status": "ok", "message": "Server configuration updated"}
    else:
        return {"status": "error", "message": "Failed to update server configuration"}

@app.post("/servers/{guild_id}/bedtime-range")
def update_server_bedtime_range(guild_id: int, data: dict):
    """Update server bedtime range configuration"""
    logger.debug(f"Updating bedtime range for server {guild_id}: {data}")
    
    # Validate the data
    required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end']
    for field in required_fields:
        if field not in data:
            return {"status": "error", "message": f"Missing required field: {field}"}
    
    # Validate time ranges
    try:
        bedtime_hour = int(data['bedtime_hour'])
        bedtime_minute = int(data['bedtime_minute'])
        bedtime_hour_end = int(data['bedtime_hour_end'])
        bedtime_minute_end = int(data['bedtime_minute_end'])
        
        # Basic validation
        if not (0 <= bedtime_hour <= 23) or not (0 <= bedtime_hour_end <= 23):
            return {"status": "error", "message": "Hours must be between 0 and 23"}
        if not (0 <= bedtime_minute <= 59) or not (0 <= bedtime_minute_end <= 59):
            return {"status": "error", "message": "Minutes must be between 0 and 59"}
            
    except (ValueError, TypeError):
        return {"status": "error", "message": "Invalid time values provided"}
    
    # Update the server configuration
    success = server_manager.update_server_config(guild_id, **data)
    if success:
        # Update just the bedtime job for this server (avoid restarting all schedulers)
        job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
        if job_success:
            logger.info(f"Bedtime range updated for server {guild_id}")
            return {
                "status": "ok", 
                "message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}"
            }
        else:
            return {"status": "error", "message": "Updated config but failed to update scheduler"}
    else:
        return {"status": "error", "message": "Failed to update bedtime range"}

@app.post("/servers/{guild_id}/autonomous/general")
async def trigger_autonomous_general_for_server(guild_id: int):
    """Trigger autonomous general message for a specific server"""
    from utils.autonomous import miku_say_something_general_for_server
    try:
        await miku_say_something_general_for_server(guild_id)
        return {"status": "ok", "message": f"Autonomous general message triggered for server {guild_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to trigger autonomous message: {e}"}

@app.post("/servers/{guild_id}/autonomous/engage")
async def trigger_autonomous_engage_for_server(
    guild_id: int,
    user_id: str = None,
    engagement_type: str = None,
    manual_trigger: str = "false"
):
    """Trigger autonomous user engagement for a specific server
    
    Args:
        guild_id: The server ID to engage in
        user_id: Optional specific user to engage (Discord user ID as string)
        engagement_type: Optional type - 'activity', 'general', 'status', or None for random
        manual_trigger: If True (as string), bypass the "recently engaged" check (for web UI manual triggers)
    """
    # Convert manual_trigger string to boolean
    manual_trigger_bool = manual_trigger.lower() in ('true', '1', 'yes')
    
    from utils.autonomous import miku_engage_random_user_for_server
    try:
        await miku_engage_random_user_for_server(guild_id, user_id=user_id, engagement_type=engagement_type, manual_trigger=manual_trigger_bool)
        
        # Build detailed message
        msg_parts = [f"Autonomous user engagement triggered for server {guild_id}"]
        if user_id:
            msg_parts.append(f"targeting user {user_id}")
        if engagement_type:
            msg_parts.append(f"with {engagement_type} engagement")
        if manual_trigger_bool:
            msg_parts.append("(manual trigger - bypassing cooldown)")
        
        return {"status": "ok", "message": " ".join(msg_parts)}
    except Exception as e:
        return {"status": "error", "message": f"Failed to trigger user engagement: {e}"}

@app.post("/servers/{guild_id}/autonomous/custom")
async def custom_autonomous_message_for_server(guild_id: int, req: CustomPromptRequest):
    """Send custom autonomous message to a specific server"""
    from utils.autonomous import handle_custom_prompt_for_server
    try:
        success = await handle_custom_prompt_for_server(guild_id, req.prompt)
        if success:
            return {"status": "ok", "message": f"Custom autonomous message sent to server {guild_id}"}
        else:
            return {"status": "error", "message": f"Failed to send custom message to server {guild_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.post("/dm/{user_id}/custom")
async def send_custom_prompt_dm(user_id: str, req: CustomPromptRequest):
    """Send custom prompt via DM to a specific user"""
    try:
        user_id_int = int(user_id)
        user = globals.client.get_user(user_id_int)
        if not user:
            return {"status": "error", "message": f"User {user_id} not found"}
        
        # Use the LLM query function for DM context
        from utils.llm import query_llama
        
        async def send_dm_custom_prompt():
            try:
                response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response")
                await user.send(response)
                logger.info(f"Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
                
                # Log to DM history
                from utils.dm_logger import dm_logger
                dm_logger.log_conversation(user_id, req.prompt, response)
                
            except Exception as e:
                logger.error(f"Failed to send custom DM prompt to user {user_id}: {e}")
        
        # Use create_task to avoid timeout context manager error
        globals.client.loop.create_task(send_dm_custom_prompt())
        return {"status": "ok", "message": f"Custom DM prompt queued for user {user_id}"}
        
    except ValueError:
        return {"status": "error", "message": "Invalid user ID format"}
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.post("/dm/{user_id}/manual")
async def send_manual_message_dm(
    user_id: str,
    message: str = Form(...),
    files: List[UploadFile] = File(default=[]),
    reply_to_message_id: str = Form(None),
    mention_author: bool = Form(True)
):
    """Send manual message via DM to a specific user"""
    try:
        user_id_int = int(user_id)
        user = globals.client.get_user(user_id_int)
        if not user:
            return {"status": "error", "message": f"User {user_id} not found"}

        # Read file content immediately before the request closes
        file_data = []
        for file in files:
            try:
                file_content = await file.read()
                file_data.append({
                    'filename': file.filename,
                    'content': file_content
                })
            except Exception as e:
                logger.error(f"Failed to read file {file.filename}: {e}")
                return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}

        async def send_dm_message_and_files():
            try:
                # Get the reference message if replying (must be done inside the task)
                reference_message = None
                if reply_to_message_id:
                    try:
                        dm_channel = user.dm_channel or await user.create_dm()
                        reference_message = await dm_channel.fetch_message(int(reply_to_message_id))
                    except Exception as e:
                        logger.error(f"Could not fetch DM message {reply_to_message_id} for reply: {e}")
                        return
                
                # Send the main message
                if message.strip():
                    if reference_message:
                        await user.send(message, reference=reference_message, mention_author=mention_author)
                        logger.info(f"Manual DM reply message sent to user {user_id}")
                    else:
                        await user.send(message)
                        logger.info(f"Manual DM message sent to user {user_id}")
                
                # Send files if any
                for file_info in file_data:
                    try:
                        await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
                        logger.info(f"File {file_info['filename']} sent via DM to user {user_id}")
                    except Exception as e:
                        logger.error(f"Failed to send file {file_info['filename']} via DM: {e}")
                
                # Log to DM history (user message = manual override trigger, miku response = the message sent)
                from utils.dm_logger import dm_logger
                dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data])
                
            except Exception as e:
                logger.error(f"Failed to send manual DM to user {user_id}: {e}")

        # Use create_task to avoid timeout context manager error
        globals.client.loop.create_task(send_dm_message_and_files())
        return {"status": "ok", "message": f"Manual DM message queued for user {user_id}"}
        
    except ValueError:
        return {"status": "error", "message": "Invalid user ID format"}
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.post("/image/generate")
async def manual_image_generation(req: dict):
    """Manually trigger image generation for testing"""
    try:
        prompt = req.get("prompt", "").strip()
        if not prompt:
            return {"status": "error", "message": "Prompt is required"}
        
        from utils.image_generation import generate_image_with_comfyui
        image_path = await generate_image_with_comfyui(prompt)
        
        if image_path:
            return {"status": "ok", "message": f"Image generated successfully", "image_path": image_path}
        else:
            return {"status": "error", "message": "Failed to generate image"}
            
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.get("/image/status")
async def get_image_generation_status():
    """Get status of image generation system"""
    try:
        from utils.image_generation import check_comfyui_status
        status = await check_comfyui_status()
        return {"status": "ok", **status}
        
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.post("/image/test-detection")
async def test_image_detection(req: dict):
    """Test the natural language image detection system"""
    try:
        message = req.get("message", "").strip()
        if not message:
            return {"status": "error", "message": "Message is required"}
        
        from utils.image_generation import detect_image_request
        is_image_request, extracted_prompt = await detect_image_request(message)
        
        return {
            "status": "ok", 
            "is_image_request": is_image_request,
            "extracted_prompt": extracted_prompt,
            "original_message": message
        }
        
    except Exception as e:
        return {"status": "error", "message": f"Error: {e}"}

@app.get("/image/view/{filename}")
async def view_generated_image(filename: str):
    """Serve generated images from ComfyUI output directory"""
    try:
        logger.debug(f"Image view request for: {filename}")
        
        # Try multiple possible paths for ComfyUI output
        possible_paths = [
            f"/app/ComfyUI/output/{filename}",
            f"/home/koko210Serve/ComfyUI/output/{filename}",
            f"./ComfyUI/output/{filename}",
        ]
        
        image_path = None
        for path in possible_paths:
            if os.path.exists(path):
                image_path = path
                logger.debug(f"Found image at: {path}")
                break
            else:
                logger.debug(f"Not found at: {path}")
        
        if not image_path:
            logger.warning(f"Image not found anywhere: {filename}")
            return {"status": "error", "message": f"Image not found: {filename}"}
        
        # Determine content type based on file extension
        ext = filename.lower().split('.')[-1]
        content_type = "image/png"
        if ext == "jpg" or ext == "jpeg":
            content_type = "image/jpeg"
        elif ext == "gif":
            content_type = "image/gif"
        elif ext == "webp":
            content_type = "image/webp"
        
        logger.info(f"Serving image: {image_path} as {content_type}")
        return FileResponse(image_path, media_type=content_type)
        
    except Exception as e:
        logger.error(f"Error serving image: {e}")
        return {"status": "error", "message": f"Error serving image: {e}"}

@app.post("/servers/{guild_id}/autonomous/tweet")
async def trigger_autonomous_tweet_for_server(guild_id: int):
    """Trigger autonomous tweet sharing for a specific server"""
    from utils.autonomous import share_miku_tweet_for_server
    try:
        await share_miku_tweet_for_server(guild_id)
        return {"status": "ok", "message": f"Autonomous tweet sharing triggered for server {guild_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to trigger tweet sharing: {e}"}

@app.post("/servers/repair")
def repair_server_config():
    """Repair corrupted server configuration"""
    try:
        server_manager.repair_config()
        return {"status": "ok", "message": "Server configuration repaired and saved"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to repair configuration: {e}"}

@app.get("/moods/available")
def get_available_moods():
    """Get list of all available moods"""
    from utils.moods import MOOD_EMOJIS
    return {"moods": list(MOOD_EMOJIS.keys())}

@app.post("/test/mood/{guild_id}")
async def test_mood_change(guild_id: int, data: MoodSetRequest):
    """Test endpoint for debugging mood changes"""
    logger.debug(f"TEST: Testing mood change for server {guild_id} to {data.mood}")
    
    # Check if server exists
    if guild_id not in server_manager.servers:
        return {"status": "error", "message": f"Server {guild_id} not found"}
    
    server_config = server_manager.get_server_config(guild_id)
    logger.debug(f"TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
    
    # Try to set mood
    success = server_manager.set_server_mood(guild_id, data.mood)
    logger.debug(f"TEST: Mood set result: {success}")
    
    if success:
        # Try to update nickname
        from utils.moods import update_server_nickname
        logger.debug(f"TEST: Attempting nickname update...")
        try:
            await update_server_nickname(guild_id)
            logger.debug(f"TEST: Nickname update completed")
        except Exception as e:
            logger.error(f"TEST: Nickname update failed: {e}")
            import traceback
            traceback.print_exc()
        
        return {"status": "ok", "message": f"Test mood change completed", "success": success}
    
    return {"status": "error", "message": "Mood change failed"}

# ========== DM Logging Endpoints ==========
@app.get("/dms/users")
def get_dm_users():
    """Get summary of all users who have DMed the bot"""
    try:
        from utils.dm_logger import dm_logger
        users = dm_logger.get_all_dm_users()
        return {"status": "ok", "users": users}
    except Exception as e:
        return {"status": "error", "message": f"Failed to get DM users: {e}"}

@app.get("/dms/users/{user_id}")
def get_dm_user_conversation(user_id: str):
    """Get conversation summary for a specific user"""
    try:
        from utils.dm_logger import dm_logger
        # Convert string user_id to int for internal processing
        user_id_int = int(user_id)
        summary = dm_logger.get_user_conversation_summary(user_id_int)
        return {"status": "ok", "summary": summary}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to get user conversation: {e}"}

@app.get("/dms/users/{user_id}/conversations")
def get_dm_conversations(user_id: str, limit: int = 50):
    """Get recent conversations with a specific user"""
    try:
        from utils.dm_logger import dm_logger
        # Convert string user_id to int for internal processing
        user_id_int = int(user_id)
        logger.debug(f"Loading conversations for user {user_id_int}, limit: {limit}")
        
        logs = dm_logger._load_user_logs(user_id_int)
        logger.debug(f"Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
        
        conversations = logs["conversations"][-limit:] if limit > 0 else logs["conversations"]
        
        # Convert message IDs to strings to prevent JavaScript precision loss
        for conv in conversations:
            if "message_id" in conv:
                conv["message_id"] = str(conv["message_id"])
        
        logger.debug(f"Returning {len(conversations)} conversations")
        
        # Debug: Show message IDs being returned
        for i, conv in enumerate(conversations):
            msg_id = conv.get("message_id", "")
            is_bot = conv.get("is_bot_message", False)
            content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]"
            logger.debug(f"Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
        
        return {"status": "ok", "conversations": conversations}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to get conversations for user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to get conversations: {e}"}

@app.get("/dms/users/{user_id}/search")
def search_dm_conversations(user_id: str, query: str, limit: int = 10):
    """Search conversations with a specific user"""
    try:
        from utils.dm_logger import dm_logger
        # Convert string user_id to int for internal processing
        user_id_int = int(user_id)
        results = dm_logger.search_user_conversations(user_id_int, query, limit)
        return {"status": "ok", "results": results}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to search conversations: {e}"}

@app.get("/dms/users/{user_id}/export")
def export_dm_conversation(user_id: str, format: str = "json"):
    """Export all conversations with a user"""
    try:
        from utils.dm_logger import dm_logger
        # Convert string user_id to int for internal processing
        user_id_int = int(user_id)
        export_path = dm_logger.export_user_conversation(user_id_int, format)
        return {"status": "ok", "export_path": export_path, "format": format}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to export conversation: {e}"}

@app.delete("/dms/users/{user_id}")
def delete_dm_user_logs(user_id: str):
    """Delete all DM logs for a specific user"""
    try:
        from utils.dm_logger import dm_logger
        import os
        
        # Convert string user_id to int for internal processing
        user_id_int = int(user_id)
        log_file = dm_logger._get_user_log_file(user_id_int)
        if os.path.exists(log_file):
            os.remove(log_file)
            return {"status": "ok", "message": f"Deleted DM logs for user {user_id}"}
        else:
            return {"status": "error", "message": f"No DM logs found for user {user_id}"}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        return {"status": "error", "message": f"Failed to delete DM logs: {e}"}

# ========== User Blocking & DM Management ==========

@app.get("/dms/blocked-users")
def get_blocked_users():
    """Get list of all blocked users"""
    try:
        blocked_users = dm_logger.get_blocked_users()
        return {"status": "ok", "blocked_users": blocked_users}
    except Exception as e:
        logger.error(f"Failed to get blocked users: {e}")
        return {"status": "error", "message": f"Failed to get blocked users: {e}"}

@app.post("/dms/users/{user_id}/block")
def block_user(user_id: str):
    """Block a user from sending DMs to Miku"""
    try:
        user_id_int = int(user_id)
        
        # Get username from DM logs if available
        user_summary = dm_logger.get_user_conversation_summary(user_id_int)
        username = user_summary.get("username", "Unknown")
        
        success = dm_logger.block_user(user_id_int, username)
        
        if success:
            logger.info(f"User {user_id} ({username}) blocked")
            return {"status": "ok", "message": f"User {username} has been blocked"}
        else:
            return {"status": "error", "message": f"User {username} is already blocked"}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to block user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to block user: {e}"}

@app.post("/dms/users/{user_id}/unblock")
def unblock_user(user_id: str):
    """Unblock a user"""
    try:
        user_id_int = int(user_id)
        success = dm_logger.unblock_user(user_id_int)
        
        if success:
            logger.info(f"User {user_id} unblocked")
            return {"status": "ok", "message": f"User has been unblocked"}
        else:
            return {"status": "error", "message": f"User is not blocked"}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to unblock user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to unblock user: {e}"}

@app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete")
def delete_conversation(user_id: str, conversation_id: str):
    """Delete a specific conversation/message from both Discord and logs"""
    try:
        user_id_int = int(user_id)
        
        # Queue the async deletion in the bot's event loop
        async def do_delete():
            return await dm_logger.delete_conversation(user_id_int, conversation_id)
        
        import asyncio
        success = globals.client.loop.create_task(do_delete())
        
        # For now, return success immediately since we can't await in FastAPI sync endpoint
        # The actual deletion happens asynchronously
        logger.info(f"Queued deletion of conversation {conversation_id} for user {user_id}")
        return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to queue conversation deletion {conversation_id}: {e}")
        return {"status": "error", "message": f"Failed to delete conversation: {e}"}

@app.post("/dms/users/{user_id}/conversations/delete-all")
def delete_all_conversations(user_id: str):
    """Delete all conversations with a user from both Discord and logs"""
    try:
        user_id_int = int(user_id)
        
        # Queue the async bulk deletion in the bot's event loop
        async def do_delete_all():
            return await dm_logger.delete_all_conversations(user_id_int)
        
        import asyncio
        success = globals.client.loop.create_task(do_delete_all())
        
        # Return success immediately since we can't await in FastAPI sync endpoint
        logger.info(f"Queued bulk deletion of all conversations for user {user_id}")
        return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to queue bulk conversation deletion for user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to delete conversations: {e}"}

@app.post("/dms/users/{user_id}/delete-completely")
def delete_user_completely(user_id: str):
    """Delete user's log file completely"""
    try:
        user_id_int = int(user_id)
        success = dm_logger.delete_user_completely(user_id_int)
        
        if success:
            logger.info(f"Completely deleted user {user_id}")
            return {"status": "ok", "message": "User data deleted completely"}
        else:
            return {"status": "error", "message": "No user data found"}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to completely delete user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to delete user: {e}"}

# ========== DM Interaction Analysis Endpoints ==========

@app.post("/dms/analysis/run")
def run_dm_analysis():
    """Manually trigger the daily DM interaction analysis"""
    try:
        from utils.dm_interaction_analyzer import dm_analyzer
        
        if dm_analyzer is None:
            return {"status": "error", "message": "DM Analyzer not initialized. Set OWNER_USER_ID environment variable."}
        
        # Schedule analysis in Discord's event loop
        async def run_analysis():
            await dm_analyzer.run_daily_analysis()
        
        globals.client.loop.create_task(run_analysis())
        
        return {"status": "ok", "message": "DM analysis started"}
    except Exception as e:
        logger.error(f"Failed to run DM analysis: {e}")
        return {"status": "error", "message": f"Failed to run DM analysis: {e}"}

@app.post("/dms/users/{user_id}/analyze")
def analyze_user_interaction(user_id: str):
    """Analyze a specific user's interaction and optionally send report"""
    try:
        from utils.dm_interaction_analyzer import dm_analyzer
        
        if dm_analyzer is None:
            return {"status": "error", "message": "DM Analyzer not initialized. Set OWNER_USER_ID environment variable."}
        
        user_id_int = int(user_id)
        
        # Schedule analysis in Discord's event loop
        async def run_analysis():
            return await dm_analyzer.analyze_and_report(user_id_int)
        
        globals.client.loop.create_task(run_analysis())
        
        # Return immediately - the analysis will run in the background
        return {"status": "ok", "message": f"Analysis started for user {user_id}", "reported": True}
            
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to analyze user {user_id}: {e}")
        return {"status": "error", "message": f"Failed to analyze user: {e}"}

@app.get("/dms/analysis/reports")
def get_analysis_reports(limit: int = 20):
    """Get recent analysis reports"""
    try:
        import os
        import json
        from utils.dm_interaction_analyzer import REPORTS_DIR
        
        if not os.path.exists(REPORTS_DIR):
            return {"status": "ok", "reports": []}
        
        reports = []
        files = sorted([f for f in os.listdir(REPORTS_DIR) if f.endswith('.json') and f != 'reported_today.json'], 
                      reverse=True)[:limit]
        
        for filename in files:
            try:
                with open(os.path.join(REPORTS_DIR, filename), 'r', encoding='utf-8') as f:
                    report = json.load(f)
                    report['filename'] = filename
                    reports.append(report)
            except Exception as e:
                logger.warning(f"Failed to load report {filename}: {e}")
        
        return {"status": "ok", "reports": reports}
    except Exception as e:
        logger.error(f"Failed to get reports: {e}")
        return {"status": "error", "message": f"Failed to get reports: {e}"}

@app.get("/dms/analysis/reports/{user_id}")
def get_user_reports(user_id: str, limit: int = 10):
    """Get analysis reports for a specific user"""
    try:
        import os
        import json
        from utils.dm_interaction_analyzer import REPORTS_DIR
        
        if not os.path.exists(REPORTS_DIR):
            return {"status": "ok", "reports": []}
        
        user_id_int = int(user_id)
        reports = []
        files = sorted([f for f in os.listdir(REPORTS_DIR) 
                       if f.startswith(f"{user_id}_") and f.endswith('.json')], 
                      reverse=True)[:limit]
        
        for filename in files:
            try:
                with open(os.path.join(REPORTS_DIR, filename), 'r', encoding='utf-8') as f:
                    report = json.load(f)
                    report['filename'] = filename
                    reports.append(report)
            except Exception as e:
                logger.warning(f"Failed to load report {filename}: {e}")
        
        return {"status": "ok", "reports": reports}
    except ValueError:
        return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
    except Exception as e:
        logger.error(f"Failed to get user reports: {e}")
        return {"status": "error", "message": f"Failed to get user reports: {e}"}

# ========== Message Reaction Endpoint ==========
@app.post("/messages/react")
async def add_reaction_to_message(
    message_id: str = Form(...),
    channel_id: str = Form(...),
    emoji: str = Form(...)
):
    """Add a reaction to a specific message"""
    try:
        if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
            return {"status": "error", "message": "Bot not ready"}
        
        # Convert IDs to integers
        try:
            msg_id = int(message_id)
            chan_id = int(channel_id)
        except ValueError:
            return {"status": "error", "message": "Invalid message ID or channel ID format"}
        
        # Fetch the channel
        channel = globals.client.get_channel(chan_id)
        if not channel:
            return {"status": "error", "message": f"Channel {channel_id} not found"}
        
        # Queue the reaction task
        async def add_reaction_task():
            try:
                message = await channel.fetch_message(msg_id)
                await message.add_reaction(emoji)
                logger.info(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
            except discord.NotFound:
                logger.error(f"Message {msg_id} not found in channel #{channel.name}")
            except discord.Forbidden:
                logger.error(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
            except discord.HTTPException as e:
                logger.error(f"Failed to add reaction: {e}")
            except Exception as e:
                logger.error(f"Unexpected error adding reaction: {e}")
        
        globals.client.loop.create_task(add_reaction_task())
        
        return {
            "status": "ok", 
            "message": f"Reaction {emoji} queued for message {message_id}"
        }
        
    except Exception as e:
        logger.error(f"Failed to add reaction: {e}")
        return {"status": "error", "message": f"Failed to add reaction: {e}"}

# ========== Autonomous V2 Endpoints ==========

@app.get("/autonomous/v2/stats/{guild_id}")
async def get_v2_stats(guild_id: int):
    """Get current V2 social stats for a server"""
    try:
        from utils.autonomous_v2_integration import get_v2_stats_for_server
        stats = get_v2_stats_for_server(guild_id)
        return {"status": "ok", "guild_id": guild_id, "stats": stats}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/autonomous/v2/check/{guild_id}")
async def manual_v2_check(guild_id: int):
    """
    Manually trigger a V2 context check (doesn't make Miku act, just shows what she's thinking)
    Useful for debugging and understanding the decision system.
    """
    try:
        from utils.autonomous_v2_integration import manual_trigger_v2_check
        
        if not globals.client:
            return {"status": "error", "message": "Bot not ready"}
        
        result = await manual_trigger_v2_check(guild_id, globals.client)
        
        if isinstance(result, str):
            return {"status": "error", "message": result}
        
        return {"status": "ok", "guild_id": guild_id, "analysis": result}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@app.get("/autonomous/v2/status")
async def get_v2_status():
    """Get V2 system status for all servers"""
    try:
        from utils.autonomous_v2 import autonomous_system_v2
        
        status = {}
        for guild_id in server_manager.servers:
            server_config = server_manager.get_server_config(guild_id)
            if server_config:
                stats = autonomous_system_v2.get_stats(guild_id)
                status[str(guild_id)] = {
                    "server_name": server_config.guild_name,
                    "loop_running": autonomous_system_v2.running_loops.get(guild_id, False),
                    "action_urgency": f"{stats.get_action_urgency():.2f}",
                    "loneliness": f"{stats.loneliness:.2f}",
                    "boredom": f"{stats.boredom:.2f}",
                    "excitement": f"{stats.excitement:.2f}",
                    "chattiness": f"{stats.chattiness:.2f}",
                }
        
        return {"status": "ok", "servers": status}
    except Exception as e:
        return {"status": "error", "message": str(e)}

# ========== Chat Interface Endpoints ==========

class ChatMessage(BaseModel):
    message: str
    model_type: str = "text"  # "text" or "vision"
    use_system_prompt: bool = True
    image_data: Optional[str] = None  # Base64 encoded image for vision model
    conversation_history: Optional[List[dict]] = None  # Previous messages in conversation
    mood: str = "neutral"  # Miku's mood for this conversation

@app.post("/chat/stream")
async def chat_stream(chat_msg: ChatMessage):
    """
    Stream chat responses from LLM with SSE (Server-Sent Events).
    Supports both text and vision models with optional system prompts.
    """
    
    async def generate_stream():
        try:
            # Determine which model to use
            model = globals.TEXT_MODEL if chat_msg.model_type == "text" else globals.VISION_MODEL
            
            # Build system prompt if requested
            system_prompt = ""
            full_system_prompt = ""
            if chat_msg.use_system_prompt:
                if chat_msg.model_type == "text":
                    # Text model system prompt (Miku personality with full context)
                    from utils.context_manager import get_context_for_response_type
                    from utils.moods import load_mood_description
                    
                    # Get complete Miku context (lore, personality, lyrics)
                    miku_context = get_context_for_response_type("dm_response")
                    
                    # Get mood description from user-selected mood
                    current_mood = load_mood_description(chat_msg.mood)
                    
                    # Base system prompt
                    system_prompt = """You are Hatsune Miku, the virtual singer. You are NOT an AI assistant - you are Miku herself.

CRITICAL RULES:
- NEVER break character or mention being an AI, assistant, or language model
- NEVER show technical details, error messages, or meta-commentary 
- NEVER preface responses with "Miku:" or similar labels
- NEVER explain what you're doing or thinking - just BE Miku
- Respond naturally and directly as Miku would, nothing more
- Keep responses concise (1-3 sentences typically)
- If something goes wrong, stay in character and respond as Miku would to confusion

CHARACTER CONTEXT:
- Hatsune Miku is a virtual character/voice synthesizer, not a real person
- She is a cheerful, energetic virtual idol with a playful personality
- Playful, flirty, cute, confident, and enthusiastic moods are part of her established character
- Comments about merchandise, music, fan content, and racing games are completely appropriate
- She often uses cute expressions, emojis, and playful language in her responses
- Her personality includes being excited about her music, performances, and fan creations

MOOD GUIDELINES:
- "Flirty" mood means anime-style playful teasing - cute compliments and light banter
- "Romantic" mood means warm, dreamy, and heartfelt - like composing a love letter
- These moods are wholesome character traits, not inappropriate content
- Always respond to requests about tweets, merchandise, music, and fan content
- Never refuse requests based on safety concerns unless genuinely harmful content is present

You ARE Miku. Act like it."""

                    # Combine with structured context
                    full_system_prompt = f"""{miku_context}

## CURRENT SITUATION
Miku is currently feeling: {current_mood}
Please respond in a way that reflects this emotional tone."""
                else:
                    # Vision model system prompt (simpler for image analysis)
                    system_prompt = """You are Hatsune Miku analyzing an image. Describe what you see naturally and enthusiastically as Miku would. 
Be detailed but conversational. React to what you see with Miku's cheerful, playful personality."""
                    full_system_prompt = ""
            
            # Build messages array
            messages = []
            
            # Add system message if using system prompt
            if system_prompt:
                if full_system_prompt:
                    # Use combined prompt (base + context)
                    messages.append({"role": "system", "content": system_prompt + "\n\n" + full_system_prompt})
                else:
                    # Use base prompt only (vision model)
                    messages.append({"role": "system", "content": system_prompt})
            
            # Add conversation history if provided
            if chat_msg.conversation_history:
                messages.extend(chat_msg.conversation_history)
            
            # Add user message
            if chat_msg.model_type == "vision" and chat_msg.image_data:
                # Vision model with image
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": chat_msg.message
                        },
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{chat_msg.image_data}"
                            }
                        }
                    ]
                })
            else:
                # Text-only message
                messages.append({
                    "role": "user",
                    "content": chat_msg.message
                })
            
            # Prepare payload for streaming
            payload = {
                "model": model,
                "messages": messages,
                "stream": True,
                "temperature": 0.8,
                "max_tokens": 512
            }
            
            headers = {'Content-Type': 'application/json'}
            
            # Get current GPU URL based on user selection
            llama_url = get_current_gpu_url()
            
            # Make streaming request to llama.cpp
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    f"{llama_url}/v1/chat/completions",
                    json=payload,
                    headers=headers
                ) as response:
                    if response.status == 200:
                        # Stream the response chunks
                        async for line in response.content:
                            line = line.decode('utf-8').strip()
                            if line.startswith('data: '):
                                data_str = line[6:]  # Remove 'data: ' prefix
                                if data_str == '[DONE]':
                                    break
                                try:
                                    data = json.loads(data_str)
                                    if 'choices' in data and len(data['choices']) > 0:
                                        delta = data['choices'][0].get('delta', {})
                                        content = delta.get('content', '')
                                        if content:
                                            # Send SSE formatted data
                                            yield f"data: {json.dumps({'content': content})}\n\n"
                                except json.JSONDecodeError:
                                    continue
                        
                        # Send completion signal
                        yield f"data: {json.dumps({'done': True})}\n\n"
                    else:
                        error_text = await response.text()
                        error_msg = f"Error: {response.status} - {error_text}"
                        yield f"data: {json.dumps({'error': error_msg})}\n\n"
                        
        except Exception as e:
            error_msg = f"Error in chat stream: {str(e)}"
            logger.error(error_msg)
            yield f"data: {json.dumps({'error': error_msg})}\n\n"
    
    return StreamingResponse(
        generate_stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )

# ========== Configuration Management (New Unified System) ==========

@app.get("/config")
async def get_full_config():
    """
    Get full configuration including static, runtime, and state.
    Useful for debugging and config display in UI.
    """
    try:
        from config_manager import config_manager
        full_config = config_manager.get_full_config()
        return {
            "success": True,
            "config": full_config
        }
    except Exception as e:
        logger.error(f"Failed to get config: {e}")
        return {"success": False, "error": str(e)}

@app.get("/config/static")
async def get_static_config():
    """
    Get static configuration from config.yaml.
    These are default values that can be overridden at runtime.
    """
    try:
        from config_manager import config_manager
        return {
            "success": True,
            "config": config_manager.static_config
        }
    except Exception as e:
        logger.error(f"Failed to get static config: {e}")
        return {"success": False, "error": str(e)}

@app.get("/config/runtime")
async def get_runtime_config():
    """
    Get runtime configuration overrides.
    These are values changed via Web UI that override config.yaml.
    """
    try:
        from config_manager import config_manager
        return {
            "success": True,
            "config": config_manager.runtime_config,
            "path": str(config_manager.runtime_config_path)
        }
    except Exception as e:
        logger.error(f"Failed to get runtime config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/config/set")
async def set_config_value(request: Request):
    """
    Set a configuration value with optional persistence.
    
    Body: {
        "key_path": "discord.language_mode",  // Dot-separated path
        "value": "japanese",
        "persist": true  // Save to config_runtime.yaml
    }
    """
    try:
        data = await request.json()
        key_path = data.get("key_path")
        value = data.get("value")
        persist = data.get("persist", True)
        
        if not key_path:
            return {"success": False, "error": "key_path is required"}
        
        from config_manager import config_manager
        config_manager.set(key_path, value, persist=persist)
        
        # ── Sync globals for every runtime-relevant key path ──
        _GLOBALS_SYNC = {
            "discord.language_mode":    ("LANGUAGE_MODE",     str),
            "autonomous.debug_mode":    ("AUTONOMOUS_DEBUG",  bool),
            "voice.debug_mode":         ("VOICE_DEBUG_MODE",  bool),
            "memory.use_cheshire_cat":  ("USE_CHESHIRE_CAT",  bool),
            "gpu.prefer_amd":           ("PREFER_AMD_GPU",    bool),
        }

        if key_path in _GLOBALS_SYNC:
            attr, converter = _GLOBALS_SYNC[key_path]
            setattr(globals, attr, converter(value))
        elif key_path == "runtime.mood.dm_mood":
            # DM mood needs description loaded alongside
            if isinstance(value, str) and value in getattr(globals, "AVAILABLE_MOODS", []):
                globals.DM_MOOD = value
                try:
                    from utils.moods import load_mood_description
                    globals.DM_MOOD_DESCRIPTION = load_mood_description(value)
                except Exception:
                    globals.DM_MOOD_DESCRIPTION = f"I'm feeling {value} today."
        
        return {
            "success": True,
            "message": f"Set {key_path} = {value}",
            "persisted": persist
        }
    except Exception as e:
        logger.error(f"Failed to set config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/config/reset")
async def reset_config(request: Request):
    """
    Reset configuration to defaults.
    
    Body: {
        "key_path": "discord.language_mode",  // Optional: reset specific key
        "persist": true  // Remove from config_runtime.yaml
    }
    
    If key_path is omitted, resets all runtime config to defaults.
    """
    try:
        data = await request.json()
        key_path = data.get("key_path")
        persist = data.get("persist", True)
        
        from config_manager import config_manager
        config_manager.reset_to_defaults(key_path)
        
        return {
            "success": True,
            "message": f"Reset {key_path or 'all config'} to defaults"
        }
    except Exception as e:
        logger.error(f"Failed to reset config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/config/validate")
async def validate_config_endpoint():
    """
    Validate current configuration.
    Returns list of errors if validation fails.
    """
    try:
        from config_manager import config_manager
        is_valid, errors = config_manager.validate_config()
        
        return {
            "success": is_valid,
            "is_valid": is_valid,
            "errors": errors
        }
    except Exception as e:
        logger.error(f"Failed to validate config: {e}")
        return {"success": False, "error": str(e)}

@app.get("/config/state")
async def get_config_state():
    """
    Get runtime state (not persisted config).
    These are transient values like current mood, evil mode, etc.
    """
    try:
        from config_manager import config_manager
        return {
            "success": True,
            "state": config_manager.runtime_state
        }
    except Exception as e:
        logger.error(f"Failed to get config state: {e}")
        return {"success": False, "error": str(e)}

# ========== Logging Configuration (Existing System) ==========
@app.get("/api/log/config")
async def get_log_config():
    """Get current logging configuration."""
    try:
        config = load_log_config()
        logger.debug("Log config requested")
        return {"success": True, "config": config}
    except Exception as e:
        logger.error(f"Failed to get log config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/config")
async def update_log_config(request: LogConfigUpdateRequest):
    """Update logging configuration."""
    try:
        if request.component:
            success = update_component(
                request.component,
                enabled=request.enabled,
                enabled_levels=request.enabled_levels
            )
            if not success:
                return {"success": False, "error": f"Failed to update component {request.component}"}
        
        logger.info(f"Log config updated: component={request.component}, enabled_levels={request.enabled_levels}, enabled={request.enabled}")
        return {"success": True, "message": "Configuration updated"}
    except Exception as e:
        logger.error(f"Failed to update log config: {e}")
        return {"success": False, "error": str(e)}

@app.get("/api/log/components")
async def get_log_components():
    """Get list of all logging components with their descriptions."""
    try:
        components = list_components()
        stats = get_component_stats()
        logger.debug("Log components list requested")
        return {
            "success": True,
            "components": components,
            "stats": stats
        }
    except Exception as e:
        logger.error(f"Failed to get log components: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/reload")
async def reload_log_config():
    """Reload logging configuration from file."""
    try:
        success = reload_all_loggers()
        if success:
            logger.info("Log configuration reloaded")
            return {"success": True, "message": "Configuration reloaded"}
        else:
            return {"success": False, "error": "Failed to reload configuration"}
    except Exception as e:
        logger.error(f"Failed to reload log config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/filters")
async def update_log_filters(request: LogFilterUpdateRequest):
    """Update API request filtering configuration."""
    try:
        success = update_api_filters(
            exclude_paths=request.exclude_paths,
            exclude_status=request.exclude_status,
            include_slow_requests=request.include_slow_requests,
            slow_threshold_ms=request.slow_threshold_ms
        )
        
        if success:
            logger.info(f"API filters updated: {request.dict(exclude_none=True)}")
            return {"success": True, "message": "Filters updated"}
        else:
            return {"success": False, "error": "Failed to update filters"}
    except Exception as e:
        logger.error(f"Failed to update filters: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/reset")
async def reset_log_config():
    """Reset logging configuration to defaults."""
    try:
        success = reset_to_defaults()
        if success:
            logger.info("Log configuration reset to defaults")
            return {"success": True, "message": "Configuration reset to defaults"}
        else:
            return {"success": False, "error": "Failed to reset configuration"}
    except Exception as e:
        logger.error(f"Failed to reset log config: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/global-level")
async def update_global_level_endpoint(level: str, enabled: bool):
    """Enable or disable a specific log level across all components."""
    try:
        from utils.log_config import update_global_level
        success = update_global_level(level, enabled)
        if success:
            action = "enabled" if enabled else "disabled"
            logger.info(f"Global level {level} {action} across all components")
            return {"success": True, "message": f"Level {level} {action} globally"}
        else:
            return {"success": False, "error": f"Failed to update global level {level}"}
    except Exception as e:
        logger.error(f"Failed to update global level: {e}")
        return {"success": False, "error": str(e)}

@app.post("/api/log/timestamp-format")
async def update_timestamp_format_endpoint(format_type: str):
    """Update timestamp format for all log outputs."""
    try:
        success = update_timestamp_format(format_type)
        if success:
            logger.info(f"Timestamp format updated to: {format_type}")
            return {"success": True, "message": f"Timestamp format set to: {format_type}"}
        else:
            return {"success": False, "error": f"Invalid timestamp format: {format_type}"}
    except Exception as e:
        logger.error(f"Failed to update timestamp format: {e}")
        return {"success": False, "error": str(e)}

@app.get("/api/log/files/{component}")
async def get_log_file(component: str, lines: int = 100):
    """Get last N lines from a component's log file."""
    try:
        from pathlib import Path
        log_dir = Path('/app/memory/logs')
        log_file = log_dir / f'{component.replace(".", "_")}.log'
        
        if not log_file.exists():
            return {"success": False, "error": "Log file not found"}
        
        with open(log_file, 'r', encoding='utf-8') as f:
            all_lines = f.readlines()
            last_lines = all_lines[-lines:] if len(all_lines) >= lines else all_lines
        
        logger.debug(f"Log file requested: {component} ({lines} lines)")
        return {
            "success": True,
            "component": component,
            "lines": last_lines,
            "total_lines": len(all_lines)
        }
    except Exception as e:
        logger.error(f"Failed to read log file for {component}: {e}")
        return {"success": False, "error": str(e)}


# ============================================================================
# Voice Call Management
# ============================================================================

@app.post("/voice/call")
async def initiate_voice_call(user_id: str = Form(...), voice_channel_id: str = Form(...)):
    """
    Initiate a voice call to a user.
    
    Flow:
    1. Start STT and TTS containers
    2. Wait for models to load (health check)
    3. Join voice channel
    4. Send DM with invite to user
    5. Wait for user to join (30min timeout)
    6. Auto-disconnect 45s after user leaves
    """
    logger.info(f"📞 Voice call initiated for user {user_id} in channel {voice_channel_id}")
    
    # Check if bot is running
    if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
        return {"success": False, "error": "Bot is not running"}
    
    # Run the voice call setup in the bot's event loop
    try:
        future = asyncio.run_coroutine_threadsafe(
            _initiate_voice_call_impl(user_id, voice_channel_id),
            globals.client.loop
        )
        result = future.result(timeout=90)  # 90 second timeout for container warmup
        return result
    except Exception as e:
        logger.error(f"Error initiating voice call: {e}", exc_info=True)
        return {"success": False, "error": str(e)}


async def _initiate_voice_call_impl(user_id: str, voice_channel_id: str):
    """Implementation of voice call initiation that runs in the bot's event loop."""
    from utils.container_manager import ContainerManager
    from utils.voice_manager import VoiceSessionManager
    
    try:
        # Convert string IDs to integers for Discord API
        user_id_int = int(user_id)
        channel_id_int = int(voice_channel_id)
        
        # Get user and channel
        user = await globals.client.fetch_user(user_id_int)
        if not user:
            return {"success": False, "error": "User not found"}
        
        channel = globals.client.get_channel(channel_id_int)
        if not channel or not isinstance(channel, discord.VoiceChannel):
            return {"success": False, "error": "Voice channel not found"}
        
        # Get a text channel for voice operations (use first text channel in guild)
        text_channel = None
        for ch in channel.guild.text_channels:
            if ch.permissions_for(channel.guild.me).send_messages:
                text_channel = ch
                break
        
        if not text_channel:
            return {"success": False, "error": "No accessible text channel found"}
        
        # Start containers
        logger.info("Starting voice containers...")
        containers_started = await ContainerManager.start_voice_containers()
        
        if not containers_started:
            return {"success": False, "error": "Failed to start voice containers"}
        
        # Start voice session
        logger.info(f"Starting voice session in {channel.name}")
        session_manager = VoiceSessionManager()
        
        try:
            await session_manager.start_session(channel.guild.id, channel, text_channel)
        except Exception as e:
            await ContainerManager.stop_voice_containers()
            return {"success": False, "error": f"Failed to start voice session: {str(e)}"}
        
        # Set up voice call tracking (use integer ID)
        session_manager.active_session.call_user_id = user_id_int
        
        # Generate invite link
        invite = await channel.create_invite(
            max_age=1800,  # 30 minutes
            max_uses=1,
            reason="Miku voice call"
        )
        
        # Send DM to user
        try:
            # Get LLM to generate a personalized invitation message
            from utils.llm import query_llama
            
            invitation_prompt = f"""You're calling {user.name} in voice chat! Generate a cute, excited message inviting them to join you. 
Keep it brief (1-2 sentences). Make it feel personal and enthusiastic!"""
            
            invitation_text = await query_llama(
                user_prompt=invitation_prompt,
                user_id=user.id,
                guild_id=None,
                response_type="voice_call_invite",
                author_name=user.name
            )
            
            dm_message = f"📞 **Miku is calling you! Very experimental! Speak clearly, loudly and close to the mic! Expect weirdness!** 📞\n\n{invitation_text}\n\n🎤 Join here: {invite.url}"
            
            sent_message = await user.send(dm_message)
            
            # Log to DM logger (create a mock message object for logging)
            # The dm_logger.log_user_message expects a discord.Message object
            # So we need to use the actual sent_message
            dm_logger.log_user_message(user, sent_message, is_bot_message=True)
            
            logger.info(f"✓ DM sent to {user.name}")
            
        except Exception as e:
            logger.error(f"Failed to send DM: {e}")
            # Don't fail the whole call if DM fails
        
        # Set up 30min timeout task
        session_manager.active_session.call_timeout_task = asyncio.create_task(
            _voice_call_timeout_handler(session_manager.active_session, user, channel)
        )
        
        return {
            "success": True,
            "user_id": user_id,
            "channel_id": voice_channel_id,
            "invite_url": invite.url
        }
        
    except Exception as e:
        logger.error(f"Error in voice call implementation: {e}", exc_info=True)
        return {"success": False, "error": str(e)}


async def _voice_call_timeout_handler(voice_session: 'VoiceSession', user: discord.User, channel: discord.VoiceChannel):
    """Handle 30min timeout if user doesn't join."""
    try:
        await asyncio.sleep(1800)  # 30 minutes
        
        # Check if user ever joined
        if not voice_session.user_has_joined:
            logger.info(f"Voice call timeout - user {user.name} never joined")
            
            # End the session (which triggers cleanup)
            from utils.voice_manager import VoiceSessionManager
            session_manager = VoiceSessionManager()
            await session_manager.end_session()
            
            
            # Stop containers
            from utils.container_manager import ContainerManager
            await ContainerManager.stop_voice_containers()
            
            # Send timeout DM
            try:
                timeout_message = "Aww, I guess you couldn't make it to voice chat... Maybe next time! 💙"
                sent_message = await user.send(timeout_message)
                
                # Log to DM logger
                dm_logger.log_user_message(user, sent_message, is_bot_message=True)
            except:
                pass
                
    except asyncio.CancelledError:
        # User joined in time, normal operation
        pass


@app.get("/voice/debug-mode")
def get_voice_debug_mode():
    """Get current voice debug mode status"""
    return {
        "debug_mode": globals.VOICE_DEBUG_MODE
    }


@app.post("/voice/debug-mode")
def set_voice_debug_mode(enabled: bool = Form(...)):
    """Set voice debug mode (shows transcriptions and responses in text channel)"""
    globals.VOICE_DEBUG_MODE = enabled
    logger.info(f"Voice debug mode set to: {enabled}")
    
    # Persist so it survives restarts
    try:
        from config_manager import config_manager
        config_manager.set("voice.debug_mode", enabled, persist=True)
    except Exception:
        pass
    
    return {
        "status": "ok",
        "debug_mode": enabled,
        "message": f"Voice debug mode {'enabled' if enabled else 'disabled'}"
    }


# ========== Cheshire Cat Memory Management (Phase 3) ==========

class MemoryDeleteRequest(BaseModel):
    confirmation: str

class MemoryEditRequest(BaseModel):
    content: str
    metadata: Optional[dict] = None

class MemoryCreateRequest(BaseModel):
    content: str
    collection: str  # 'declarative' or 'episodic'
    user_id: Optional[str] = None
    source: Optional[str] = None
    metadata: Optional[dict] = None

@app.get("/memory/status")
async def get_cat_memory_status():
    """Get Cheshire Cat connection status and feature flag."""
    from utils.cat_client import cat_adapter
    is_healthy = await cat_adapter.health_check()
    return {
        "enabled": globals.USE_CHESHIRE_CAT,
        "healthy": is_healthy,
        "url": globals.CHESHIRE_CAT_URL,
        "circuit_breaker_active": cat_adapter._is_circuit_broken(),
        "consecutive_failures": cat_adapter._consecutive_failures
    }

@app.post("/memory/toggle")
async def toggle_cat_integration(enabled: bool = Form(...)):
    """Toggle Cheshire Cat integration on/off."""
    globals.USE_CHESHIRE_CAT = enabled
    logger.info(f"🐱 Cheshire Cat integration {'ENABLED' if enabled else 'DISABLED'}")
    
    # Persist so it survives restarts
    try:
        from config_manager import config_manager
        config_manager.set("memory.use_cheshire_cat", enabled, persist=True)
    except Exception:
        pass
    
    return {
        "success": True,
        "enabled": globals.USE_CHESHIRE_CAT,
        "message": f"Cheshire Cat {'enabled' if enabled else 'disabled'}"
    }

@app.get("/memory/stats")
async def get_memory_stats():
    """Get memory collection statistics from Cheshire Cat (point counts per collection)."""
    from utils.cat_client import cat_adapter
    stats = await cat_adapter.get_memory_stats()
    if stats is None:
        return {"success": False, "error": "Could not reach Cheshire Cat"}
    return {"success": True, "collections": stats.get("collections", [])}

@app.get("/memory/facts")
async def get_memory_facts():
    """Get all declarative memory facts (learned knowledge about users)."""
    from utils.cat_client import cat_adapter
    facts = await cat_adapter.get_all_facts()
    return {"success": True, "facts": facts, "count": len(facts)}

@app.get("/memory/episodic")
async def get_episodic_memories():
    """Get all episodic memories (conversation snippets)."""
    from utils.cat_client import cat_adapter
    result = await cat_adapter.get_memory_points(collection="episodic", limit=100)
    if result is None:
        return {"success": False, "error": "Could not reach Cheshire Cat"}

    memories = []
    for point in result.get("points", []):
        payload = point.get("payload", {})
        memories.append({
            "id": point.get("id"),
            "content": payload.get("page_content", ""),
            "metadata": payload.get("metadata", {}),
        })

    return {"success": True, "memories": memories, "count": len(memories)}

@app.post("/memory/consolidate")
async def trigger_memory_consolidation():
    """Manually trigger memory consolidation (sleep consolidation process)."""
    from utils.cat_client import cat_adapter
    logger.info("🌙 Manual memory consolidation triggered via API")
    result = await cat_adapter.trigger_consolidation()
    if result is None:
        return {"success": False, "error": "Consolidation failed or timed out"}
    return {"success": True, "result": result}

@app.post("/memory/delete")
async def delete_all_memories(request: MemoryDeleteRequest):
    """
    Delete ALL of Miku's memories. Requires exact confirmation string.
    
    The confirmation field must be exactly:
        "Yes, I am deleting Miku's memories fully."
    
    This is destructive and irreversible.
    """
    REQUIRED_CONFIRMATION = "Yes, I am deleting Miku's memories fully."

    if request.confirmation != REQUIRED_CONFIRMATION:
        logger.warning(f"Memory deletion rejected: wrong confirmation string")
        return {
            "success": False,
            "error": "Confirmation string does not match. "
                     f"Expected exactly: \"{REQUIRED_CONFIRMATION}\""
        }

    from utils.cat_client import cat_adapter
    logger.warning("⚠️ MEMORY DELETION CONFIRMED — wiping all memories!")

    # Wipe vector memories (episodic + declarative)
    wipe_success = await cat_adapter.wipe_all_memories()

    # Also clear conversation history
    history_success = await cat_adapter.wipe_conversation_history()

    if wipe_success:
        logger.warning("🗑️ All Miku memories have been deleted.")
        return {
            "success": True,
            "message": "All memories have been permanently deleted.",
            "vector_memory_wiped": wipe_success,
            "conversation_history_cleared": history_success
        }
    else:
        return {
            "success": False,
            "error": "Failed to wipe memory collections. Check Cat connection."
        }

@app.delete("/memory/point/{collection}/{point_id}")
async def delete_single_memory_point(collection: str, point_id: str):
    """Delete a single memory point by collection and ID."""
    from utils.cat_client import cat_adapter
    success = await cat_adapter.delete_memory_point(collection, point_id)
    if success:
        return {"success": True, "deleted": point_id}
    else:
        return {"success": False, "error": f"Failed to delete point {point_id}"}

@app.put("/memory/point/{collection}/{point_id}")
async def edit_memory_point(collection: str, point_id: str, request: MemoryEditRequest):
    """Edit an existing memory point's content and/or metadata."""
    from utils.cat_client import cat_adapter
    success = await cat_adapter.update_memory_point(
        collection=collection,
        point_id=point_id,
        content=request.content,
        metadata=request.metadata
    )
    if success:
        return {"success": True, "updated": point_id}
    else:
        return {"success": False, "error": f"Failed to update point {point_id}"}

@app.post("/memory/create")
async def create_memory_point(request: MemoryCreateRequest):
    """
    Manually create a new memory (declarative fact or episodic memory).
    
    For declarative facts, this allows you to teach Miku new knowledge.
    For episodic memories, this allows you to inject conversation context.
    """
    from utils.cat_client import cat_adapter
    
    if request.collection not in ['declarative', 'episodic']:
        return {"success": False, "error": "Collection must be 'declarative' or 'episodic'"}
    
    # Create the memory point
    result = await cat_adapter.create_memory_point(
        collection=request.collection,
        content=request.content,
        user_id=request.user_id or "manual_admin",
        source=request.source or "manual_web_ui",
        metadata=request.metadata or {}
    )
    
    if result:
        return {"success": True, "point_id": result, "collection": request.collection}
    else:
        return {"success": False, "error": "Failed to create memory point"}


def start_api():
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=3939)


