pub mod builder;
pub mod content;
pub mod errors;
mod helper;

use crate::{
    commands::look::{LookFlags, TimeDisplayType},
    component_models::{ComponentDataModel, UndoReminder},
    consts::{REGEX_CHANNEL_USER, THEME_COLOR},
    interval_parser::parse_duration,
    models::{
        reminder::{
            builder::{ChannelWithThread, MultiReminderBuilder, ReminderScope},
            content::Content,
            errors::ReminderError,
        },
        CtxData,
    },
    time_parser::{cron_next_timestamp, natural_parser},
    utils::check_subscription,
    Context, Database, Error,
};
use chrono::{DateTime, NaiveDateTime, Utc};
use chrono_tz::Tz;
use log::warn;
use poise::{
    serenity_prelude::{
        model::id::{ChannelId, GuildId, UserId},
        ButtonStyle, Cache, ChannelType, CreateActionRow, CreateButton, CreateEmbed, ReactionType,
    },
    CreateReply,
};
use serenity::all::Channel;
use sqlx::{Executor, FromRow};
use std::{
    collections::HashSet,
    hash::{Hash, Hasher},
};

#[derive(Debug, Clone, FromRow)]
#[allow(dead_code)]
pub struct Reminder {
    pub id: u32,
    pub uid: String,
    pub channel: u64,
    pub utc_time: DateTime<Utc>,
    pub interval_seconds: Option<u32>,
    pub interval_days: Option<u32>,
    pub interval_months: Option<u32>,
    pub expires: Option<NaiveDateTime>,
    pub enabled: bool,
    pub content: String,
    pub embed_description: String,
    pub set_by: Option<u64>,
}

impl Hash for Reminder {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.uid.hash(state);
    }
}

impl PartialEq<Self> for Reminder {
    fn eq(&self, other: &Self) -> bool {
        self.uid == other.uid
    }
}

impl Eq for Reminder {}

impl Reminder {
    pub async fn from_uid(pool: impl Executor<'_, Database = Database>, uid: &str) -> Option<Self> {
        sqlx::query_as_unchecked!(
            Self,
            "
            SELECT
                reminders.id,
                reminders.uid,
                channels.channel,
                reminders.utc_time,
                reminders.interval_seconds,
                reminders.interval_days,
                reminders.interval_months,
                reminders.expires,
                reminders.enabled,
                reminders.content,
                reminders.embed_description,
                reminders.set_by
            FROM
                reminders
            INNER JOIN
                channels
            ON
                reminders.channel_id = channels.id
            WHERE
                reminders.uid = ?
            ",
            uid
        )
        .fetch_one(pool)
        .await
        .ok()
    }

    pub async fn from_id(pool: impl Executor<'_, Database = Database>, id: u32) -> Option<Self> {
        sqlx::query_as_unchecked!(
            Self,
            "
            SELECT
                reminders.id,
                reminders.uid,
                channels.channel,
                reminders.utc_time,
                reminders.interval_seconds,
                reminders.interval_days,
                reminders.interval_months,
                reminders.expires,
                reminders.enabled,
                reminders.content,
                reminders.embed_description,
                reminders.set_by
            FROM
                reminders
            INNER JOIN
                channels
            ON
                reminders.channel_id = channels.id
            WHERE
                reminders.id = ?
            ",
            id
        )
        .fetch_one(pool)
        .await
        .ok()
    }

    pub async fn from_channel<C: Into<ChannelId>>(
        pool: impl Executor<'_, Database = Database>,
        channel_id: C,
        flags: &LookFlags,
    ) -> Vec<Self> {
        let enabled = if flags.show_disabled { "0" } else { "1" };
        let channel_id = channel_id.into();

        sqlx::query_as_unchecked!(
            Self,
            "
            SELECT
                reminders.id,
                reminders.uid,
                channels.channel,
                reminders.utc_time,
                reminders.interval_seconds,
                reminders.interval_days,
                reminders.interval_months,
                reminders.expires,
                reminders.enabled,
                reminders.content,
                reminders.embed_description,
                reminders.set_by
            FROM
                reminders
            INNER JOIN
                channels
            ON
                reminders.channel_id = channels.id
            WHERE
                `status` = 'pending' AND
                channels.channel = ? AND
                reminders.enabled >= ?
            ORDER BY
                reminders.utc_time
            ",
            channel_id.get(),
            enabled,
        )
        .fetch_all(pool)
        .await
        .unwrap()
    }

