use crate::{Context, Error};
use ab_glyph::{FontRef, PxScale};
use image::{ImageBuffer, Rgba, DynamicImage};
use imageproc::drawing::{draw_filled_rect_mut, draw_filled_circle_mut, draw_text_mut};
use imageproc::rect::Rect;
use poise::serenity_prelude as serenity;
use serde::Deserialize;
use serenity::Mentionable;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use std::time::Duration;

use serenity::prelude::TypeMapKey;
use surrealdb::Surreal;
use surrealdb::engine::remote::ws::Client;
use futures::future::join_all;
use tokio::sync::RwLock;

/// Global avatar cache - stores decoded images by URL
/// TTL is handled by periodic cleanup or could use moka crate
static AVATAR_CACHE: OnceLock<Arc<RwLock<HashMap<String, Arc<DynamicImage>>>>> = OnceLock::new();

fn get_avatar_cache() -> &'static Arc<RwLock<HashMap<String, Arc<DynamicImage>>>> {
    AVATAR_CACHE.get_or_init(|| Arc::new(RwLock::new(HashMap::new())))
}

/// Global HTTP client for avatar fetches - connection pooling
static AVATAR_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

fn get_avatar_client() -> &'static reqwest::Client {
    AVATAR_CLIENT.get_or_init(|| {
        reqwest::Client::builder()
            .timeout(Duration::from_millis(800))
            .pool_max_idle_per_host(10)
            .build()
            .unwrap_or_default()
    })
}

pub struct DbKey;

impl TypeMapKey for DbKey {
    type Value = Surreal<Client>;
}

#[derive(Deserialize, serde::Serialize, Debug)]
pub struct UserLevel {
    pub xp: u64,
    pub level: u64,
    pub track: String,
    pub last_message: chrono::DateTime<chrono::Utc>,
}

/// Role with level requirement - used for level track system
#[derive(serde::Serialize, Clone, Debug)]
pub struct TrackLevelRole {
    pub role_id: u64,
    pub level: u64,
}

impl<'de> Deserialize<'de> for TrackLevelRole {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct TrackLevelRoleVisitor;

        impl<'de> serde::de::Visitor<'de> for TrackLevelRoleVisitor {
            type Value = TrackLevelRole;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a u64 (legacy) or a TrackLevelRole struct")
            }

            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(TrackLevelRole {
                    role_id: value,
                    level: 0, // Default level for legacy data
                })
            }

            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(TrackLevelRole {
                    role_id: value as u64,
                    level: 0,
                })
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                let mut role_id = None;
                let mut level = None;

                while let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "role_id" => {
                            if role_id.is_some() {
                                return Err(serde::de::Error::duplicate_field("role_id"));
                            }
                            role_id = Some(map.next_value()?);
                        }
                        "level" => {
                            if level.is_some() {
                                return Err(serde::de::Error::duplicate_field("level"));
                            }
                            level = Some(map.next_value()?);
                        }
                         _ => {
                            let _ = map.next_value::<serde::de::IgnoredAny>()?;
                        }
                    }
                }

                let role_id = role_id.ok_or_else(|| serde::de::Error::missing_field("role_id"))?;
                let level = level.ok_or_else(|| serde::de::Error::missing_field("level"))?;

                Ok(TrackLevelRole { role_id, level })
            }
        }

        deserializer.deserialize_any(TrackLevelRoleVisitor)
    }
}

#[derive(Deserialize)]
struct GuildRecord {
    level_role_stack: Option<HashMap<String, Vec<TrackLevelRole>>>,
    levelup_channel: Option<u64>,
    levelup_message: Option<String>,
    level_up_role_mapper: Option<HashMap<String, u64>>,
}

#[derive(Deserialize, Debug)]
struct LeaderboardEntry {
    id: surrealdb::sql::Thing,
    xp: u64,
    level: u64,
}


