use poise::{
    serenity_prelude::{
        http::CacheHttp,
        model::id::{GuildId, UserId},
        CreateEmbedFooter, CreateInteractionResponseMessage,
    },
    CreateReply,
};

use crate::{
    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
    ApplicationContext, Context, Database, Error,
};

/// Check if this user/guild combination should be considered subscribed.
/// If the guild has a patreon linked, check the user involved in the link.
/// Otherwise, check the user and the guild's owner
pub async fn check_subscription(
    ctx: impl CacheHttp,
    database: impl Executor<'_, Database = Database>,
    user_id: UserId,
    guild_id: Option<GuildId>,
) -> bool {
    if let Some(subscription_guild) = *CNC_GUILD {
        let user_subscribed = check_user_subscription(&ctx, user_id).await;

        let owner_subscribed = match guild_id {
            Some(guild_id) => {
                if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
                    check_user_subscription(&ctx, owner).await
                } else {
                    false
                }
            }

            None => false,
        };

        let link_subscribed = match guild_id {
            Some(guild_id) => {
                if let Ok(row) = sqlx::query!(
                    "SELECT user_id FROM patreon_link WHERE guild_id = ?",
                    guild_id.get()
                )
                .fetch_one(database)
                .await
                {
                    check_user_subscription(&ctx, row.user_id).await
                } else {
                    false
                }
            }

            None => false,
        };

        user_subscribed || owner_subscribed || link_subscribed
    } else {
        true
    }
}

/// Check a user's subscription status, ignoring Patreon linkage
pub async fn check_user_subscription(
    cache_http: impl CacheHttp,
    user_id: impl Into<UserId>,
) -> bool {
    if let Some(subscription_guild) = *CNC_GUILD {
        let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;

        if let Ok(member) = guild_member {
            for role in member.roles {
                if SUBSCRIPTION_ROLES.contains(&role.get()) {
                    return true;
                }
            }
        }

        false
    } else {
        true
    }
}

pub fn reply_to_interaction_response_message(
    reply: CreateReply,
) -> CreateInteractionResponseMessage {
    let mut builder = CreateInteractionResponseMessage::new().embeds(reply.embeds);

    if let Some(components) = reply.components {
        builder = builder.components(components)
    }

    builder
}

pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter {
    let shard_count = ctx.serenity_context().cache.shard_count();
    let shard = ctx.serenity_context().shard_id;

    CreateEmbedFooter::new(format!(
        "{}\nshard {} of {}",
        concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")),
        shard,
        shard_count,
    ))
}

pub trait Recordable {
    async fn run(self, ctx: Context<'_>) -> Result<(), Error>;
}

pub use recordable_derive::Recordable;

pub trait Extract {
    fn extract(ctx: ApplicationContext) -> Self;
}

pub use extract_derive::Extract;
use sqlx::Executor;

macro_rules! extract_arg {
    ($ctx:ident, $name:ident, String) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map_or_else(
                || String::new(),
                |v| match v {
                    poise::serenity_prelude::ResolvedValue::String(s) => s.to_string(),
                    _ => String::new(),
                },
            )
    };
    ($ctx:ident, $name:ident, Option<String>) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map(|v| match v {
                poise::serenity_prelude::ResolvedValue::String(s) => Some(s.to_string()),
                _ => None,
            })
            .flatten()
    };
    ($ctx:ident, $name:ident, bool) => {
        $ctx.args.iter().find(|opt| opt.name == stringify!($name)).map(|opt| &opt.value).map_or(
            false,
            |v| match v {
                poise::serenity_prelude::ResolvedValue::Boolean(b) => b.to_owned(),
                _ => false,
            },
        )
    };
    ($ctx:ident, $name:ident, Option<bool>) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map(|v| match v {
                poise::serenity_prelude::ResolvedValue::Boolean(b) => Some(b.to_owned()),
                _ => None,
            })
            .flatten()
    };
    ($ctx:ident, $name:ident, Option<PartialChannel>) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map(|v| match v {
                poise::serenity_prelude::ResolvedValue::Channel(partial) => {
                    Some(partial.to_owned())
                }
                _ => None,
            })
            .flatten()
            .cloned()
    };
    ($ctx:ident, $name:ident, i64) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map(|v| match v {
                poise::serenity_prelude::ResolvedValue::Integer(int) => *int,
                _ => 0,
            })
            .flatten()
    };
    ($ctx:ident, $name:ident, Option<i64>) => {
        $ctx.args
            .iter()
            .find(|opt| opt.name == stringify!($name))
            .map(|opt| &opt.value)
            .map(|v| match v {
                poise::serenity_prelude::ResolvedValue::Integer(int) => Some(*int),
                _ => None,
            })
            .flatten()
    };
}

pub(crate) use extract_arg;