    pub async fn from_guild(
        cache: impl AsRef<Cache>,
        pool: impl Executor<'_, Database = Database>,
        guild_id: Option<GuildId>,
        user: UserId,
    ) -> Vec<Self> {
        if let Some(guild_id) = guild_id {
            let channel_query = if let Some(guild) = guild_id.to_guild_cached(&cache) {
                Some(
                    guild
                        .channels
                        .keys()
                        .into_iter()
                        .map(|k| k.get().to_string())
                        .collect::<Vec<String>>(),
                )
            } else {
                None
            };

            match channel_query {
                Some(channels) => {
                    let placeholder = vec!["?"; channels.len()].join(",");
                    let sql = format!(
                        "
                        SELECT
                            reminders.id,
                            reminders.uid,
                            channels.channel,
                            reminders.utc_time,
                            reminders.interval_seconds,
                            reminders.interval_days,
                            reminders.interval_months,
                            reminders.expires,
                            reminders.enabled,
                            reminders.content,
                            reminders.embed_description,
                            reminders.set_by
                        FROM
                            reminders
                        LEFT JOIN
                            channels
                        ON
                            channels.id = reminders.channel_id
                        WHERE
                            `status` = 'pending' AND
                            channels.channel IN ({placeholder})
                        "
                    );

                    let mut query = sqlx::query_as::<Database, Self>(&sql);
                    for channel in channels {
                        query = query.bind(channel);
                    }

                    query.fetch_all(pool).await
                }
                None => {
                    sqlx::query_as_unchecked!(
                        Self,
                        "
                        SELECT
                            reminders.id,
                            reminders.uid,
                            channels.channel,
                            reminders.utc_time,
                            reminders.interval_seconds,
                            reminders.interval_days,
                            reminders.interval_months,
                            reminders.expires,
                            reminders.enabled,
                            reminders.content,
                            reminders.embed_description,
                            reminders.set_by
                        FROM
                            reminders
                        LEFT JOIN
                            channels
                        ON
                            channels.id = reminders.channel_id
                        WHERE
                            `status` = 'pending' AND
                            channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
                        ",
                        guild_id.get()
                    )
                    .fetch_all(pool)
                    .await
                }
            }
        } else {
            sqlx::query_as_unchecked!(
                Self,
                "
                SELECT
                    reminders.id,
                    reminders.uid,
                    channels.channel,
                    reminders.utc_time,
                    reminders.interval_seconds,
                    reminders.interval_days,
                    reminders.interval_months,
                    reminders.expires,
                    reminders.enabled,
                    reminders.content,
                    reminders.embed_description,
                    reminders.set_by
                FROM
                    reminders
                INNER JOIN
                    channels
                ON
                    channels.id = reminders.channel_id
                WHERE
                    `status` = 'pending' AND
                    channels.id = (SELECT dm_channel FROM users WHERE id = ?)
                ",
                user.get()
            )
            .fetch_all(pool)
            .await
        }
        .unwrap()
    }