#[poise::command(
    slash_command,
    prefix_command,
    guild_only,
    default_member_permissions = "MANAGE_GUILD",
    required_permissions = "MANAGE_GUILD"
)]
pub async fn set_levelup_message_channel(
    ctx: Context<'_>,
    #[description = "Channel to send level up messages to (default: current channel)"]
    channel: Option<serenity::Channel>,
) -> Result<(), Error> {
    let guild_id = ctx
        .guild_id()
        .ok_or_else(|| Error::msg("Guild only command"))?;
    let db = &ctx.data().db;

    let channel_id = match channel {
        Some(c) => c.id().get(),
        None => ctx.channel_id().get(),
    };

    let _: Option<serde::de::IgnoredAny> = db
        .update(("guilds", guild_id.to_string()))
        .merge(serde_json::json!({
            "levelup_channel": channel_id
        }))
        .await?;

    ctx.say(format!(
        "Level up messages will now be sent to <#{}>.",
        channel_id
    ))
    .await?;

    Ok(())
}

#[poise::command(
    slash_command,
    prefix_command,
    guild_only,
    default_member_permissions = "MANAGE_GUILD",
    required_permissions = "MANAGE_GUILD"
)]
pub async fn set_levelup_message(
    ctx: Context<'_>,
    #[description = "Template message (use {user.mention} and {user.level})"] message: String,
) -> Result<(), Error> {
    let guild_id = ctx
        .guild_id()
        .ok_or_else(|| Error::msg("Guild only command"))?;
    let db = &ctx.data().db;

    let _: Option<serde::de::IgnoredAny> = db
        .update(("guilds", guild_id.to_string()))
        .merge(serde_json::json!({
            "levelup_message": message
        }))
        .await?;

    ctx.say(format!("Level up message template updated to: {}", message))
        .await?;

    Ok(())
}



pub async fn process_message(
    ctx: &serenity::Context,
    msg: &serenity::Message,
) -> Result<(), Error> {
    if msg.author.bot {
        return Ok(());
    }

    let guild_id = match msg.guild_id {
        Some(id) => id,
        None => return Ok(()),
    };

    let data = ctx.data.read().await;
    let db = match data.get::<DbKey>() {
        Some(db) => db,
        None => {
            tracing::error!("Database connection not found in context data");
            return Ok(());
        }
    };

    let user_id = msg.author.id;
    let record_id = ("levels", format!("{}:{}", guild_id, user_id));

    let user_level: Option<UserLevel> = db.select(record_id.clone()).await?;

    if let Some(mut level_data) = user_level {
        tracing::info!(
            "Processing message for user {} in guild {}",
            user_id,
            guild_id
        );
        // User exists, add XP
        // Check cooldown
        let now = chrono::Utc::now();
        if now
            .signed_duration_since(level_data.last_message)
            .num_seconds()
            < 60
        {
            return Ok(());
        }

        use rand::Rng;
        let xp_gain = rand::rng().random_range(15..=25);
        level_data.xp += xp_gain;
        level_data.last_message = now;
        tracing::info!(
            "Added {} XP to user {}. Total XP: {}",
            xp_gain,
            user_id,
            level_data.xp
        );

        let next_level_xp = (level_data.level + 1) * 100;
        if level_data.xp >= next_level_xp {
            level_data.level += 1;
            level_data.xp = 0;
            tracing::info!(
                "User {} leveled up to {} in guild {}",
                user_id,
                level_data.level,
                guild_id
            );

            // Handle role assignment
            let guild_record: Option<GuildRecord> =
                db.select(("guilds", guild_id.to_string())).await?;

            let mut role_assigned_id: Option<serenity::RoleId> = None;

            if let Some(record) = &guild_record {
                if let Some(stack) = &record.level_role_stack {
                    tracing::info!("Checking roles for track: {}", level_data.track);
                    if let Some(track_roles) = stack.get(&level_data.track) {
                        tracing::info!("Found {} roles in track {}", track_roles.len(), level_data.track);
                        // Find the role that matches the current level
                        for track_role in track_roles {
                            tracing::info!("Checking role {} for level {}", track_role.role_id, track_role.level);
                            if level_data.level == track_role.level {
                                let role_id = serenity::RoleId::new(track_role.role_id);
                                tracing::info!(
                                    "Assigning role {} at level {} to user {}",
                                    role_id,
                                    track_role.level,
                                    user_id
                                );
                                if let Err(e) = ctx
                                    .http
                                    .add_member_role(
                                        guild_id,
                                        user_id,
                                        role_id,
                                        Some("Level up"),
                                    )
                                    .await
                                {
                                    tracing::error!("Failed to add role: {}", e);
                                } else {
                                    role_assigned_id = Some(role_id);
                                }
                                break; // Only assign one role per level up
                            }
                        }
                    } else {
                        tracing::warn!("Track {} not found in guild {}", level_data.track, guild_id);
                    }
                } else {
                    tracing::warn!("No level role stack configured for guild {}", guild_id);
                }
            }

            // Determine target channel
            let target_channel_id = if let Some(record) = &guild_record {
                if let Some(channel_id) = record.levelup_channel {
                    serenity::ChannelId::new(channel_id)
                } else {
                    msg.channel_id
                }
            } else {
                msg.channel_id
            };

            // Determine message content
            let message_content = if let Some(record) = &guild_record {
                if let Some(template) = &record.levelup_message {
                    let mut content = template
                        .replace("{user.mention}", &msg.author.mention().to_string())
                        .replace("{user.level}", &level_data.level.to_string());

                    if let Some(role_id) = role_assigned_id {
                        content.push_str(&format!("\nYou also received the <@&{}> role!", role_id));
                    }
                    content
                } else {
                    // Default message
                    if let Some(role_id) = role_assigned_id {
                        format!(
                            "Congratulations {}, you leveled up to level {} and received the <@&{}> role!",
                            msg.author.mention(),
                            level_data.level,
                            role_id
                        )
                    } else {
                        format!(
                            "Congratulations {}, you leveled up to level {}!",
                            msg.author.mention(),
                            level_data.level
                        )
                    }
                }
            } else {
                // Default message if no record
                if let Some(role_id) = role_assigned_id {
                    format!(
                        "Congratulations {}, you leveled up to level {} and received the <@&{}> role!",
                        msg.author.mention(),
                        level_data.level,
                        role_id
                    )
                } else {
                    format!(
                        "Congratulations {}, you leveled up to level {}!",
                        msg.author.mention(),
                        level_data.level
                    )
                }
            };

            let message = serenity::CreateMessage::new()
                .content(message_content)
                .allowed_mentions(serenity::CreateAllowedMentions::new().users(vec![user_id]));

            if let Err(e) = target_channel_id.send_message(&ctx.http, message).await {
                tracing::error!("Failed to send level up message: {}", e);
            }
        }

        let _: Option<UserLevel> = db.update(record_id).content(level_data).await?;
    } else {
        // User doesn't exist, check if they have a starting role
        let guild_record: Option<GuildRecord> = db.select(("guilds", guild_id.to_string())).await?;

        if let Some(record) = guild_record {
            if let Some(stack) = record.level_role_stack {
                let member = match guild_id.member(&ctx.http, user_id).await {
                    Ok(m) => m,
                    Err(_) => return Ok(()),
                };

                for (track_name, track_roles) in stack {
                    // Find the starting role (the one with the lowest level, typically 0)
                    if let Some(starting_role) = track_roles.iter().min_by_key(|r| r.level) {
                        if member
                            .roles
                            .contains(&serenity::RoleId::new(starting_role.role_id))
                        {
                            // User has the starting role for this track
                            let new_level = UserLevel {
                                xp: 0,
                                level: 0,
                                track: track_name.clone(),
                                last_message: chrono::Utc::now(),
                            };
                            tracing::info!(
                                "Initializing user {} on track {} in guild {}",
                                user_id,
                                track_name,
                                guild_id
                            );
                            let _: Option<UserLevel> =
                                db.create(record_id).content(new_level).await?;
                            break; // Only assign to one track
                        }
                    }
                }
            }
        }
    }

    Ok(())
}

struct LeaderboardRenderEntry {
    username: String,
    rank: usize,
    level: u64,
    xp: u64,
    next_level_xp: u64,
    avatar: Option<Arc<DynamicImage>>,
    /// Used to generate a colored placeholder if avatar is missing
    user_id: u64,
}