    pub async fn delete(
        &self,
        db: impl Executor<'_, Database = Database>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query!(
            "
            UPDATE reminders SET `status` = 'deleted' WHERE uid = ?
            ",
            self.uid
        )
        .execute(db)
        .await
        .map(|_| ())
    }

    pub fn display_content(&self) -> &str {
        if self.content.is_empty() {
            &self.embed_description
        } else {
            &self.content
        }
    }

    pub fn display_del(&self, count: usize, timezone: &Tz) -> String {
        format!(
            "**{}**: '{}' *<#{}>* at **{}**",
            count + 1,
            self.display_content(),
            self.channel,
            self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S")
        )
    }

    pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String {
        let time_display = match flags.time_display {
            TimeDisplayType::Absolute => {
                self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S").to_string()
            }

            TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
        };

        if self.interval_seconds.is_some()
            || self.interval_days.is_some()
            || self.interval_months.is_some()
        {
            format!(
                "'{}' *occurs next at* **{}**, repeating (set by {})\n",
                self.display_content(),
                time_display,
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
            )
        } else {
            format!(
                "'{}' *occurs next at* **{}** (set by {})\n",
                self.display_content(),
                time_display,
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
            )
        }
    }
}

async fn parse_scopes(ctx: Context<'_>, mentions: &str) -> Vec<ReminderScope> {
    let captures = REGEX_CHANNEL_USER.captures_iter(mentions);
    let mut scopes = vec![];

    for capture in captures {
        let pref = capture.get(1).unwrap().as_str();
        let id = capture.get(2).unwrap().as_str().parse::<u64>().unwrap();

        if pref == "#" {
            let channel_id = ChannelId::new(id);
            let channel = channel_id.to_channel(&ctx).await;

            match channel {
                Ok(channel) => match channel {
                    Channel::Guild(channel) => match channel.kind {
                        ChannelType::PublicThread | ChannelType::PrivateThread => {
                            scopes.push(ReminderScope::Channel(ChannelWithThread {
                                channel_id: channel
                                    .parent_id
                                    .expect("No parent_id for a thread")
                                    .get(),
                                thread_id: Some(id),
                            }));
                        }
                        _ => {
                            scopes.push(ReminderScope::Channel(ChannelWithThread {
                                channel_id: id,
                                thread_id: None,
                            }));
                        }
                    },
                    Channel::Private(channel) => {
                        scopes.push(ReminderScope::User(channel.recipient.id.get()));
                    }
                    _ => {
                        warn!("Unknown channel type");
                    }
                },

                Err(e) => {
                    warn!("Could not get channel {}: {}", id, e);
                    scopes.push(ReminderScope::Channel(ChannelWithThread {
                        channel_id: id,
                        thread_id: None,
                    }))
                }
            }
        } else {
            scopes.push(ReminderScope::User(id));
        }
    }

    scopes
}