#[poise::command(slash_command, prefix_command, guild_only)]
pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
    let guild_id = ctx
        .guild_id()
        .ok_or_else(|| Error::msg("Guild only command"))?;
    let db = &ctx.data().db;

    // Defer interaction as image generation might take time
    ctx.defer().await?;

    // Query top 10 users for this guild
    let sql = "SELECT * FROM levels WHERE string::starts_with(record::id(id), $prefix) ORDER BY level DESC, xp DESC LIMIT 10";
    let prefix = format!("{}:", guild_id);

    let mut response = db.query(sql).bind(("prefix", prefix)).await?;
    let entries: Vec<LeaderboardEntry> = response.take(0)?;

    if entries.is_empty() {
        ctx.say("No leaderboard data found for this guild.").await?;
        return Ok(());
    }

    // 1. Fetch all user data and avatars in parallel
    let avatar_client = get_avatar_client();
    let avatar_cache = get_avatar_cache();

    let mut tasks = Vec::new();

    // Try to get guild from cache first (much faster than API calls)
    let cached_guild = guild_id.to_guild_cached(&ctx.serenity_context().cache).map(|g| g.clone());

    for (i, entry) in entries.iter().enumerate() {
        let http = ctx.serenity_context().http.clone();
        let entry_level = entry.level;
        let entry_xp = entry.xp;
        let guild_id = guild_id;
        let avatar_client = avatar_client.clone();
        let avatar_cache = avatar_cache.clone();
        let cached_guild = cached_guild.clone();
        
        // Extract user id robustly from Surreal `Thing`
        let id_value = &entry.id.id;
        let clean_id_str = match id_value {
            surrealdb::sql::Id::String(s) => s.as_str().to_string(),
            _ => entry.id.id.to_string(),
        };

        tasks.push(async move {
            let parts: Vec<&str> = clean_id_str.split(':').collect();
            let user_id_str = parts.last().unwrap_or(&"0");
            let user_id_u64 = user_id_str.parse::<u64>().unwrap_or(0);

            let mut user_name = String::from("Unknown User");
            let mut avatar_url = String::new();

            if user_id_u64 != 0 {
                let user_id = serenity::UserId::new(user_id_u64);
                
                // Try cache first (instant), then fall back to API
                let member_from_cache = cached_guild.as_ref()
                    .and_then(|g| g.members.get(&user_id).cloned());
                
                if let Some(member) = member_from_cache {
                    user_name = member.display_name().to_string();
                    avatar_url = get_small_avatar_url(&member.user);
                } else if let Ok(member) = http.get_member(guild_id, user_id).await {
                    user_name = member.display_name().to_string();
                    avatar_url = get_small_avatar_url(&member.user);
                } else if let Ok(user) = http.get_user(user_id).await {
                    user_name = user.global_name.as_ref().unwrap_or(&user.name).to_string();
                    avatar_url = get_small_avatar_url(&user);
                }
            }

            // Check avatar cache first
            let avatar_img: Option<Arc<DynamicImage>> = if !avatar_url.is_empty() {
                // Fast path: check cache
                {
                    let cache = avatar_cache.read().await;
                    if let Some(cached) = cache.get(&avatar_url) {
                        Some(cached.clone())
                    } else {
                        None
                    }
                }.or_else(|| None).map(Some).unwrap_or_else(|| {
                    // Cache miss - need to fetch (will be done below)
                    None
                })
            } else {
                None
            };

            // If not in cache, fetch with short timeout
            let avatar_img = if avatar_img.is_some() {
                avatar_img
            } else if !avatar_url.is_empty() {
                // Use tokio timeout for precise control
                let fetch_result = tokio::time::timeout(
                    Duration::from_millis(500),
                    async {
                        match avatar_client.get(&avatar_url).send().await {
                            Ok(response) => response.bytes().await.ok(),
                            Err(_) => None,
                        }
                    }
                ).await;

                match fetch_result {
                    Ok(Some(bytes)) => {
                        if let Ok(img) = image::load_from_memory(&bytes) {
                            let arc_img = Arc::new(img);
                            // Store in cache for next time
                            {
                                let mut cache = avatar_cache.write().await;
                                // Limit cache size to prevent memory issues
                                if cache.len() < 500 {
                                    cache.insert(avatar_url, arc_img.clone());
                                }
                            }
                            Some(arc_img)
                        } else {
                            None
                        }
                    }
                    _ => None,
                }
            } else {
                None
            };

            LeaderboardRenderEntry {
                username: user_name,
                rank: i + 1,
                level: entry_level,
                xp: entry_xp,
                next_level_xp: (entry_level + 1) * 100,
                avatar: avatar_img,
                user_id: user_id_u64,
            }
        });
    }

    // Wait for all fetches to complete
    let render_entries = join_all(tasks).await;

    // 2. Generate image on a blocking thread
    let image_data = tokio::task::spawn_blocking(move || {
        generate_leaderboard_image(render_entries)
    }).await??;

    ctx.send(
        poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes(
            image_data,
            "leaderboard.jpg",
        )),
    )
    .await?;

    Ok(())
}

/// Get a small avatar URL (48px) - webp is faster to download & decode
fn get_small_avatar_url(user: &serenity::User) -> String {
    // Use webp format (smaller file size, faster download) and small size
    // The image crate has webp support enabled
    match &user.avatar {
        Some(hash) => {
            let ext = if hash.is_animated() { "gif" } else { "webp" };
            format!(
                "https://cdn.discordapp.com/avatars/{}/{}.{}?size=48",
                user.id, hash, ext
            )
        }
        None => {
            // Default avatar - use small size
            let index = if let Some(discrim) = user.discriminator {
                discrim.get() % 5
            } else {
                ((user.id.get() >> 22) % 6) as u16
            };
            format!(
                "https://cdn.discordapp.com/embed/avatars/{}.png?size=48",
                index
            )
        }
    }
}

/// Pre-defined colors for avatar placeholders based on user ID
const PLACEHOLDER_COLORS: [Rgba<u8>; 8] = [
    Rgba([114, 137, 218, 255]), // Blurple
    Rgba([67, 181, 129, 255]),  // Green
    Rgba([250, 166, 26, 255]),  // Yellow
    Rgba([240, 71, 71, 255]),   // Red
    Rgba([255, 115, 250, 255]), // Pink
    Rgba([26, 188, 156, 255]),  // Teal
    Rgba([230, 126, 34, 255]),  // Orange
    Rgba([155, 89, 182, 255]),  // Purple
];

fn generate_leaderboard_image(
    entries: Vec<LeaderboardRenderEntry>,
) -> Result<Vec<u8>, Error> {
    // Image dimensions
    let width = 800u32;
    let height = 100 + (entries.len() as u32 * 80); // Header + rows
    let mut image = ImageBuffer::from_pixel(width, height, Rgba([40, 44, 52, 255])); // Dark background

    // Load font once (compiled into binary)
    let font_bytes = include_bytes!("../assets/Roboto-Regular.ttf");
    let font = FontRef::try_from_slice(font_bytes)?;

    let white = Rgba([255, 255, 255, 255]);
    let gray = Rgba([180, 180, 180, 255]);
    let bar_bg = Rgba([60, 63, 69, 255]);
    let bar_fill = Rgba([114, 137, 218, 255]); // Blurple-ish

    // Draw Title
    let scale_title = PxScale::from(40.0);
    draw_text_mut(&mut image, white, 20, 20, scale_title, &font, "Leaderboard");

    let scale_text = PxScale::from(24.0);
    let scale_small = PxScale::from(18.0);

    for (i, entry) in entries.iter().enumerate() {
        let y_offset = 100 + (i as u32 * 80);

        // Draw Rank
        draw_text_mut(
            &mut image,
            white,
            20,
            y_offset as i32 + 20,
            scale_text,
            &font,
            &format!("#{}", entry.rank),
        );

        // Draw Avatar or colored circle placeholder
        let avatar_x = 80i64;
        let avatar_y = y_offset as i64 + 16;
        
        if let Some(avatar_img) = &entry.avatar {
            // Use Nearest filter - fastest possible, good enough for small avatars
            let avatar_resized = avatar_img.resize_exact(48, 48, image::imageops::FilterType::Nearest);
            image::imageops::overlay(&mut image, &avatar_resized, avatar_x, avatar_y);
        } else {
            // Draw colored circle placeholder based on user ID
            let color_idx = (entry.user_id % 8) as usize;
            let color = PLACEHOLDER_COLORS[color_idx];
            let center_x = (avatar_x + 24) as i32;
            let center_y = (avatar_y + 24) as i32;
            draw_filled_circle_mut(&mut image, (center_x, center_y), 24, color);
            
            // Draw first letter of username
            let first_char = entry.username.chars().next().unwrap_or('?').to_uppercase().to_string();
            draw_text_mut(
                &mut image,
                white,
                center_x - 8,
                center_y - 12,
                scale_text,
                &font,
                &first_char,
            );
        }

        // Draw Username
        draw_text_mut(
            &mut image,
            white,
            160,
            y_offset as i32 + 15,
            scale_text,
            &font,
            &entry.username,
        );

        // Draw Level
        draw_text_mut(
            &mut image,
            gray,
            160,
            y_offset as i32 + 45,
            scale_small,
            &font,
            &format!("Level {}", entry.level),
        );

        // Draw XP Bar
        let bar_width = 300u32;
        let bar_height = 20u32;
        let bar_x = 450i32;
        let bar_y = y_offset as i32 + 30;

        draw_filled_rect_mut(
            &mut image,
            Rect::at(bar_x, bar_y).of_size(bar_width, bar_height),
            bar_bg,
        );

        let progress = if entry.next_level_xp > 0 {
            entry.xp as f32 / entry.next_level_xp as f32
        } else {
            0.0
        };
        let fill_width = (progress * bar_width as f32) as u32;

        if fill_width > 0 {
            draw_filled_rect_mut(
                &mut image,
                Rect::at(bar_x, bar_y).of_size(fill_width, bar_height),
                bar_fill,
            );
        }

        // Draw XP Text
        let xp_text = format!("{}/{} XP", entry.xp, entry.next_level_xp);
        draw_text_mut(
            &mut image,
            white,
            bar_x + 5,
            bar_y + 2,
            PxScale::from(14.0),
            &font,
            &xp_text,
        );
    }

    // Use JPEG for much faster encoding (PNG is slow)
    // Quality 85 is a good balance of size vs quality
    let mut bytes: Vec<u8> = Vec::with_capacity(width as usize * height as usize);
    let rgb_image = DynamicImage::ImageRgba8(image).into_rgb8();
    let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, 85);
    encoder.encode_image(&rgb_image)?;
    Ok(bytes)
}