pub async fn create_reminder(
    ctx: Context<'_>,
    time: String,
    content: String,
    channels: Option<String>,
    interval: Option<String>,
    expires: Option<String>,
    tts: Option<bool>,
    timezone: Option<Tz>,
) -> Result<(), Error> {
    fn create_response(
        successes: &HashSet<(Reminder, ReminderScope)>,
        errors: &HashSet<ReminderError>,
        time: i64,
    ) -> CreateEmbed {
        let success_part = match successes.len() {
            0 => "".to_string(),
            n => format!(
                "Reminder{s} for {locations} set for <t:{offset}:R>",
                s = if n > 1 { "s" } else { "" },
                locations =
                    successes.iter().map(|(_, l)| l.mention()).collect::<Vec<String>>().join(", "),
                offset = time
            ),
        };

        let error_part = match errors.len() {
            0 => "".to_string(),
            n => format!(
                "{n} reminder{s} failed to set:\n{errors}",
                s = if n > 1 { "s" } else { "" },
                n = n,
                errors = errors.iter().map(|e| e.to_string()).collect::<Vec<String>>().join("\n")
            ),
        };

        CreateEmbed::default()
            .title(format!(
                "{n} Reminder{s} Set",
                n = successes.len(),
                s = if successes.len() > 1 { "s" } else { "" }
            ))
            .description(format!("{}\n\n{}", success_part, error_part))
            .color(*THEME_COLOR)
    }

    if interval.is_none() && expires.is_some() {
        ctx.say("`expires` can only be used with `interval`").await?;

        return Ok(());
    }

    let ephemeral =
        ctx.guild_data().await.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations));
    if ephemeral {
        ctx.defer_ephemeral().await?;
    } else {
        ctx.defer().await?;
    }

    let user_data = ctx.author_data().await.unwrap();
    let timezone = timezone.unwrap_or(ctx.timezone().await);

    let time = match cron_next_timestamp(&time, timezone) {
        Some(ts) => Some(ts),
        None => natural_parser(&time, &timezone.to_string()).await,
    };

    match time {
        Some(time) => {
            let content = {
                let tts = tts.unwrap_or(false);

                Content { content, tts, attachment: None, attachment_name: None }
            };

            let scopes = {
                let list = match channels {
                    Some(channels) => parse_scopes(ctx, &channels).await,
                    None => vec![],
                };

                if list.is_empty() {
                    if let Some(channel) = ctx.guild_channel().await {
                        if channel.kind == ChannelType::PublicThread
                            || channel.kind == ChannelType::PrivateThread
                        {
                            let parent = channel.parent_id.unwrap();
                            let channel_with_threads = ChannelWithThread {
                                channel_id: parent.get(),
                                thread_id: Some(ctx.channel_id().get()),
                            };
                            vec![ReminderScope::Channel(channel_with_threads)]
                        } else {
                            let channel_with_threads = ChannelWithThread {
                                channel_id: ctx.channel_id().get(),
                                thread_id: None,
                            };
                            vec![ReminderScope::Channel(channel_with_threads)]
                        }
                    } else {
                        vec![ReminderScope::User(ctx.author().id.get())]
                    }
                } else {
                    list
                }
            };

            let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
                if check_subscription(&ctx, &ctx.data().database, ctx.author().id, ctx.guild_id())
                    .await
                {
                    (
                        parse_duration(repeat)
                            .or_else(|_| parse_duration(&format!("1 {}", repeat)))
                            .ok(),
                        {
                            if let Some(arg) = &expires {
                                natural_parser(arg, &timezone.to_string()).await
                            } else {
                                None
                            }
                        },
                    )
                } else {
                    ctx.send(CreateReply::default().content(
                        "`interval` is only available to Patreon subscribers or self-hosted users",
                    ))
                    .await?;

                    return Ok(());
                }
            } else {
                (None, None)
            };

            if processed_interval.is_none() && interval.is_some() {
                ctx.send(CreateReply::default().content(
                    "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`",
                ))
                .await?;
            } else if processed_expires.is_none() && expires.is_some() {
                ctx.send(
                    CreateReply::default().ephemeral(true).content(
                        "Expiry time failed to process. Please make it as clear as possible",
                    ),
                )
                .await?;
            } else {
                let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id())
                    .author(user_data)
                    .content(content)
                    .time(time)
                    .timezone(timezone)
                    .expires(processed_expires)
                    .interval(processed_interval);

                builder.set_scopes(scopes);

                let (errors, successes) = builder.build().await;

                let embed = create_response(&successes, &errors, time);

                if successes.len() == 1 {
                    let reminder = successes.iter().next().map(|(r, _)| r.id).unwrap();
                    let undo_button = ComponentDataModel::UndoReminder(UndoReminder {
                        user_id: ctx.author().id,
                        reminder_id: reminder,
                    });

                    ctx.send(CreateReply::default().embed(embed).components(vec![
                        CreateActionRow::Buttons(vec![
                                CreateButton::new(undo_button.to_custom_id())
                                    .emoji(ReactionType::Unicode("🔕".to_string()))
                                    .label("Cancel")
                                    .style(ButtonStyle::Danger),
                                CreateButton::new_link("https://beta.reminder-bot.com/dashboard")
                                    .emoji(ReactionType::Unicode("📝".to_string()))
                                    .label("Edit"),
                            ]),
                    ]))
                    .await?;
                } else {
                    ctx.send(CreateReply::default().embed(embed)).await?;
                }
            }
        }

        None => {
            ctx.say("Time could not be processed").await?;
        }
    }

    Ok(())
}