pub async fn on_guild_member_update(
    ctx: &serenity::Context,
    _old: &Option<serenity::Member>,
    new: &serenity::Member,
) -> Result<(), Error> {
    let guild_id = new.guild_id;
    let data = ctx.data.read().await;
    let db = match data.get::<DbKey>() {
        Some(db) => db,
        None => {
            tracing::error!("Database connection not found in context data");
            return Ok(());
        }
    };

    let guild_record: Option<GuildRecord> = db.select(("guilds", guild_id.to_string())).await?;

    if let Some(record) = guild_record {
        if let Some(mapper) = record.level_up_role_mapper {
            // Check if user has any "in_role"
            // We iterate through the mapper to find if any key (in_role) matches user's roles
            for (in_role_str, out_role_id) in &mapper {
                if let Ok(in_role_id) = in_role_str.parse::<u64>() {
                    let in_role = serenity::RoleId::new(in_role_id);

                    if new.roles.contains(&in_role) {
                        // User has the in_role.
                        // Now check if they have ANY other out_role from the mapper
                        let mut has_any_out_role = false;
                        for (_, other_out_role_id) in &mapper {
                            if new
                                .roles
                                .contains(&serenity::RoleId::new(*other_out_role_id))
                            {
                                has_any_out_role = true;
                                break;
                            }
                        }

                        if !has_any_out_role {
                            // User has no out_roles, so we bridge them
                            tracing::info!(
                                "Bridging user {} from role {} to {}",
                                new.user.id,
                                in_role_id,
                                out_role_id
                            );

                            // Add out_role
                            if let Err(e) = ctx
                                .http
                                .add_member_role(
                                    guild_id,
                                    new.user.id,
                                    serenity::RoleId::new(*out_role_id),
                                    Some("Role Bridge"),
                                )
                                .await
                            {
                                tracing::error!("Failed to add bridged role: {}", e);
                            }

                            // Remove in_role
                            if let Err(e) = ctx
                                .http
                                .remove_member_role(
                                    guild_id,
                                    new.user.id,
                                    in_role,
                                    Some("Role Bridge"),
                                )
                                .await
                            {
                                tracing::error!("Failed to remove bridged role: {}", e);
                            }
                        } else {
                            // User already has an out_role, just remove the in_role
                            tracing::info!(
                                "User {} already has an out_role, removing in_role {}",
                                new.user.id,
                                in_role_id
                            );
                            if let Err(e) = ctx
                                .http
                                .remove_member_role(
                                    guild_id,
                                    new.user.id,
                                    in_role,
                                    Some("Role Bridge cleanup"),
                                )
                                .await
                            {
                                tracing::error!("Failed to remove bridged role: {}", e);
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(())
}
